Passed
Push — master ( bd33a4...a78440 )
by Camilo
01:57
created

Publish::setPayloadType()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 5
ccs 0
cts 3
cp 0
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
crap 2

1 Method

Rating   Name   Duplication   Size   Complexity  
A Publish::shouldExpectAnswer() 0 3 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace unreal4u\MQTT\Protocol;
6
7
use unreal4u\MQTT\Application\EmptyReadableResponse;
8
use unreal4u\MQTT\Application\Message;
9
use unreal4u\MQTT\Exceptions\InvalidQoSLevel;
10
use unreal4u\MQTT\Internals\ClientInterface;
11
use unreal4u\MQTT\Internals\ProtocolBase;
12
use unreal4u\MQTT\Internals\ReadableContent;
13
use unreal4u\MQTT\Internals\ReadableContentInterface;
14
use unreal4u\MQTT\Internals\WritableContent;
15
use unreal4u\MQTT\Internals\WritableContentInterface;
16
use unreal4u\MQTT\Utilities;
17
18
/**
19
 * A PUBLISH Control Packet is sent from a Client to a Server or vice-versa to transport an Application Message.
20
 *
21
 * @see http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718037
22
 */
23
final class Publish extends ProtocolBase implements ReadableContentInterface, WritableContentInterface
24
{
25
    use ReadableContent;
26
    use WritableContent;
27
28
    const CONTROL_PACKET_VALUE = 3;
29
30
    /**
31
     * Contains the message to be sent
32
     * @var Message
33
     */
34
    private $message;
35
36
    /**
37
     * Some interchanges with the broker will send or receive a packet identifier
38
     * @var int
39
     */
40
    public $packetIdentifier = 0;
41
42
    /**
43
     * Flag to check whether a message is a redelivery (DUP flag)
44
     * @see http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718038
45
     * @var bool
46
     */
47
    public $isRedelivery = false;
48
49 4
    public function createVariableHeader(): string
50
    {
51 4
        if ($this->message === null) {
52 1
            throw new \InvalidArgumentException('You must at least provide a message object with a topic name');
53
        }
54
55 3
        $bitString = $this->createUTF8String($this->message->getTopicName());
56
        // Reset the special flags should the object be reused with another message
57 3
        $this->specialFlags = 0;
58
59 3
        if ($this->isRedelivery) {
60
            $this->logger->debug('Activating redelivery bit');
61
            // DUP flag: if the message is a re-delivery, mark it as such
62
            $this->specialFlags |= 8;
63
        }
64
65
        // Check QoS level and perform the corresponding actions
66 3
        if ($this->message->getQoSLevel() !== 0) {
67
            // 2 for QoS lvl1 and 4 for QoS lvl2
68 2
            $this->specialFlags |= ($this->message->getQoSLevel() * 2);
69 2
            $this->packetIdentifier++;
70 2
            $bitString .= Utilities::convertNumberToBinaryString($this->packetIdentifier);
71 2
            $this->logger->debug(sprintf('Activating QoS level %d bit', $this->message->getQoSLevel()), [
72 2
                'specialFlags' => $this->specialFlags,
73
            ]);
74
        }
75
76 3
        if ($this->message->isRetained()) {
77
            // RETAIN flag: should the server retain the message?
78 1
            $this->specialFlags |= 1;
79 1
            $this->logger->debug('Activating retain flag', ['specialFlags' => $this->specialFlags]);
80
        }
81
82 3
        $this->logger->info('Variable header created', ['specialFlags' => $this->specialFlags]);
83
84 3
        return $bitString;
85
    }
86
87
    public function createPayload(): string
88
    {
89
        if (!$this->message->validateMessage()) {
90
            throw new \InvalidArgumentException('Invalid message');
91
        }
92
93
        return $this->message->getPayload();
94
    }
95
96
    /**
97
     * QoS level 0 does not have to wait for a answer, so return false. Any other QoS level returns true
98
     * @return bool
99
     */
100 4
    public function shouldExpectAnswer(): bool
101
    {
102 4
        return !($this->message->getQoSLevel() === 0);
103
    }
104
105 2
    public function expectAnswer(string $data, ClientInterface $client): ReadableContentInterface
106
    {
107 2
        if ($this->shouldExpectAnswer() === false) {
108 1
            return new EmptyReadableResponse($this->logger);
109
        }
110
111 1
        $pubAck = new PubAck($this->logger);
112 1
        $pubAck->instantiateObject($data);
113 1
        return $pubAck;
114
    }
115
116
    /**
117
     * Sets the to be sent message
118
     *
119
     * @param Message $message
120
     * @return WritableContentInterface
121
     */
122 6
    public function setMessage(Message $message): WritableContentInterface
123
    {
124 6
        $this->message = $message;
125 6
        return $this;
126
    }
127
128
    /**
129
     * Gets the set message
130
     *
131
     * @return Message
132
     */
133
    public function getMessage(): Message
134
    {
135
        return $this->message;
136
    }
137
138
    /**
139
     * Sets several bits and pieces from the first byte of the fixed header for the Publish packet
140
     *
141
     * @param int $firstByte
142
     * @return Publish
143
     * @throws \unreal4u\MQTT\Exceptions\InvalidQoSLevel
144
     */
145
    private function analyzeFirstByte(int $firstByte): Publish
146
    {
147
        $this->logger->debug('Analyzing first byte', [sprintf('%08d', decbin($firstByte))]);
148
        // Retained bit is bit 0 of first byte
149
        $this->message->setRetainFlag(false);
150
        if ($firstByte & 1) {
151
            $this->message->setRetainFlag(true);
152
        }
153
        // QoS level are the last bits 2 & 1 of the first byte
154
        $this->message->setQoSLevel($this->determineIncomingQoSLevel($firstByte));
155
156
        // Duplicate message must be checked only on QoS > 0, else set it to false
157
        $this->isRedelivery = false;
158
        if ($firstByte & 8 && $this->message->getQoSLevel() !== 0) {
159
            // Is a duplicate is always bit 3 of first byte
160
            $this->isRedelivery = true;
161
        }
162
163
        return $this;
164
    }
165
166
    /**
167
     * Finds out the QoS level in a fixed header for the Publish object
168
     *
169
     * @param int $bitString
170
     * @return int
171
     * @throws \unreal4u\MQTT\Exceptions\InvalidQoSLevel
172
     */
173
    private function determineIncomingQoSLevel(int $bitString): int
174
    {
175
        // QoS lvl 6 does not exist, throw exception
176
        if (($bitString & 6) >= 6) {
177
            throw new InvalidQoSLevel('Invalid QoS level "' . $bitString . '" found (both bits set?)');
178
        }
179
180
        // Strange operation, why? Because 4 == QoS lvl2; 2 == QoS lvl1, 0 == QoS lvl0
181
        return $bitString & 4 / 2;
182
    }
183
184
    /**
185
     * Will perform sanity checks and fill in the Readable object with data
186
     * @param string $rawMQTTHeaders
187
     * @return ReadableContentInterface
188
     * @throws \OutOfRangeException
189
     * @throws \unreal4u\MQTT\Exceptions\InvalidQoSLevel
190
     */
191
    public function fillObject(string $rawMQTTHeaders): ReadableContentInterface
192
    {
193
        $this->message = new Message();
194
        $this->analyzeFirstByte(\ord($rawMQTTHeaders{0}));
195
196
        // Topic size is always the 3rd byte
197
        $topicSize = \ord($rawMQTTHeaders{3});
198
199
        $messageStartPosition = 4;
200
        if ($this->message->getQoSLevel() > 0) {
201
            // [2 (fixed header) + 2 (topic size) + $topicSize] marks the beginning of the 2 packet identifier bytes
202
            $this->packetIdentifier = Utilities::convertBinaryStringToNumber(
203
                $rawMQTTHeaders{5 + $topicSize} . $rawMQTTHeaders{4 + $topicSize}
204
            );
205
            $messageStartPosition += 2;
206
        }
207
208
        $this->logger->debug('Determined headers', [
209
            'topicSize' => $topicSize,
210
            'QoSLevel' => $this->message->getQoSLevel(),
211
            'isDuplicate' => $this->isRedelivery,
212
            'isRetained' => $this->message->isRetained(),
213
            'packetIdentifier' => $this->packetIdentifier,
214
        ]);
215
216
        $this->message->setPayload(substr($rawMQTTHeaders, $messageStartPosition + $topicSize));
0 ignored issues
show
Bug introduced by
It seems like substr($rawMQTTHeaders, ...tPosition + $topicSize) can also be of type false; however, parameter $payload of unreal4u\MQTT\Application\Message::setPayload() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

216
        $this->message->setPayload(/** @scrutinizer ignore-type */ substr($rawMQTTHeaders, $messageStartPosition + $topicSize));
Loading history...
217
        $this->message->setTopicName(substr($rawMQTTHeaders, $messageStartPosition, $topicSize));
0 ignored issues
show
Bug introduced by
It seems like substr($rawMQTTHeaders, ...rtPosition, $topicSize) can also be of type false; however, parameter $topicName of unreal4u\MQTT\Application\Message::setTopicName() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

217
        $this->message->setTopicName(/** @scrutinizer ignore-type */ substr($rawMQTTHeaders, $messageStartPosition, $topicSize));
Loading history...
218
219
        return $this;
220
    }
221
222
    /**
223
     * @inheritdoc
224
     * @throws \unreal4u\MQTT\Exceptions\ServerClosedConnection
225
     * @throws \unreal4u\MQTT\Exceptions\NotConnected
226
     * @throws \unreal4u\MQTT\Exceptions\Connect\NoConnectionParametersDefined
227
     */
228
    public function performSpecialActions(ClientInterface $client, WritableContentInterface $originalRequest): bool
229
    {
230
        if ($this->message->getQoSLevel() === 0) {
231
            $this->logger->debug('No response needed', ['qosLevel', $this->message->getQoSLevel()]);
232
        } else {
233
            $client->setBlocking(true);
234
            if ($this->message->getQoSLevel() === 1) {
235
                $this->logger->debug('Responding with PubAck', ['qosLevel' => $this->message->getQoSLevel()]);
236
                $client->sendData($this->composePubAckAnswer());
237
            } elseif ($this->message->getQoSLevel() === 2) {
238
                $this->logger->debug('Responding with PubRec', ['qosLevel' => $this->message->getQoSLevel()]);
239
                $client->sendData(new PubRec($this->logger));
240
            }
241
            $client->setBlocking(false);
242
        }
243
244
        return true;
245
    }
246
247
    /**
248
     * Composes a PubAck answer with the same packetIdentifier as what we received
249
     * @return PubAck
250
     */
251
    private function composePubAckAnswer(): PubAck
252
    {
253
        $pubAck = new PubAck($this->logger);
254
        $pubAck->packetIdentifier = $this->packetIdentifier;
255
        return $pubAck;
256
    }
257
}
258