WBXMLDecoder   F
last analyzed

Complexity

Total Complexity 89

Size/Duplication

Total Lines 484
Duplicated Lines 0 %

Importance

Changes 7
Bugs 2 Features 0
Metric Value
eloc 203
c 7
b 2
f 0
dl 0
loc 484
rs 2
wmc 89

20 Methods

Rating   Name   Duplication   Size   Complexity  
A readRemainingData() 0 3 2
A IsWBXML() 0 2 1
A GetPlainInputStream() 0 2 1
A getToken() 0 15 4
D _getToken() 0 62 26
A getOpaque() 0 11 3
A getByte() 0 4 2
A getTermStr() 0 21 5
A getMapping() 0 10 4
A __construct() 0 26 5
B getElement() 0 34 8
A getMBUInt() 0 17 3
B logToken() 0 35 8
A peek() 0 5 1
A InWhile() 0 13 3
A ResetInWhile() 0 6 2
A getElementStartTag() 0 15 4
A getElementContent() 0 11 2
A getElementEndTag() 0 17 3
A ungetElement() 0 6 2

How to fix   Complexity   

Complex Class

Complex classes like WBXMLDecoder often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use WBXMLDecoder, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * SPDX-License-Identifier: AGPL-3.0-only
5
 * SPDX-FileCopyrightText: Copyright 2007-2016 Zarafa Deutschland GmbH
6
 * SPDX-FileCopyrightText: Copyright 2020-2022 grommunio GmbH
7
 *
8
 * WBXMLDecoder decodes from Wap Binary XML
9
 */
