Gateway::sendE2EFile()   B
last analyzed

Complexity

Conditions 6
Paths 8

Size

Total Lines 39
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 39
rs 8.439
c 0
b 0
f 0
cc 6
eloc 21
nc 8
nop 5
1
<?php
2
/**
3
 * Class Gateway
4
 *
5
 * @filesource   Gateway.php
6
 * @created      10.06.2017
7
 * @package      chillerlan\Threema
8
 * @author       Smiley <[email protected]>
9
 * @copyright    2017 Smiley
10
 * @license      MIT
11
 */
12
13
namespace chillerlan\Threema;
14
15
use chillerlan\Threema\Crypto\CryptoInterface;
16
use chillerlan\Threema\HTTP\HTTPClientInterface;
17
18
class Gateway{
19
20
	const API_BASE = 'https://msgapi.threema.ch';
21
22
	const API_ERRORS = [
23
		400 => 'bad request',
24
		401 => 'unauthorized',
25
		402 => 'no credits remain',
26
		404 => 'not found',
27
		413 => 'message too large',
28
		500 => 'internal server error',
29
	];
30
31
	const HMAC_KEY_EMAIL_BIN = "\x30\xa5\x50\x0f\xed\x97\x01\xfa\x6d\xef\xdb\x61\x08\x41\x90\x0f\xeb\xb8\xe4\x30\x88\x1f\x7a\xd8\x16\x82\x62\x64\xec\x09\xba\xd7";
32
	const HMAC_KEY_PHONE_BIN = "\x85\xad\xf8\x22\x69\x53\xf3\xd9\x6c\xfd\x5d\x09\xbf\x29\x55\x5e\xb9\x55\xfc\xd8\xaa\x5e\xc4\xf9\xfc\xd8\x69\xe2\x58\x37\x07\x23";
33
34
	const FILE_NONCE           = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01";
35
	const FILE_THUMBNAIL_NONCE = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02";
36
37
	/**
38
	 * @var \chillerlan\Threema\HTTP\HTTPClientInterface
39
	 */
40
	protected $HTTP;
41
42
	/**
43
	 * @var \chillerlan\Threema\Crypto\CryptoInterface
44
	 */
45
	protected $crypto;
46
47
	/**
48
	 * @var string
49
	 */
50
	protected $gatewayID;
51
52
	/**
53
	 * @var string
54
	 */
55
	protected $gatewaySecret;
56
57
	/**
58
	 * Gateway constructor.
59
	 *
60
	 * @param \chillerlan\Threema\HTTP\HTTPClientInterface $HTTP
61
	 * @param \chillerlan\Threema\Crypto\CryptoInterface   $crypto
62
	 * @param string                                       $gatewayID
63
	 * @param string                                       $gatewaySecret
64
	 */
65
	public function __construct(HTTPClientInterface $HTTP, CryptoInterface $crypto, string $gatewayID, string $gatewaySecret){
66
		$this->HTTP          = $HTTP;
67
		$this->crypto        = $crypto;
68
		$this->gatewayID     = $gatewayID;
69
		$this->gatewaySecret = $gatewaySecret;
70
	}
71
72
	/**
73
	 * @return array
74
	 */
75
	protected function getAuthParams():array {
76
		return [
77
			'from'   => $this->gatewayID,
78
			'secret' => $this->gatewaySecret,
79
		];
80
	}
81
82
	/**
83
	 * @param       $endpoint
84
	 * @param array $params
85
	 * @param null  $body
86
	 * @param array $headers
87
	 *
88
	 * @return mixed
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use string.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
89
	 * @throws \chillerlan\Threema\EndpointException
90
	 */
91
	protected function getResponse($endpoint, $params = [], $body = null, $headers = []){
92
		$response = $this->HTTP->getResponse(self::API_BASE.$endpoint, $params, !is_null($body) ? 'POST' : 'GET', $body, $headers);
93
94
		if($response->code !== 200){
95
96
			if(array_key_exists($response->code, self::API_ERRORS)){
97
				throw new EndpointException('gateway error: '.self::API_ERRORS[$response->code].' '.print_r($response->body, true));
98
			}
99
100
			throw new EndpointException('unknown error: "compiles on my machine." '.print_r([$response->code, $response->body], true));
101
		}
102
103
		return $response->body;
104
	}
105
106
	/**
107
	 * Get remaining credits
108
	 *
109
	 * URL: https://msgapi.threema.ch/credits?from=<gatewayID>&secret=<gatewaySecret>
110
	 *
111
	 * The API identity and secret must be passed in the corresponding GET parameters for authentication (use URL
112
	 * encoding). The number of credits left on the account that the given ID belongs to will be returned as a
113
	 * text/plain response. Note: several IDs may use the same account, and thus share the same credit balance.
114
	 *
115
	 * Possible HTTP result codes:
116
	 *
117
	 * - 200 (on success)
118
	 * - 401 (if API identity or secret are incorrect)
119
	 * - 500 (if a temporary internal server error occurs)
120
	 *
121
	 * @return int
122
	 */
123
	public function checkCredits():int{
124
		return (int)$this->getResponse('/credits', $this->getAuthParams());
125
	}
126
127
	/**
128
	 *Check file reception capability of an ID
129
	 *
130
	 * Before you send a file to a Threema ID using the blob upload (+ file message), you may want to check whether the
131
	 * recipient uses a Threema version that supports receiving files. The receiver may be using an old version, or a
132
	 * platform where file reception is not supported.
133
	 *
134
	 * URL: https://msgapi.threema.ch/capabilities/<threemaID>?from=<gatewayID>&secret=<gatewaySecret>
135
	 *
136
	 * The API identity and secret must be passed in the corresponding GET parameters for authentication (use URL
137
	 * encoding). The result is a text/plain response of supported capabilities, separated by commas. Currently defined
138
	 * capabilities:
139
	 *
140
	 * - text
141
	 * - image
142
	 * - video
143
	 * - audio
144
	 * - file
145
	 *
146
	 * More capabilities may be added in the future (separated with commas), so you should match on substrings when
147
	 * checking for file. The order in which the capabilities are returned is not defined.
148
	 *
149
	 * Example result: text,image,video,audio,file
150
	 *
151
	 * Possible HTTP result codes:
152
	 *
153
	 * - 200 (on success)
154
	 * - 401 (if API identity or secret are incorrect)
155
	 * - 404 (if no matching ID could be found)
156
	 * - 500 (if a temporary internal server error occurs)
157
	 *
158
	 * @param string $threemaID
159
	 *
160
	 * @return array
161
	 */
162
	public function checkCapabilities(string $threemaID):array{
163
		$response = explode(',', $this->getResponse('/capabilities/'.$this->checkThreemaID($threemaID), $this->getAuthParams()));
164
165
		sort($response);
166
167
		return $response;
168
	}
169
170
	/**
171
	 * Find ID by phone number
172
	 *
173
	 * URL: https://msgapi.threema.ch/lookup/phone/<phoneno>?from=<gatewayID>&secret=<gatewaySecret>
174
	 *
175
	 * The phone number must be passed in E.164 format, without the leading +. The API identity and secret must be
176
	 * passed in the corresponding GET parameters for authentication (use URL encoding).
177
	 * The Threema ID corresponding to the phone number will be returned as a text/plain response.
178
	 *
179
	 * Possible HTTP result codes:
180
	 *
181
	 * - 200 (on success)
182
	 * - 401 (if API identity or secret are incorrect)
183
	 * - 404 (if no matching ID could be found)
184
	 * - 500 (if a temporary internal server error occurs)
185
	 *
186
	 * @param string $phoneno
187
	 *
188
	 * @return string
189
	 */
190
	public function getIdByPhone(string $phoneno):string{
191
		return $this->getResponse('/lookup/phone/'.$this->checkPhoneNo($phoneno), $this->getAuthParams());
192
	}
193
194
	/**
195
	 * URL: https://msgapi.threema.ch/lookup/phone_hash/<phonenoHash>?from=<gatewayID>&secret=<gatewaySecret>
196
	 *
197
	 * The phone number must be passed as an HMAC-SHA256 hash of the E.164 number without the leading +.
198
	 * The HMAC key is 85adf8226953f3d96cfd5d09bf29555eb955fcd8aa5ec4f9fcd869e258370723 (in hexadecimal).
199
	 *
200
	 * Example: the phone number 41791234567 hashes to
201
	 * ad398f4d7ebe63c6550a486cc6e07f9baa09bd9d8b3d8cb9d9be106d35a7fdbc.
202
	 *
203
	 * The API identity and secret must be passed in the corresponding GET parameters for authentication (use URL
204
	 * encoding). The Threema ID corresponding to the phone number will be returned as a text/plain response.
205
	 *
206
	 * Possible HTTP result codes:
207
	 *
208
	 * - 200 (on success)
209
	 * - 400 (if the hash length is wrong)
210
	 * - 401 (if API identity or secret are incorrect)
211
	 * - 404 (if no matching ID could be found)
212
	 * - 500 (if a temporary internal server error occurs)
213
	 *
214
	 * @param string $phonenoHash
215
	 *
216
	 * @return string
217
	 */
218
	public function getIdByPhoneHash(string $phonenoHash):string{
219
		return $this->getResponse('/lookup/phone_hash/'.$this->checkHash($phonenoHash), $this->getAuthParams());
220
	}
221
222
	/**
223
	 * URL: https://msgapi.threema.ch/lookup/email/<email>?from=<gatewayID>&secret=<gatewaySecret>
224
	 *
225
	 * The API identity and secret must be passed in the corresponding GET parameters for authentication (use URL
226
	 * encoding). The Threema ID corresponding to the email address will be returned as a text/plain response.
227
	 *
228
	 * Possible HTTP result codes:
229
	 *
230
	 * - 200 (on success)
231
	 * - 401 (if API identity or secret are incorrect)
232
	 * - 404 (if no matching ID could be found)
233
	 * - 500 (if a temporary internal server error occurs)
234
	 *
235
	 *
236
	 * @param string $email
237
	 *
238
	 * @return string
239
	 */
240
	public function getIdByEmail(string $email):string{
241
		return $this->getResponse('/lookup/email/'.$this->checkEmail($email), $this->getAuthParams());
242
	}
243
244
	/**
245
	 * Find ID by email address hash
246
	 *
247
	 * URL: https://msgapi.threema.ch/lookup/email_hash/<emailHash>?from=<gatewayID>&secret=<gatewaySecret>
248
	 *
249
	 * The lowercased and whitespace-trimmed email address must be hashed with HMAC-SHA256.
250
	 * The HMAC key is 30a5500fed9701fa6defdb610841900febb8e430881f7ad816826264ec09bad7 (in hexadecimal).
251
	 *
252
	 * Example: the email address [email protected] hashes to
253
	 * 1ea093239cc5f0e1b6ec81b866265b921f26dc4033025410063309f4d1a8ee2c.
254
	 *
255
	 * The API identity and secret must be passed in the corresponding GET parameters for authentication (use URL
256
	 * encoding). The Threema ID corresponding to the email address will be returned as a text/plain response.
257
	 *
258
	 * Possible HTTP result codes:
259
	 *
260
	 * - 200 (on success)
261
	 * - 400 (if the hash length is wrong)
262
	 * - 401 (if API identity or secret are incorrect)
263
	 * - 404 (if no matching ID could be found)
264
	 * - 500 (if a temporary internal server error occurs)
265
	 *
266
	 * @param string $emailHash
267
	 *
268
	 * @return string
269
	 */
270
	public function getIdByEmailHash(string $emailHash):string{
271
		return $this->getResponse('/lookup/email_hash/'.$this->checkHash($emailHash), $this->getAuthParams());
272
	}
273
274
	/**
275
	 * @param array $emails
276
	 * @param array $phonenumbers
277
	 *
278
	 * @return mixed
279
	 */
280
/*
0 ignored issues
show
Unused Code Comprehensibility introduced by
61% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
281
	public function bulkLookup($emails = [], $phonenumbers = []){
282
		$lookup = [];
283
284
		foreach($phonenumbers as $phonenumber){
285
			$lookup['phoneHashes'][] = $this->hashPhoneNo($phonenumber);
286
		}
287
288
		foreach($emails as $email){
289
			$lookup['emailHashes'][] = $this->hashEmail($email);
290
		}
291
292
		$r = $this->getResponse('/lookup/bulk', $this->getAuthParams(), json_encode($lookup));
293
294
		return json_decode($r);
295
	}
296
*/
297
298
	/**
299
	 * Key lookups
300
	 *
301
	 * For the end-to-end encrypted mode, you need the public key of the recipient in order to encrypt a message. While
302
	 * it's best to obtain this directly from the recipient (extract it from the QR code), this may not be convenient,
303
	 * and therefore you can also look up the key associated with a given ID from the server.
304
	 *
305
	 * URL: https://msgapi.threema.ch/pubkeys/<threemaID>?from=<gatewayID>&secret=<gatewaySecret>
306
	 *
307
	 * The API identity and secret must be passed in the corresponding GET parameters for authentication (use URL
308
	 * encoding). The public key corresponding to the ID will be returned as a text/plain response (hex encoded).
309
	 *
310
	 * Possible HTTP result codes:
311
	 *
312
	 * - 200 (on success)
313
	 * - 401 (if API identity or secret are incorrect)
314
	 * - 404 (if no matching ID could be found)
315
	 * - 500 (if a temporary internal server error occurs)
316
	 *
317
	 * It is strongly recommended that you cache the public keys to avoid querying the API for each message.
318
	 *
319
	 * @param string $threemaID
320
	 *
321
	 * @return string
322
	 * @throws \chillerlan\Threema\EndpointException
323
	 */
324
	public function getPublicKey(string $threemaID):string{
325
		$threemaID = $this->checkThreemaID($threemaID);
326
327
		if(!$threemaID){
0 ignored issues
show
Bug Best Practice introduced by
The expression $threemaID of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
328
			throw new EndpointException('invalid threema id');
329
		}
330
331
		$response = $this->getResponse('/pubkeys/'.$threemaID, $this->getAuthParams());
332
333
		if(!$response || !$this->checkHash($response)){
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->checkHash($response) of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
334
			throw new EndpointException('invalid public key');
335
		}
336
337
		return $response;
338
	}
339
340
	/**
341
	 * Send Messages, Basic mode
342
	 *
343
	 * URL: https://msgapi.threema.ch/send_simple
344
	 *
345
	 * POST parameters (application/x-www-form-urlencoded):
346
	 *
347
	 * - recipient:   choose one of the following:
348
	 *   - to           recipient identity (8 characters)
349
	 *   - phone        recipient phone number (E.164), without leading +
350
	 *   - email        recipient email address
351
	 * - text         message text, max. 3500 bytes, UTF-8 encoded
352
	 *
353
	 * - from         your API identity (8 characters, usually starts with '*')
354
	 * - secret       API authentication secret
355
	 *
356
	 * By using the phone or email recipient specifiers, one can avoid having to look up the corresponding ID
357
	 * and instead do everything in one call (may be more suitable for SMS gateway style integration).
358
	 *
359
	 * Possible HTTP result codes:
360
	 *
361
	 * - 200 (on success)
362
	 * - 400 (if the recipient identity is invalid or the account is not set up for basic mode)
363
	 * - 401 (if API identity or secret are incorrect)
364
	 * - 402 (if no credits remain)
365
	 * - 404 (if using phone or email as the recipient specifier, and the corresponding recipient could not be found)
366
	 * - 413 (if the message is too long)
367
	 * - 500 (if a temporary internal server error occurs)
368
	 *
369
	 * On success (HTTP 200), the ID of the new message is returned as text/plain.
370
	 *
371
	 * @param string $to
372
	 * @param string $message
373
	 *
374
	 * @return string
375
	 * @throws \chillerlan\Threema\EndpointException
376
	 *
377
	 */
378
/*
0 ignored issues
show
Unused Code Comprehensibility introduced by
62% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
379
	public function sendSimple(string $to, string $message):string{
380
		$params = array_merge($this->getRecipient($to), ['text' => $message], $this->getAuthParams());
381
382
		ksort($params);
383
384
		return $this->getResponse('/send_simple', [], $params);
385
	}
386
*/
387
388
389
	/**
390
	 * @param string $to
391
	 *
392
	 * @return array
393
	 * @throws \chillerlan\Threema\EndpointException
394
	 */
395
	protected function getRecipient(string $to):array {
396
397
		switch(true){
398
			case $x = $this->checkEmail($to):
399
				return ['email' => $x];
0 ignored issues
show
Bug introduced by
The variable $x seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
400
			case $x = $this->checkThreemaID($to):
401
				return ['to' => $x];
0 ignored issues
show
Bug introduced by
The variable $x seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
402
			case $x = $this->checkPhoneNo($to):
403
				return ['phone' => $x];
0 ignored issues
show
Bug introduced by
The variable $x seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
404
			default:
405
				throw new EndpointException('"to" not specified: '.$to);
406
		}
407
408
	}
409
410
	/**
411
	 * End-to-end encrypted mode
412
	 *
413
	 * URL: https://msgapi.threema.ch/send_e2e
414
	 *
415
	 * POST parameters (application/x-www-form-urlencoded):
416
	 *
417
	 * - to      recipient identity (8 characters)
418
	 * - box     encrypted message data (max. 4000 bytes, hex encoded)
419
	 * - nonce   nonce used for encryption (24 bytes, hex encoded)
420
	 *
421
	 * - from    your API identity (8 characters, usually starts with '*')
422
	 * - secret  API authentication secret
423
	 *
424
	 * Possible HTTP result codes:
425
	 *
426
	 * - 200 (on success)
427
	 * - 400 (if the recipient identity is invalid or the account is not set up for end-to-end mode)
428
	 * - 401 (if API identity or secret are incorrect)
429
	 * - 402 (if no credits remain)
430
	 * - 413 (if the message is too long)
431
	 * - 500 (if a temporary internal server error occurs)
432
	 *
433
	 * On success (HTTP 200), the ID of the new message is returned as text/plain.
434
	 *
435
	 * @param string      $recipientThreemaID
436
	 * @param string      $senderPrivateKey
437
	 *
438
	 * @param string      $data
439
	 *
440
	 *
441
	 * @return string
442
	 * @throws \chillerlan\Threema\EndpointException
443
	 */
444
	protected function sendE2E(string $recipientThreemaID, string $senderPrivateKey, string $data):string{
445
		$recipientThreemaID = $this->checkThreemaID($recipientThreemaID);
446
447
		if(!$recipientThreemaID){
0 ignored issues
show
Bug Best Practice introduced by
The expression $recipientThreemaID of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
448
			throw new EndpointException('no threema id given');
449
		}
450
451
		$recipientPubKey = $this->getPublicKey($recipientThreemaID);
452
453
		$params = array_merge(
454
			$this->getAuthParams(),
455
			['to' => $recipientThreemaID],
456
			$this->crypto->createBox($data.$this->crypto->getPadBytes(), $senderPrivateKey, $recipientPubKey)
457
		);
458
459
		ksort($params);
460
461
		return $this->getResponse('/send_e2e', [], $params);
462
	}
463
464
	/**
465
	 * @param string $recipientThreemaID
466
	 * @param string $senderPrivateKey
467
	 * @param string $text
468
	 *
469
	 * @return string
470
	 */
471
	public function sendE2EText(string $recipientThreemaID, string $senderPrivateKey, string $text):string{
472
		return $this->sendE2E($recipientThreemaID, $senderPrivateKey, "\x01".$text);
473
	}
474
475
	/**
476
	 * @param string      $recipientThreemaID
477
	 * @param string      $senderPrivateKey
478
	 * @param string      $file
479
	 * @param string      $description
480
	 * @param string|null $thumbnail
481
	 *
482
	 * @return string
483
	 * @throws \chillerlan\Threema\EndpointException
484
	 */
485
	public function sendE2EFile(string $recipientThreemaID, string $senderPrivateKey, string $file, string $description = '', string $thumbnail = null):string{
486
487
		if(!in_array('file', $this->checkCapabilities($recipientThreemaID))){
488
			throw new EndpointException('given threema id is not capable of receiving files');
489
		}
490
491
		$fileinfo = $this->checkFile($file);
492
493
		if(!is_array($fileinfo)){
494
			throw new EndpointException('invalid file');
495
		}
496
497
		$key = $this->crypto->getRandomBytes(32);
498
499
		$content = [
500
			'b' => $this->upload($this->crypto->createSecretBox(file_get_contents($file), self::FILE_NONCE, $key)),
501
			'k' => $this->crypto->bin2hex($key),
502
			'm' => $fileinfo['mime'],
503
			'n' => $fileinfo['name'],
504
			's' => $fileinfo['size'],
505
			'i' => 0,
506
		];
507
508
		if(!empty($description)){
509
			$content['d'] = $description;
510
		}
511
512
		if($thumbnail){
0 ignored issues
show
Bug Best Practice introduced by
The expression $thumbnail of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
513
			// @todo: autocreate thumbnail
514
			$thumbinfo = $this->checkFile($thumbnail);
515
516
			if(is_array($thumbinfo)){
517
				$content['t'] = $this->upload($this->crypto->createSecretBox(file_get_contents($thumbnail), self::FILE_THUMBNAIL_NONCE, $key));
518
			}
519
520
		}
521
522
		return $this->sendE2E($recipientThreemaID, $senderPrivateKey, "\x17".json_encode($content));
523
	}
524
525
	/**
526
	 * @param string $recipientThreemaID
527
	 * @param string $senderPrivateKey
528
	 * @param string $image
529
	 *
530
	 * @return mixed
531
	 * @throws \chillerlan\Threema\EndpointException
532
	 */
533
/*
0 ignored issues
show
Unused Code Comprehensibility introduced by
62% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
534
	public function sendE2EImage(string $recipientThreemaID, string $senderPrivateKey, string $image){
535
536
		if(!in_array('image', $this->checkCapabilities($recipientThreemaID))){
537
			throw new EndpointException('given threema id is not capable of receiving image');
538
		}
539
540
		$fileinfo = $this->checkFile($image);
541
542
		if(!is_array($fileinfo) || !in_array($fileinfo['mime'], ['image/jpg', 'image/jpeg', 'image/png'])){
543
			throw new EndpointException('invalid image');
544
		}
545
546
		$recipientPubKey = $this->getPublicKey($recipientThreemaID);
547
548
		$box = $this->crypto->createBox(file_get_contents($image), $senderPrivateKey, $recipientPubKey);
549
550
		$message = "\x02".$this->crypto->hex2bin($this->upload($box['box']));
551
		$message .= pack('V', $fileinfo['size']);
552
		$message .= $this->crypto->hex2bin($box['nonce']);
553
554
		$params = array_merge(
555
			$this->getAuthParams(),
556
			['to' => $recipientThreemaID],
557
			$this->crypto->createBox($message.$this->crypto->getPadBytes(), $senderPrivateKey, $recipientPubKey)
558
		);
559
560
		ksort($params);
561
562
		return $this->getResponse('/send_e2e', [], $params);
563
	}
564
*/
565
566
	/**
567
	 * Upload
568
	 *
569
	 * URL: https://msgapi.threema.ch/upload_blob
570
	 *
571
	 * POST parameters (multipart/form-data):
572
	 *
573
	 * - blob    blob data (binary), max. 20 MB
574
	 *
575
	 * GET parameters:
576
	 *
577
	 * - from    your API identity (8 characters, usually starts with '*')
578
	 * - secret  API authentication secret
579
	 *
580
	 * Please note that the authentication parameters must be passed in the request URL,
581
	 * while the actual blob data needs to be sent as a multipart/form-data parameter.
582
	 *
583
	 * Possible HTTP result codes:
584
	 *
585
	 * - 200 (on success)
586
	 * - 400 (if required parameters are missing or the blob is empty)
587
	 * - 401 (if API identity or secret are incorrect)
588
	 * - 402 (if no credits remain)
589
	 * - 413 (if the blob is too big)
590
	 * - 500 (if a temporary internal server error occurs)
591
	 *
592
	 * The ID of the new blob is returned as text/plain. One credit is deducted for the upload of a blob.
593
	 *
594
	 * @param string $blob
595
	 *
596
	 * @return string
597
	 */
598
	protected function upload(string $blob):string{
599
		return $this->getResponse('/upload_blob', $this->getAuthParams(), ['blob' => $blob], ['Content-type' => 'multipart/form-data']);
600
	}
601
602
	/**
603
	 * URL: https://msgapi.threema.ch/blobs/<blobID>
604
	 *
605
	 * GET parameters:
606
	 *
607
	 * - from your API identity (8 characters, usually starts with '*')
608
	 * - secret API authentication secret
609
	 *
610
	 * Possible HTTP result codes:
611
	 *
612
	 * 200 (on success, body is the blob data as application/octet-stream)
613
	 * 401 (if API identity or secret are incorrect)
614
	 * 404 (if no blob with this ID could be found)
615
	 * 500 (if a temporary internal server error occurs)
616
	 *
617
	 * Please note: after a blob download has first been attempted, the blob may be deleted from the server within an
618
	 * hour.
619
	 *
620
	 * @param string $blobID
621
	 *
622
	 * @return mixed
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use string.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
623
	 */
624
	public function download(string $blobID){
625
		return $this->getResponse('/blobs/'.$blobID, $this->getAuthParams());
626
	}
627
628
	/**
629
	 * Hashes an email address for identity lookup.
630
	 *
631
	 * @param string $email the email address
632
	 *
633
	 * @return string the email hash (hex)
634
	 * @throws \chillerlan\Threema\EndpointException
635
	 */
636
	public function hashEmail($email){
637
		$email = $this->checkEmail($email);
638
639
		if(!$email){
0 ignored issues
show
Bug Best Practice introduced by
The expression $email of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
640
			throw new EndpointException('invalid email');
641
		}
642
643
		return $this->crypto->hmac_hash($email, self::HMAC_KEY_EMAIL_BIN);
644
	}
645
646
	/**
647
	 * Hashes an phone number address for identity lookup.
648
	 *
649
	 * @param string $phoneNo the phone number (in E.164 format, no leading +)
650
	 *
651
	 * @return bool|string the phone number hash (hex), false on failure
652
	 * @throws \chillerlan\Threema\EndpointException
653
	 */
654
	public function hashPhoneNo($phoneNo){
655
		$phoneNo = $this->checkPhoneNo($phoneNo);
656
657
		if(!$phoneNo){
0 ignored issues
show
Bug Best Practice introduced by
The expression $phoneNo of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
658
			throw new EndpointException('invalid phonenumber');
659
		}
660
661
		return $this->crypto->hmac_hash($phoneNo, self::HMAC_KEY_PHONE_BIN);
662
	}
663
664
	/**
665
	 * @param string $threemaID
666
	 *
667
	 * @return null|string
668
	 */
669
	protected function checkThreemaID(string $threemaID){
670
		$threemaID = trim($threemaID);
671
672
		if(!preg_match('/^[a-z\d\*]{8}$/i', $threemaID)){
673
			return null;
674
		}
675
676
		return strtoupper($threemaID);
677
	}
678
679
	/**
680
	 * @param string $phoneNo
681
	 *
682
	 * @return null|string
683
	 */
684
	protected function checkPhoneNo(string $phoneNo){
685
		$phoneNo = trim($phoneNo);
686
687
		if(!preg_match('/^[\d]+$/', $phoneNo)){
688
			return null;
689
		}
690
691
		return $phoneNo;
692
	}
693
694
	/**
695
	 * @param $email
696
	 *
697
	 * @return null|string
698
	 */
699
	protected function checkEmail($email){
700
		$email = filter_var(trim($email), FILTER_VALIDATE_EMAIL);
701
702
		if(empty($email)){
703
			return null;
704
		}
705
706
		return strtolower($email);
707
	}
708
709
	/**
710
	 * @param string $hash
711
	 *
712
	 * @return null|string
713
	 */
714
	protected function checkHash(string $hash){
715
		$hash = trim($hash);
716
717
		if(!preg_match('/^[a-f\d]{64}$/i', $hash)){
718
			return null;
719
		}
720
721
		return $hash;
722
	}
723
724
	/**
725
	 * @param string $path
726
	 *
727
	 * @return array|null
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use null|array<string,string|integer>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
728
	 */
729
	protected function checkFile(string $path){
730
731
		if(!file_exists($path) || !is_file($path)){
732
			return null;
733
		}
734
735
		$mime = 'application/octet-stream';
736
737
		if(class_exists('finfo')){
738
			$mime = (new \finfo(FILEINFO_MIME_TYPE ))->file($path);
739
		}
740
		else if(function_exists('mime_content_type')) {
741
			$mime = mime_content_type($path);
742
		}
743
744
		return ['path' => $path, 'name' => basename($path), 'size' => filesize($path), 'mime' => $mime];
745
	}
746
}
747