WBXMLDecoder   F
last analyzed

Complexity

Total Complexity 101

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 101

20 Methods

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

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
 * SPDX-License-Identifier: AGPL-3.0-only
4
 * SPDX-FileCopyrightText: Copyright 2007-2016 Zarafa Deutschland GmbH
5
 * SPDX-FileCopyrightText: Copyright 2020-2022 grommunio GmbH
6
 *
7
 * WBXMLDecoder decodes from Wap Binary XML
8
 */
9
10
class WBXMLDecoder extends WBXMLDefs {
11
	private $in;
12
	private $inLog;
0 ignored issues
show
introduced by
The private property $inLog is not used, and could be removed.
Loading history...
13
	private $tagcp = 0;
14
	private $ungetbuffer;
15
	private $log = false;
16
	private $logStack = [];
17
	private $inputBuffer = "";
18
	private $isWBXML = true;
19
	private static $loopCounter = [];
20
	public const MAXLOOP = 5000;
21
	public const VERSION = 0x03;
22
23
	/**
24
	 * Counts the amount of times a code part has been executed.
25
	 * When being executed too often, the code throws a WBMXLException.
26
	 *
27
	 * @param string $name
28
	 *
29
	 * @return bool
30
	 *
31
	 * @throws WBXMLException
32
	 */
33
	public static function InWhile($name) {
34
		if (!isset(self::$loopCounter[$name])) {
35
			self::$loopCounter[$name] = 0;
36
		}
37
		else {
38
			++self::$loopCounter[$name];
39
		}
40
41
		if (self::$loopCounter[$name] > self::MAXLOOP) {
42
			throw new WBXMLException(sprintf("Loop count in while too high, code '%s' exceeded max. amount of permitted loops", $name));
43
		}
44
45
		return true;
46
	}
47
48
	/**
49
	 * Resets the inWhile counter.
50
	 *
51
	 * @param string $name
52
	 *
53
	 * @return bool
54
	 */
55
	public static function ResetInWhile($name) {
56
		if (isset(self::$loopCounter[$name])) {
57
			unset(self::$loopCounter[$name]);
58
		}
59
60
		return true;
61
	}
62
63
	/**
64
	 * WBXML Decode Constructor
65
	 * We only handle ActiveSync WBXML, which is a subset of WBXML.
66
	 *
67
	 * @param stream $input the incoming data stream
68
	 */
69
	public function __construct($input) {
70
		$this->log = SLog::IsWbxmlDebugEnabled();
71
72
		$this->in = $input;
73
74
		$version = $this->getByte();
75
		if ($version != self::VERSION) {
76
			$this->inputBuffer .= chr($version);
77
			$this->isWBXML = false;
78
79
			return;
80
		}
81
82
		$publicid = $this->getMBUInt();
83
		if ($publicid !== 1) {
84
			throw new WBXMLException("Wrong publicid : " . $publicid);
85
		}
86
87
		$charsetid = $this->getMBUInt();
88
		if ($charsetid !== 106) {
89
			throw new WBXMLException("Wrong charset : " . $charsetid);
90
		}
91
92
		$stringtablesize = $this->getMBUInt();
93
		if ($stringtablesize !== 0) {
94
			throw new WBXMLException("Wrong string table size : " . $stringtablesize);
95
		}
96
	}
97
98
	/**
99
	 * Returns either start, content or end, and auto-concatenates successive content.
100
	 *
101
	 * @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...
102
	 */
103
	public function getElement() {
104
		$element = $this->getToken();
105
		if (is_null($element)) {
106
			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...
107
		}
108
109
		switch ($element[EN_TYPE]) {
110
			case EN_TYPE_STARTTAG:
111
				return $element;
112
113
			case EN_TYPE_ENDTAG:
114
				return $element;
115
116
			case EN_TYPE_CONTENT:
117
				WBXMLDecoder::ResetInWhile("decoderGetElement");
118
				while (WBXMLDecoder::InWhile("decoderGetElement")) {
119
					$next = $this->getToken();
120
					if ($next == false) {
121
						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...
122
					}
123
					if ($next[EN_TYPE] == EN_CONTENT) {
124
						$element[EN_CONTENT] .= $next[EN_CONTENT];
125
					}
126
					else {
127
						$this->ungetElement($next);
128
129
						break;
130
					}
131
				}
132
133
				return $element;
134
		}
135
136
		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...
137
	}
138
139
	/**
140
	 * Get a peek at the next element.
141
	 *
142
	 * @return element
143
	 */
144
	public function peek() {
145
		$element = $this->getElement();
146
		$this->ungetElement($element);
147
148
		return $element;
149
	}
150
151
	/**
152
	 * Get the element of a StartTag.
153
	 *
154
	 * @param mixed $tag
155
	 *
156
	 * @return bool|element returns false if not available
157
	 */
158
	public function getElementStartTag($tag) {
159
		$element = $this->getToken();
160
161
		if (!$element) {
0 ignored issues
show
introduced by
$element is of type token, thus it always evaluated to true.
Loading history...
162
			return false;
163
		}
164
165
		if ($element[EN_TYPE] == EN_TYPE_STARTTAG && $element[EN_TAG] == $tag) {
166
			return $element;
167
		}
168
169
		SLog::Write(LOGLEVEL_WBXMLSTACK, sprintf("WBXMLDecoder->getElementStartTag(): unmatched WBXML tag: '%s' matching '%s' type '%s' flags '%s'", $tag, (isset($element[EN_TAG])) ? $element[EN_TAG] : "", (isset($element[EN_TYPE])) ? $element[EN_TYPE] : "", (isset($element[EN_FLAGS])) ? $element[EN_FLAGS] : ""));
170
		$this->ungetElement($element);
171
172
		return false;
173
	}
174
175
	/**
176
	 * Get the element of a EndTag.
177
	 *
178
	 * @return bool|element returns false if not available
179
	 */
180
	public function getElementEndTag() {
181
		$element = $this->getToken();
182
183
		if ($element[EN_TYPE] == EN_TYPE_ENDTAG) {
184
			return $element;
185
		}
186
187
		SLog::Write(LOGLEVEL_WBXMLSTACK, sprintf("WBXMLDecoder->getElementEndTag(): unmatched WBXML tag: '%s' type '%s' flags '%s'", (isset($element[EN_TAG])) ? $element[EN_TAG] : "", (isset($element[EN_TYPE])) ? $element[EN_TYPE] : "", (isset($element[EN_FLAGS])) ? $element[EN_FLAGS] : ""));
188
189
		$bt = debug_backtrace();
190
		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"]));
191
192
		// log the remaining wbxml content
193
		$this->ungetElement($element);
194
		while ($el = $this->getElement());
0 ignored issues
show
Unused Code introduced by
The assignment to $el is dead and can be removed.
Loading history...
195
196
		return false;
197
	}
198
199
	/**
200
	 * Get the content of an element.
201
	 *
202
	 * @return bool|string returns false if not available
203
	 */
204
	public function getElementContent() {
205
		$element = $this->getToken();
206
207
		if ($element[EN_TYPE] == EN_TYPE_CONTENT) {
208
			return $element[EN_CONTENT];
209
		}
210
211
		SLog::Write(LOGLEVEL_WBXMLSTACK, sprintf("WBXMLDecoder->getElementContent(): unmatched WBXML content: '%s' type '%s' flags '%s'", (isset($element[EN_TAG])) ? $element[EN_TAG] : "", (isset($element[EN_TYPE])) ? $element[EN_TYPE] : "", (isset($element[EN_FLAGS])) ? $element[EN_FLAGS] : ""));
212
		$this->ungetElement($element);
213
214
		return false;
215
	}
216
217
	/**
218
	 * 'Ungets' an element writing it into a buffer to be 'get' again.
219
	 *
220
	 * @param element $element the element to get ungetten
221
	 */
222
	public function ungetElement($element) {
223
		if ($this->ungetbuffer) {
224
			SLog::Write(LOGLEVEL_ERROR, sprintf("WBXMLDecoder->ungetElement(): WBXML double unget on tag: '%s' type '%s' flags '%s'", (isset($element[EN_TAG])) ? $element[EN_TAG] : "", (isset($element[EN_TYPE])) ? $element[EN_TYPE] : "", (isset($element[EN_FLAGS])) ? $element[EN_FLAGS] : ""));
225
		}
226
227
		$this->ungetbuffer = $element;
228
	}
229
230
	/**
231
	 * Returns the plain input stream.
232
	 *
233
	 * @return string
234
	 */
235
	public function GetPlainInputStream() {
236
		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

236
		return $this->inputBuffer . stream_get_contents(/** @scrutinizer ignore-type */ $this->in);
Loading history...
237
	}
238
239
	/**
240
	 * Returns if the input is WBXML.
241
	 *
242
	 * @return bool
243
	 */
244
	public function IsWBXML() {
245
		return $this->isWBXML;
246
	}
247
248
	/**
249
	 * Reads the remaining data from the input stream.
250
	 */
251
	public function readRemainingData() {
252
		SLog::Write(LOGLEVEL_DEBUG, "WBXMLDecoder->readRemainingData() reading remaining data from input stream");
253
		while ($this->getElement());
254
	}
255
256
	/*----------------------------------------------------------------------------------------------------------
257
	 * Private WBXMLDecoder stuff
258
	 */
259
260
	/**
261
	 * Returns the next token.
262
	 *
263
	 * @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...
264
	 */
265
	private function getToken() {
266
		// See if there's something in the ungetBuffer
267
		if ($this->ungetbuffer) {
268
			$element = $this->ungetbuffer;
269
			$this->ungetbuffer = false;
270
271
			return $element;
272
		}
273
274
		$el = $this->_getToken();
275
		if ($this->log && $el) {
276
			$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

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

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

419
		return stream_get_line(/** @scrutinizer ignore-type */ $this->in, 1073741824, "\0");
Loading history...
420
	}
421
422
	/**
423
	 * Reads $len from the input stream.
424
	 *
425
	 * @param int $len
426
	 *
427
	 * @return string
428
	 */
429
	private function getOpaque($len) {
430
		$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

430
		$d = stream_get_contents(/** @scrutinizer ignore-type */ $this->in, $len);
Loading history...
431
		if ($d === false) {
432
			throw new HTTPReturnCodeException("WBXMLDecoder->getOpaque(): stream_get_contents === false", HTTP_CODE_500, null, LOGLEVEL_WARN);
433
		}
434
		$l = strlen($d);
435
		if ($l !== $len) {
436
			throw new HTTPReturnCodeException("WBXMLDecoder->getOpaque(): only {$l} byte read instead of {$len}", HTTP_CODE_500, null, LOGLEVEL_WARN);
437
		}
438
439
		return $d;
440
	}
441
442
	/**
443
	 * Reads one byte from the input stream.
444
	 *
445
	 * @return int|void
446
	 */
447
	private function getByte() {
448
		$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

448
		$ch = fread(/** @scrutinizer ignore-type */ $this->in, 1);
Loading history...
449
		if (strlen($ch) > 0) {
450
			return ord($ch);
451
		}
452
	}
453
454
	/**
455
	 * Reads string length from the input stream.
456
	 */
457
	private function getMBUInt() {
458
		$uint = 0;
459
460
		while (1) {
461
			$byte = $this->getByte();
462
463
			$uint |= $byte & 0x7F;
464
465
			if ($byte & 0x80) {
466
				$uint = $uint << 7;
467
			}
468
			else {
469
				break;
470
			}
471
		}
472
473
		return $uint;
474
	}
475
476
	/**
477
	 * Returns the mapping for a specified codepage and id.
478
	 *
479
	 * @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...
480
	 * @param mixed $id
481
	 *
482
	 * @return string
483
	 */
484
	private function getMapping($cp, $id) {
485
		if (!isset($this->dtd["codes"][$cp]) || !isset($this->dtd["codes"][$cp][$id])) {
486
			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...
487
		}
488
489
		if (isset($this->dtd["namespaces"][$cp])) {
490
			return $this->dtd["namespaces"][$cp] . ":" . $this->dtd["codes"][$cp][$id];
491
		}
492
493
		return $this->dtd["codes"][$cp][$id];
494
	}
495
}
496