Completed
Push — master ( fb03b4...646b2a )
by smiley
02:45
created

CLIRunner::parseDocBlock()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 22
Code Lines 14

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 22
rs 8.9197
cc 4
eloc 14
nc 4
nop 1
1
<?php
2
/**
3
 * Class CLIRunner
4
 *
5
 * @filesource   CLIRunner.php
6
 * @created      01.04.2016
7
 * @package      chillerlan\Threema
8
 * @author       Smiley <[email protected]>
9
 * @copyright    2016 Smiley
10
 * @license      MIT
11
 */
12
13
namespace chillerlan\Threema;
14
15
use chillerlan\Threema\{
16
	Crypto\CryptoInterface, Endpoint\EndpointException, Endpoint\EndpointInterface
17
};
18
use ReflectionClass;
19
use ReflectionMethod;
20
use stdClass;
21
22
/**
23
 *
24
 */
25
class CLIRunner implements CLIRunnerInterface{
26
27
	/**
28
	 * @var array
29
	 */
30
	const COMMANDS = [
31
		// local
32
		'keypair'    => 'getKeypair',
33
		'hash_email' => 'hashEmail',
34
		'hash_phone' => 'hashPhone',
35
		'encrypt'    => 'encryptFile',
36
		'decrypt'    => 'decryptFile',
37
		// network
38
		'credits'    => 'checkCredits',
39
		'check'      => 'checkCapabilities',
40
		'email2id'   => 'getIdByEmail',
41
		'phone2id'   => 'getIdByPhone',
42
		'id2pubkey'  => 'getPubkeyById',
43
		'send'       => 'sendMessage',
44
	];
45
46
	/**
47
	 * @var \chillerlan\Threema\Crypto\CryptoInterface
48
	 */
49
	protected $cryptoInterface;
50
51
	/**
52
	 * @var \chillerlan\Threema\Endpoint\EndpointInterface
53
	 */
54
	protected $endpointInterface;
55
56
	/**
57
	 * @var \ReflectionClass
58
	 */
59
	protected $reflection;
60
61
	/**
62
	 * @var array[\ReflectionMethod]
63
	 */
64
	protected $CLIRunnerInterfaceMap;
65
66
	/**
67
	 * Gateway constructor.
68
	 *
69
	 * @param \chillerlan\Threema\Endpoint\EndpointInterface $endpointInterface
70
	 * @param \chillerlan\Threema\Crypto\CryptoInterface     $cryptoInterface
71
	 */
72
	public function __construct(EndpointInterface $endpointInterface, CryptoInterface $cryptoInterface){
73
		$this->endpointInterface = $endpointInterface;
74
		$this->cryptoInterface   = $cryptoInterface;
75
		$this->mapMethods();
76
	}
77
78
	/**
79
	 * @inheritdoc
80
	 */
81
	protected function mapMethods(){
82
		$this->reflection = new ReflectionClass(CLIRunnerInterface::class);
83
84
		foreach($this->reflection->getMethods() as $method){
85
			$this->CLIRunnerInterfaceMap[$method->name] = $method;
86
		}
87
	}
88
89
	/**
90
	 * @param array $arguments $_SERVER['argc']
91
	 *
92
	 * @return string
93
	 */
94
	public function run(array $arguments):string{
95
		array_shift($arguments); // shift the scriptname off the top
96
		$command = strtolower(array_shift($arguments));
97
98
		if(array_key_exists($command, self::COMMANDS) && array_key_exists(self::COMMANDS[$command], $this->CLIRunnerInterfaceMap)){
99
			try{
100
				$method = $this->CLIRunnerInterfaceMap[self::COMMANDS[$command]];
101
				$method = new ReflectionMethod($this, $method->name);
102
103
				// @todo: check method arguments
104
				return $this->log2cli($method->invokeArgs($this, $arguments));
105
			}
106
			catch(EndpointException $gatewayException){
107
				return $this->log2cli('ERROR: '.$gatewayException->getMessage());
108
			}
109
		}
110
111
		return $this->log2cli($this->help());
112
	}
113
114
	/**
115
	 * output a string, wrap at 100 chars
116
	 *
117
	 * @param string $string string to output
118
	 *
119
	 * @return string
120
	 */
121
	private function log2cli(string $string):string{
122
		return PHP_EOL.wordwrap($string, 78, PHP_EOL).PHP_EOL;
123
	}
124
125
	/** @noinspection PhpUnusedPrivateMethodInspection
126
	 *
127
	 * @return string
128
	 * @codeCoverageIgnore
129
	 */
130
	private function readStdIn(){
1 ignored issue
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
131
		$stdin = fopen('php://stdin', 'r');
132
		$lines = [];
133
134
		while($line = trim(fgets($stdin))){
135
136
			if(strlen($line) === 0 || $line === "\n"){
137
				continue;
138
			}
139
140
			$lines[] = $line;
141
		}
142
143
		return implode("\n", $lines);
144
	}
145
146
	/**
147
	 * @param array $params
148
	 *
149
	 * @return \stdClass
150
	 */
151
	private function parseDocBlock(array $params):stdClass{
152
		$parsed             = new stdClass;
153
		$parsed->paramNames = [];
154
		$parsed->paramDoc   = [];
155
		$parsed->returnDoc  = '';
156
157
		if(empty($params)){
158
			return $parsed; // @codeCoverageIgnore
159
		}
160
161
		foreach($params as $p){
162
			$p = explode(' ', $p, 2);
163
			if(isset($p[1])){
164
				$this->parseDocTag($p, $parsed);
165
			}
166
		}
167
168
		$parsed->paramNames = implode(' ', $parsed->paramNames);
169
		$parsed->paramDoc   = implode(PHP_EOL, $parsed->paramDoc);
170
171
		return $parsed;
172
	}
173
174
	/**
175
	 * @param array      $tag
176
	 * @param \stdClass &$out
177
	 */
178
	private function parseDocTag(array $tag, stdClass &$out){
179
		switch($tag[0]){
180
			case 'param':
181
				$tag               = (explode(' ', trim($tag[1]), 3));
182
				$name              = '<'.trim($tag[1], ' $').'>';
183
				$out->paramNames[] = $name;
184
				$out->paramDoc[]   = isset($tag[2]) ? $name.' '.trim($tag[2]) : $name;
185
				break;
186
			case 'return':
187
				$tag = explode(' ', trim($tag[1]), 2);
188
				$out->returnDoc .= isset($tag[1]) ? trim($tag[1]) : '';
189
				break;
190
		}
191
	}
192
193
	/**
194
	 * @return string
195
	 */
196
	public function help():string{
197
		// return info in case no command was found
198
		$help = 'Threema Gateway CLI tool.'.PHP_EOL;
199
		$help .= 'Crypto: '.$this->cryptoInterface->version().PHP_EOL.PHP_EOL;
200
201
		foreach(self::COMMANDS as $command => $method){
202
			$comment = $this->reflection->getMethod($method)->getDocComment();
203
			$comment = str_replace(["\t", ' *'], '', substr($comment, 3, -2));
204
205
			$params  = explode('@', $comment);
206
			$comment = trim(array_shift($params));
207
			$parsed  = $this->parseDocBlock($params);
208
209
			$help .= PHP_EOL.'threema.php '.$command.' '.$parsed->paramNames.PHP_EOL;
210
			$help .= str_repeat('-', strlen($command) + 12).PHP_EOL;
211
			$help .= PHP_EOL.$comment.PHP_EOL;
212
			$help .= PHP_EOL.$parsed->paramDoc.PHP_EOL;
213
			$help .= PHP_EOL.'Returns: '.$parsed->returnDoc.PHP_EOL.PHP_EOL;
214
		}
215
216
		return $help;
217
	}
218
219
	/**
220
	 * @param string $path
221
	 * @param string $data
222
	 *
223
	 * @return string
224
	 */
225
	protected function writeFile(string $path, string $data):string{
226
		// @todo: check writable
227
		if(is_dir(dirname($path))){
228
			$bytes = file_put_contents($path, $data);
229
			return $bytes.' bytes written to: '.$path.PHP_EOL;
230
		}
231
		// or is not writable
232
		return $path.' does not exist'; // @codeCoverageIgnore
233
	}
234
235
	/**
236
	 * @inheritdoc
237
	 */
238
	public function getKeypair(string $privateKeyFile = null, string $publicKeyFile = null):string{
239
		$keypair  = $this->cryptoInterface->getKeypair();
240
		$message  = !empty($privateKeyFile) ? $this->writeFile($privateKeyFile, $keypair->privateKey) : '';
241
		$message .= !empty($publicKeyFile)  ? $this->writeFile($publicKeyFile, $keypair->publicKey)   : '';
242
243
		return $message.PHP_EOL.'private:'.$keypair->privateKey.PHP_EOL.'public:'.$keypair->publicKey;
244
	}
245
246
	/**
247
	 * @inheritdoc
248
	 */
249
	public function hashEmail(string $email):string{
250
		return $this->cryptoInterface->hmac_hash($email, EndpointInterface::HMAC_KEY_EMAIL_BIN);
251
	}
252
253
	/**
254
	 * @inheritdoc
255
	 */
256
	public function hashPhone(string $phoneNo):string{
257
		return $this->cryptoInterface->hmac_hash($phoneNo, EndpointInterface::HMAC_KEY_PHONE_BIN);
258
	}
259
260
	/**
261
	 * @inheritdoc
262
	 */
263
	public function checkCredits():string{
264
		return $this->endpointInterface->checkCredits();
265
	}
266
267
	/**
268
	 * @inheritdoc
269
	 */
270
	public function checkCapabilities(string $threemaID):string{
271
		return implode(',', $this->endpointInterface->checkCapabilities($threemaID));
272
	}
273
274
	/**
275
	 * @inheritdoc
276
	 */
277
	public function getIdByEmail(string $email):string{
278
		return $this->endpointInterface->getIdByEmailHash($this->hashEmail($email));
279
	}
280
281
	/**
282
	 * @inheritdoc
283
	 */
284
	public function getIdByPhone(string $phoneNo):string{
285
		return $this->endpointInterface->getIdByPhoneHash($this->hashPhone($phoneNo));
286
	}
287
288
	/**
289
	 * @inheritdoc
290
	 */
291
	public function getPubkeyById(string $threemaID):string{
292
		return $this->endpointInterface->getPublicKey($threemaID);
293
	}
294
295
	/**
296
	 * @inheritdoc
297
	 */
298
	public function encryptFile(string $privateKey, string $publicKey, string $plaintextFile, string $encryptedFile):string{
299
		$encrypted = $this->cryptoInterface->encrypt(file_get_contents($plaintextFile), $privateKey, $publicKey);
300
		return $this->writeFile($encryptedFile, $encrypted->nonce."\n".$encrypted->box);
301
	}
302
303
	/**
304
	 * @inheritdoc
305
	 */
306
	public function decryptFile(string $privateKey, string $publicKey, string $encryptedFile, string $decryptedFile):string{
307
		$data = explode("\n", file_get_contents($encryptedFile));
308
		return $this->writeFile($decryptedFile, $this->cryptoInterface->decrypt(trim($data[1]), trim($data[0]), $privateKey, $publicKey));
309
	}
310
311
	/**
312
	 * @inheritdoc
313
	 */
314
	public function sendMessage(string $toThreemaID, string $message):string{
315
		// TODO: Implement sendMessage() method.
316
	}
317
}
318