10
11
class WBXMLDecoder extends WBXMLDefs {
12
	private $in;
13
	private $inLog;
0 ignored issues
show
introduced by
The private property $inLog is not used, and could be removed.
Loading history...
14
	private $tagcp = 0;
15
	private $ungetbuffer;
16
	private $log = false;
17
	private $logStack = [];
18
	private $inputBuffer = "";
19
	private $isWBXML = true;
20
	private static $loopCounter = [];
21
	public const MAXLOOP = 5000;
22
	public const VERSION = 0x03;
23
24
	/**
25
	 * Counts the amount of times a code part has been executed.
26
	 * When being executed too often, the code throws a WBMXLException.
27
	 *
28
	 * @param string $name
29
	 *
30
	 * @return bool
31
	 *
32
	 * @throws WBXMLException
33
	 */
34
	public static function InWhile($name) {
35
		if (!isset(self::$loopCounter[$name])) {
36
			self::$loopCounter[$name] = 0;
37
		}
38
		else {
39
			++self::$loopCounter[$name];
40
		}
41
42
		if (self::$loopCounter[$name] > self::MAXLOOP) {
43
			throw new WBXMLException(sprintf("Loop count in while too high, code '%s' exceeded max. amount of permitted loops", $name));
44
		}
45
46
		return true;
47
	}
48
49
	/**
50
	 * Resets the inWhile counter.
51
	 *
52
	 * @param string $name
53
	 *
54
	 * @return bool
55
	 */
56
	public static function ResetInWhile($name) {
57
		if (isset(self::$loopCounter[$name])) {
58
			unset(self::$loopCounter[$name]);
59
		}
60
61
		return true;
62
	}
63
64
	/**
65
	 * WBXML Decode Constructor
66
	 * We only handle ActiveSync WBXML, which is a subset of WBXML.
67
	 *
68
	 * @param stream $input the incoming data stream
69
	 */
70
	public function __construct($input) {
71
		$this->log = SLog::IsWbxmlDebugEnabled();
72
73
		$this->in = $input;
74
75
		$version = $this->getByte();
76
		if ($version != self::VERSION) {
77
			$this->inputBuffer .= chr($version);
78
			$this->isWBXML = false;
79
80
			return;
81
		}
82
83
		$publicid = $this->getMBUInt();
84
		if ($publicid !== 1) {
85
			throw new WBXMLException("Wrong publicid : " . $publicid);
86
		}
87
88
		$charsetid = $this->getMBUInt();
89
		if ($charsetid !== 106) {
90
			throw new WBXMLException("Wrong charset : " . $charsetid);
91
		}
92
93
		$stringtablesize = $this->getMBUInt();
94
		if ($stringtablesize !== 0) {
95
			throw new WBXMLException("Wrong string table size : " . $stringtablesize);
96
		}
97
	}
98
99
	/**
100
	 * Returns either start, content or end, and auto-concatenates successive content.
101
	 *
102
	 * @return element|value
0 ignored issues
show
Bug introduced by
The type element was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
103
	 */
104
	public function getElement() {
105
		$element = $this->getToken();
106
		if (is_null($element)) {
107
			return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type element|value.
Loading history...
108
		}
109
110
		switch ($element[EN_TYPE]) {
111
			case EN_TYPE_STARTTAG:
112
				return $element;
113
114
			case EN_TYPE_ENDTAG:
115
				return $element;
116
117
			case EN_TYPE_CONTENT:
118
				WBXMLDecoder::ResetInWhile("decoderGetElement");
119
				while (WBXMLDecoder::InWhile("decoderGetElement")) {
120
					$next = $this->getToken();
121
					if ($next == false) {
122
						return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type element|value.
Loading history...
123
					}
124
					if ($next[EN_TYPE] == EN_CONTENT) {
125
						$element[EN_CONTENT] .= $next[EN_CONTENT];
126
					}
127
					else {
128
						$this->ungetElement($next);
129
130
						break;
131
					}
132
				}
133
134
				return $element;
135
		}
136
137
		return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type element|value.
Loading history...
138
	}
139
140
	/**
141
	 * Get a peek at the next element.
142
	 *
143
	 * @return element
144
	 */
145
	public function peek() {
146
		$element = $this->getElement();
147
		$this->ungetElement($element);
148
149
		return $element;
150
	}
151
152
	/**
153
	 * Get the element of a StartTag.
154
	 *
155
	 * @param mixed $tag
156
	 *
157
	 * @return bool|element returns false if not available
158
	 */
159
	public function getElementStartTag($tag) {
160
		$element = $this->getToken();
161
162
		if (!$element) {
0 ignored issues
show
introduced by
$element is of type token, thus it always evaluated to true.
Loading history...
163
			return false;
164
		}
165
166
		if ($element[EN_TYPE] == EN_TYPE_STARTTAG && $element[EN_TAG] == $tag) {
167
			return $element;
168
		}
169
170
		SLog::Write(LOGLEVEL_WBXMLSTACK, sprintf("WBXMLDecoder->getElementStartTag(): unmatched WBXML tag: '%s' matching '%s' type '%s' flags '%s'", $tag, $element[EN_TAG] ?? "", $element[EN_TYPE] ?? "", $element[EN_FLAGS] ?? ""));
171
		$this->ungetElement($element);
172
173
		return false;
174
	}
175
176
	/**
177
	 * Get the element of a EndTag.
178
	 *
179
	 * @return bool|element returns false if not available
180
	 */
181
	public function getElementEndTag() {
182
		$element = $this->getToken();
183
184
		if ($element[EN_TYPE] == EN_TYPE_ENDTAG) {
185
			return $element;
186
		}
187
188
		SLog::Write(LOGLEVEL_WBXMLSTACK, sprintf("WBXMLDecoder->getElementEndTag(): unmatched WBXML tag: '%s' type '%s' flags '%s'", $element[EN_TAG] ?? "", $element[EN_TYPE] ?? "", $element[EN_FLAGS] ?? ""));
189
190
		$bt = debug_backtrace();
191
		SLog::Write(LOGLEVEL_ERROR, sprintf("WBXMLDecoder->getElementEndTag(): could not read end tag in '%s'. Please enable the LOGLEVEL_WBXML and send the log to the grommunio-sync dev team.", $bt[0]["file"] . ":" . $bt[0]["line"]));
192
193
		// log the remaining wbxml content
194
		$this->ungetElement($element);
195
		while ($el = $this->getElement());
0 ignored issues
show
Unused Code introduced by
The assignment to $el is dead and can be removed.
Loading history...
196
197
		return false;
198
	}
199
200
	/**
201
	 * Get the content of an element.
202
	 *
203
	 * @return bool|string returns false if not available
204
	 */
205
	public function getElementContent() {
206
		$element = $this->getToken();
207
208
		if ($element[EN_TYPE] == EN_TYPE_CONTENT) {
209
			return $element[EN_CONTENT];
210
		}
211
212
		SLog::Write(LOGLEVEL_WBXMLSTACK, sprintf("WBXMLDecoder->getElementContent(): unmatched WBXML content: '%s' type '%s' flags '%s'", $element[EN_TAG] ?? "", $element[EN_TYPE] ?? "", $element[EN_FLAGS] ?? ""));
213
		$this->ungetElement($element);
214
215
		return false;
216
	}
217
218
	/**
219
	 * 'Ungets' an element writing it into a buffer to be 'get' again.
220
	 *
221
	 * @param element $element the element to get ungetten
222
	 */
223
	public function ungetElement($element) {
224
		if ($this->ungetbuffer) {
225
			SLog::Write(LOGLEVEL_ERROR, sprintf("WBXMLDecoder->ungetElement(): WBXML double unget on tag: '%s' type '%s' flags '%s'", $element[EN_TAG] ?? "", $element[EN_TYPE] ?? "", $element[EN_FLAGS] ?? ""));
226
		}
227
228
		$this->ungetbuffer = $element;
229
	}
230
231
	/**
232
	 * Returns the plain input stream.
233
	 *
234
	 * @return string
235
	 */
236
	public function GetPlainInputStream() {
237
		return $this->inputBuffer . stream_get_contents($this->in);
0 ignored issues
show
Bug introduced by
$this->in of type stream is incompatible with the type resource expected by parameter $stream of stream_get_contents(). ( Ignorable by Annotation )

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

237
		return $this->inputBuffer . stream_get_contents(/** @scrutinizer ignore-type */ $this->in);
Loading history...
238
	}
239
240
	/**
241
	 * Returns if the input is WBXML.
242
	 *
243
	 * @return bool
244
	 */
245
	public function IsWBXML() {
246
		return $this->isWBXML;
247
	}
248
249
	/**
250
	 * Reads the remaining data from the input stream.
251
	 */
252
	public function readRemainingData() {
253
		SLog::Write(LOGLEVEL_DEBUG, "WBXMLDecoder->readRemainingData() reading remaining data from input stream");
254
		while ($this->getElement());
255
	}
256
257
	/*----------------------------------------------------------------------------------------------------------
258
	 * Private WBXMLDecoder stuff
259
	 */
260
261
	/**
262
	 * Returns the next token.
263
	 *
264
	 * @return token
0 ignored issues
show
Bug introduced by
The type token was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
265
	 */
266
	private function getToken() {
267
		// See if there's something in the ungetBuffer
268
		if ($this->ungetbuffer) {
269
			$element = $this->ungetbuffer;
270
			$this->ungetbuffer = false;
271
272
			return $element;
273
		}
274
275
		$el = $this->_getToken();
276
		if ($this->log && $el) {
277
			$this->logToken($el);
0 ignored issues
show
Bug introduced by
$el of type array is incompatible with the type string expected by parameter $el of WBXMLDecoder::logToken(). ( Ignorable by Annotation )

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

277
			$this->logToken(/** @scrutinizer ignore-type */ $el);
Loading history...
278
		}
279
280
		return $el;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $el also could return the type array which is incompatible with the documented return type token.
Loading history...
281
	}
282
283
	/**
284
	 * Log the a token to SLog.
285
	 *
286
	 * @param string $el token
287
	 */
288
	private function logToken($el) {
289
		$spaces = str_repeat(" ", count($this->logStack));
290
291
		switch ($el[EN_TYPE]) {
292
			case EN_TYPE_STARTTAG:
293
				if ($el[EN_FLAGS] & EN_FLAGS_CONTENT) {
294
					SLog::Write(LOGLEVEL_WBXML, sprintf("I %s <%s>", $spaces, $el[EN_TAG]));
295
					array_push($this->logStack, $el[EN_TAG]);
296
				}
297
				else {
298
					SLog::Write(LOGLEVEL_WBXML, sprintf("I %s <%s/>", $spaces, $el[EN_TAG]));
299
				}
300
				break;
301
302
			case EN_TYPE_ENDTAG:
303
				$tag = array_pop($this->logStack);
304
				SLog::Write(LOGLEVEL_WBXML, sprintf("I %s</%s>", $spaces, $tag));
305
				break;
306
307
			case EN_TYPE_CONTENT:
308
				$messagesize = strlen($el[EN_CONTENT]);
309
				// don't log binary data
310
				if (mb_detect_encoding($el[EN_CONTENT], null, true) === false) {
311
					$content = sprintf("(BINARY DATA: %d bytes long)", $messagesize);
312
				}
313
				// truncate logged data to 10K
314
				elseif ($messagesize > 10240 && !defined('WBXML_DEBUGGING')) {
315
					$content = sprintf("%s (log message with %d bytes truncated)", substr($el[EN_CONTENT], 0, 10240), $messagesize);
316
				}
317
				else {
318
					$content = $el[EN_CONTENT];
319
				}
320
321
				SLog::Write(LOGLEVEL_WBXML, sprintf("I %s %s", $spaces, $content), false);
322
				break;
323
		}
324
	}
325
326
	/**
327
	 * Returns either a start tag, content or end tag.
328
	 */
329
	private function _getToken() {
330
		// Get the data from the input stream
331
		$element = [];
332
333
		WBXMLDecoder::ResetInWhile("decoderGetToken");
334
		while (WBXMLDecoder::InWhile("decoderGetToken")) {
335
			$byte = fread($this->in, 1);
0 ignored issues
show
Bug introduced by
$this->in of type stream is incompatible with the type resource expected by parameter $stream of fread(). ( Ignorable by Annotation )

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

335
			$byte = fread(/** @scrutinizer ignore-type */ $this->in, 1);
Loading history...
336
			if ($byte === "" || $byte === false) {
337
				break;
338
			}
339
			$byte = ord($byte);
340
341
			switch ($byte) {
342
				case self::WBXML_SWITCH_PAGE:
343
					$this->tagcp = $this->getByte();
344
					break;
345
346
				case self::WBXML_END:
347
					$element[EN_TYPE] = EN_TYPE_ENDTAG;
348
349
					return $element;
350
351
				case self::WBXML_STR_I:
352
					$element[EN_TYPE] = EN_TYPE_CONTENT;
353
					$element[EN_CONTENT] = $this->getTermStr();
354
355
					return $element;
356
357
				case self::WBXML_OPAQUE:
358
					$length = $this->getMBUInt();
359
					$element[EN_TYPE] = EN_TYPE_CONTENT;
360
					$element[EN_CONTENT] = $this->getOpaque($length);
361
362
					return $element;
363
364
				case self::WBXML_ENTITY:
365
				case self::WBXML_LITERAL:
366
				case self::WBXML_EXT_I_0:
367
				case self::WBXML_EXT_I_1:
368
				case self::WBXML_EXT_I_2:
369
				case self::WBXML_PI:
370
				case self::WBXML_LITERAL_C:
371
				case self::WBXML_EXT_T_0:
372
				case self::WBXML_EXT_T_1:
373
				case self::WBXML_EXT_T_2:
374
				case self::WBXML_STR_T:
375
				case self::WBXML_LITERAL_A:
376
				case self::WBXML_EXT_0:
377
				case self::WBXML_EXT_1:
378
				case self::WBXML_EXT_2:
379
				case self::WBXML_LITERAL_AC:
380
					throw new WBXMLException("Invalid token :" . $byte);
381
382
				default:
383
					if ($byte & self::WBXML_WITH_ATTRIBUTES) {
384
						throw new WBXMLException("Attributes are not allowed :" . $byte);
385
					}
386
					$element[EN_TYPE] = EN_TYPE_STARTTAG;
387
					$element[EN_TAG] = $this->getMapping($this->tagcp, $byte & 0x3F);
388
					$element[EN_FLAGS] = ($byte & self::WBXML_WITH_CONTENT ? EN_FLAGS_CONTENT : 0);
389
390
					return $element;
391
			}
392
		}
393
	}
394
395
	/**
396
	 * Reads from the stream until getting a string terminator.
397
	 *
398
	 * @return string
399
	 */
400
	private function getTermStr() {
401
		if (defined('WBXML_DEBUGGING') && WBXML_DEBUGGING === true) {
0 ignored issues
show
Bug introduced by
The constant WBXML_DEBUGGING was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
402
			$str = "";
403
			while (1) {
404
				$in = $this->getByte();
405
				if ($in == 0) {
406
					break;
407
				}
408
409
				$str .= chr($in);
410
			}
411
412
			return $str;
413
		}
414
415
		// there is no unlimited "length" for stream_get_line,
416
		// so we use a huge value for "length" param (1Gb)
417
		// (0 == PHP_SOCK_CHUNK_SIZE (8192))
418
		// internally php read at most PHP_SOCK_CHUNK_SIZE at a time,
419
		// so we can use a huge value for "length" without problem
420
		return stream_get_line($this->in, 1073741824, "\0");
0 ignored issues
show
Bug introduced by
$this->in of type stream is incompatible with the type resource expected by parameter $stream of stream_get_line(). ( Ignorable by Annotation )

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

420
		return stream_get_line(/** @scrutinizer ignore-type */ $this->in, 1073741824, "\0");
Loading history...
421
	}
422
423
	/**
424
	 * Reads $len from the input stream.
425
	 *
426
	 * @param int $len
427
	 *
428
	 * @return string
429
	 */
430
	private function getOpaque($len) {
431
		$d = stream_get_contents($this->in, $len);
0 ignored issues
show
Bug introduced by
$this->in of type stream is incompatible with the type resource expected by parameter $stream of stream_get_contents(). ( Ignorable by Annotation )

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

431
		$d = stream_get_contents(/** @scrutinizer ignore-type */ $this->in, $len);
Loading history...
432
		if ($d === false) {
433
			throw new HTTPReturnCodeException("WBXMLDecoder->getOpaque(): stream_get_contents === false", HTTP_CODE_500, null, LOGLEVEL_WARN);
434
		}
435
		$l = strlen($d);
436
		if ($l !== $len) {
437
			throw new HTTPReturnCodeException("WBXMLDecoder->getOpaque(): only {$l} byte read instead of {$len}", HTTP_CODE_500, null, LOGLEVEL_WARN);
438
		}
439
440
		return $d;
441
	}
442
443
	/**
444
	 * Reads one byte from the input stream.
445
	 *
446
	 * @return int|void
447
	 */
448
	private function getByte() {
449
		$ch = fread($this->in, 1);
0 ignored issues
show
Bug introduced by
$this->in of type stream is incompatible with the type resource expected by parameter $stream of fread(). ( Ignorable by Annotation )

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

449
		$ch = fread(/** @scrutinizer ignore-type */ $this->in, 1);
Loading history...
450
		if (strlen($ch) > 0) {
451
			return ord($ch);
452
		}
453
	}
454
455
	/**
456
	 * Reads string length from the input stream.
457
	 */
458
	private function getMBUInt() {
459
		$uint = 0;
460
461
		while (1) {
462
			$byte = $this->getByte();
463
464
			$uint |= $byte & 0x7F;
465
466
			if ($byte & 0x80) {
467
				$uint = $uint << 7;
468
			}
469
			else {
470
				break;
471
			}
472
		}
473
474
		return $uint;
475
	}
476
477
	/**
478
	 * Returns the mapping for a specified codepage and id.
479
	 *
480
	 * @param       $cp codepage
0 ignored issues
show
Bug introduced by
The type codepage was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
481
	 * @param mixed $id
482
	 *
483
	 * @return string
484
	 */
485
	private function getMapping($cp, $id) {
486
		if (!isset($this->dtd["codes"][$cp]) || !isset($this->dtd["codes"][$cp][$id])) {
487
			return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type string.
Loading history...
488
		}
489
490
		if (isset($this->dtd["namespaces"][$cp])) {
491
			return $this->dtd["namespaces"][$cp] . ":" . $this->dtd["codes"][$cp][$id];
492
		}
493
494
		return $this->dtd["codes"][$cp][$id];
495
	}
496
}
497