WBXMLEncoder   F
last analyzed

Complexity

Total Complexity 69

Size/Duplication

Total Lines 475
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 175
c 3
b 0
f 0
dl 0
loc 475
rs 2.88
wmc 69

25 Methods

Rating   Name   Duplication   Size   Complexity  
A _content() 0 6 2
A getMapping() 0 18 2
A outSwitchPage() 0 3 1
A splitTag() 0 19 3
A getMultipart() 0 2 1
A _outputStack() 0 6 3
A writeLog() 0 11 3
A startWBXML() 0 14 2
A endTag() 0 15 6
A __construct() 0 16 4
A _endTag() 0 5 2
A logStartTag() 0 8 2
A _startTag() 0 23 6
A addBodypartStream() 0 6 3
A logEndTag() 0 4 1
B _contentStream() 0 25 7
A content() 0 9 2
A outMBUInt() 0 18 4
A startTag() 0 16 2
A processMultipart() 0 24 4
A outTermStr() 0 3 1
A getBodypartsCount() 0 2 1
A outByte() 0 2 1
A contentStream() 0 15 5
A logContent() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like WBXMLEncoder 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 WBXMLEncoder, 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
 * WBXMLEncoder encodes to Wap Binary XML
9
 */
10
11
class WBXMLEncoder extends WBXMLDefs {
12
	private $_dtd;
13
	private $_tagcp = 0;
14
	private $log = false;
15
	private $logStack = [];
16
17
	// We use a delayed output mechanism in which we only output a tag when it actually has something
18
	// in it. This can cause entire XML trees to disappear if they don't have output data in them; Ie
19
	// calling 'startTag' 10 times, and then 'endTag' will cause 0 bytes of output apart from the header.
20
21
	// Only when content() is called do we output the current stack of tags
22
23
	private $_stack; // the content is multipart
24
	private $bodyparts;
25
26
	public function __construct(private $_out, private $multipart = false) {
27
		$this->log = SLog::IsWbxmlDebugEnabled();
28
29
		// reverse-map the DTD
30
		foreach ($this->dtd["namespaces"] as $nsid => $nsname) {
31
			$this->_dtd["namespaces"][$nsname] = $nsid;
32
		}
33
34
		foreach ($this->dtd["codes"] as $cp => $value) {
35
			$this->_dtd["codes"][$cp] = [];
36
			foreach ($this->dtd["codes"][$cp] as $tagid => $tagname) {
37
				$this->_dtd["codes"][$cp][$tagname] = $tagid;
38
			}
39
		}
40
		$this->_stack = [];
41
		$this->bodyparts = [];
42
	}
43
44
	/**
45
	 * Puts the WBXML header on the stream.
46
	 */
47
	public function startWBXML() {
48
		if ($this->multipart) {
49
			header("Content-Type: application/vnd.ms-sync.multipart");
50
			SLog::Write(LOGLEVEL_DEBUG, "WBXMLEncoder->startWBXML() type: vnd.ms-sync.multipart");
51
		}
52
		else {
53
			header("Content-Type: application/vnd.ms-sync.wbxml");
54
			SLog::Write(LOGLEVEL_DEBUG, "WBXMLEncoder->startWBXML() type: vnd.ms-sync.wbxml");
55
		}
56
57
		$this->outByte(0x03); // WBXML 1.3
58
		$this->outMBUInt(0x01); // Public ID 1
59
		$this->outMBUInt(106); // UTF-8
60
		$this->outMBUInt(0x00); // string table length (0)
61
	}
62
63
	/**
64
	 * Puts a StartTag on the output stack.
65
	 *
66
	 * @param mixed $tag
67
	 * @param mixed $attributes
68
	 * @param mixed $nocontent
69
	 */
70
	public function startTag($tag, $attributes = false, $nocontent = false) {
0 ignored issues
show
Unused Code introduced by
The parameter $attributes is not used and could be removed. ( Ignorable by Annotation )

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

70
	public function startTag($tag, /** @scrutinizer ignore-unused */ $attributes = false, $nocontent = false) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
71
		$stackelem = [];
72
73
		if (!$nocontent) {
74
			$stackelem['tag'] = $tag;
75
			$stackelem['nocontent'] = $nocontent;
76
			$stackelem['sent'] = false;
77
78
			array_push($this->_stack, $stackelem);
79
80
		// If 'nocontent' is specified, then apparently the user wants to force
81
		// output of an empty tag, and we therefore output the stack here
82
		}
83
		else {
84
			$this->_outputStack();
85
			$this->_startTag($tag, $nocontent);
86
		}
87
	}
88
89
	/**
90
	 * Puts an EndTag on the stack.
91
	 */
92
	public function endTag() {
93
		$stackelem = array_pop($this->_stack);
94
95
		// Only output end tags for items that have had a start tag sent
96
		if ($stackelem['sent']) {
97
			$this->_endTag();
98
99
			if (count($this->_stack) == 0) {
100
				SLog::Write(LOGLEVEL_DEBUG, "WBXMLEncoder->endTag() WBXML output completed");
101
			}
102
			if (count($this->_stack) == 0 && $this->multipart == true) {
103
				$this->processMultipart();
104
			}
105
			if (count($this->_stack) == 0) {
106
				$this->writeLog();
107
			}
108
		}
109
	}
110
111
	/**
112
	 * Puts content on the output stack.
113
	 *
114
	 * @param string $content
115
	 */
116
	public function content($content) {
117
		// We need to filter out any \0 chars because it's the string terminator in WBXML. We currently
118
		// cannot send \0 characters within the XML content anywhere.
119
		$content = str_replace("\0", "", $content);
120
		if ("x" . $content == "x") {
121
			return;
122
		}
123
		$this->_outputStack();
124
		$this->_content($content);
125
	}
126
127
	/**
128
	 * Puts content of a stream on the output stack AND closes it.
129
	 *
130
	 * @param resource $stream
131
	 * @param bool     $asBase64 if true, the data will be encoded as base64, default: false
132
	 * @param bool     $opaque   if true, output the opaque data, default: false
133
	 */
134
	public function contentStream($stream, $asBase64 = false, $opaque = false) {
135
		// Do not append filters to opaque data as it might contain null char
136
		if (!$asBase64 && !$opaque) {
137
			stream_filter_register('replacenullchar', 'ReplaceNullcharFilter');
138
			$rnc_filter = stream_filter_append($stream, 'replacenullchar');
139
		}
140
141
		$this->_outputStack();
142
		$this->_contentStream($stream, $asBase64, $opaque);
143
144
		if (!$asBase64 && !$opaque) {
145
			stream_filter_remove($rnc_filter);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $rnc_filter does not seem to be defined for all execution paths leading up to this point.
Loading history...
146
		}
147
148
		fclose($stream);
149
	}
150
151
	/**
152
	 * Gets the value of multipart.
153
	 *
154
	 * @return bool
155
	 */
156
	public function getMultipart() {
157
		return $this->multipart;
158
	}
159
160
	/**
161
	 * Adds a bodypart.
162
	 *
163
	 * @param Stream $bp
164
	 */
165
	public function addBodypartStream($bp) {
166
		if (!is_resource($bp)) {
0 ignored issues
show
introduced by
The condition is_resource($bp) is always false.
Loading history...
167
			throw new WBXMLException("WBXMLEncoder->addBodypartStream(): trying to add a " . gettype($bp) . " instead of a stream");
168
		}
169
		if ($this->multipart) {
170
			$this->bodyparts[] = $bp;
171
		}
172
	}
173
174
	/**
175
	 * Gets the number of bodyparts.
176
	 *
177
	 * @return int
178
	 */
179
	public function getBodypartsCount() {
180
		return count($this->bodyparts);
181
	}
182
183
	/*----------------------------------------------------------------------------------------------------------
184
	 * Private WBXMLEncoder stuff
185
	 */
186
187
	/**
188
	 * Output any tags on the stack that haven't been output yet.
189
	 */
190
	private function _outputStack() {
191
		$stackCount = count($this->_stack);
192
		for ($i = 0; $i < $stackCount; ++$i) {
193
			if (!$this->_stack[$i]['sent']) {
194
				$this->_startTag($this->_stack[$i]['tag'], $this->_stack[$i]['nocontent']);
195
				$this->_stack[$i]['sent'] = true;
196
			}
197
		}
198
	}
199
200
	/**
201
	 * Outputs an actual start tag.
202
	 *
203
	 * @param mixed $tag
204
	 * @param mixed $nocontent
205
	 */
206
	private function _startTag($tag, $nocontent = false) {
207
		if ($this->log) {
208
			$this->logStartTag($tag, $nocontent);
209
		}
210
211
		$mapping = $this->getMapping($tag);
212
213
		if (!$mapping) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $mapping of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
introduced by
$mapping is a non-empty array, thus ! $mapping is always false.
Loading history...
214
			return false;
215
		}
216
217
		if ($this->_tagcp != $mapping["cp"]) {
218
			$this->outSwitchPage($mapping["cp"]);
219
			$this->_tagcp = $mapping["cp"];
220
		}
221
222
		$code = $mapping["code"];
223
224
		if (!isset($nocontent) || !$nocontent) {
225
			$code |= 0x40;
226
		}
227
228
		$this->outByte($code);
229
	}
230
231
	/**
232
	 * Outputs actual data.
233
	 *
234
	 * @param string $content
235
	 */
236
	private function _content($content) {
237
		if ($this->log) {
238
			$this->logContent($content);
239
		}
240
		$this->outByte(self::WBXML_STR_I);
241
		$this->outTermStr($content);
242
	}
243
244
	/**
245
	 * Outputs actual data coming from a stream, optionally encoded as base64.
246
	 *
247
	 * @param resource $stream
248
	 * @param bool     $asBase64
249
	 * @param mixed    $opaque
250
	 */
251
	private function _contentStream($stream, $asBase64, $opaque) {
252
		$stat = fstat($stream);
253
		// write full stream, including the finalizing terminator to the output stream (stuff outTermStr() would do)
254
		if ($opaque) {
255
			$this->outByte(self::WBXML_OPAQUE);
256
			$this->outMBUInt($stat['size']);
257
		}
258
		else {
259
			$this->outByte(self::WBXML_STR_I);
260
		}
261
262
		if ($asBase64) {
263
			$out_filter = stream_filter_append($this->_out, 'convert.base64-encode');
264
		}
265
		$written = stream_copy_to_stream($stream, $this->_out);
266
		if ($asBase64) {
267
			stream_filter_remove($out_filter);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $out_filter does not seem to be defined for all execution paths leading up to this point.
Loading history...
268
		}
269
		if (!$opaque) {
270
			fwrite($this->_out, chr(0));
271
		}
272
273
		if ($this->log) {
274
			// data is out, do some logging
275
			$this->logContent(sprintf("<<< written %d of %d bytes of %s data >>>", $written, $stat['size'], $asBase64 ? "base64 encoded" : "plain"));
276
		}
277
	}
278
279
	/**
280
	 * Outputs an actual end tag.
281
	 */
282
	private function _endTag() {
283
		if ($this->log) {
284
			$this->logEndTag();
285
		}
286
		$this->outByte(self::WBXML_END);
287
	}
288
289
	/**
290
	 * Outputs a byte.
291
	 *
292
	 * @param mixed $byte
293
	 */
294
	private function outByte($byte) {
295
		fwrite($this->_out, chr($byte));
296
	}
297
298
	/**
299
	 * Output the multibyte integers to the stream.
300
	 *
301
	 * A multi-byte integer consists of a series of octets,
302
	 * where the most significant bit is the continuation flag
303
	 * and the remaining seven bits are a scalar value.
304
	 * The octets are arranged in a big-endian order,
305
	 * eg, the most significant seven bits are transmitted first.
306
	 *
307
	 * @see https://www.w3.org/1999/06/NOTE-wbxml-19990624/#_Toc443384895
308
	 *
309
	 * @param int $uint
310
	 */
311
	private function outMBUInt($uint) {
312
		if ($uint == 0x0) {
313
			return $this->outByte($uint);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->outByte($uint) targeting WBXMLEncoder::outByte() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
314
		}
315
316
		$out = '';
317
318
		for ($i = 0; $uint != 0; ++$i) {
319
			$byte = $uint & 0x7F;
320
			$uint = $uint >> 7;
321
			if ($i == 0) {
322
				$out = chr($byte) . $out;
323
			}
324
			else {
325
				$out = chr($byte | 0x80) . $out;
326
			}
327
		}
328
		fwrite($this->_out, $out);
329
	}
330
331
	/**
332
	 * Outputs content with string terminator.
333
	 *
334
	 * @param mixed $content
335
	 */
336
	private function outTermStr($content) {
337
		fwrite($this->_out, (string) $content);
338
		fwrite($this->_out, chr(0));
339
	}
340
341
	/**
342
	 * Switches the codepage.
343
	 *
344
	 * @param mixed $page
345
	 */
346
	private function outSwitchPage($page) {
347
		$this->outByte(self::WBXML_SWITCH_PAGE);
348
		$this->outByte($page);
349
	}
350
351
	/**
352
	 * Get the mapping for a tag.
353
	 *
354
	 * @param mixed $tag
355
	 *
356
	 * @return array
357
	 */
358
	private function getMapping($tag) {
359
		$mapping = [];
360
361
		$split = $this->splitTag($tag);
362
363
		if (isset($split["ns"])) {
364
			$cp = $this->_dtd["namespaces"][$split["ns"]];
365
		}
366
		else {
367
			$cp = 0;
368
		}
369
370
		$code = $this->_dtd["codes"][$cp][$split["tag"]];
371
372
		$mapping["cp"] = $cp;
373
		$mapping["code"] = $code;
374
375
		return $mapping;
376
	}
377
378
	/**
379
	 * Split a tag from a the fulltag (namespace + tag).
380
	 *
381
	 * @param mixed $fulltag
382
	 *
383
	 * @return array keys: 'ns' (namespace), 'tag' (tag)
384
	 */
385
	private function splitTag($fulltag) {
386
		$ns = false;
387
		$pos = strpos((string) $fulltag, chr(58)); // chr(58) == ':'
388
389
		if ($pos) {
390
			$ns = substr((string) $fulltag, 0, $pos);
391
			$tag = substr((string) $fulltag, $pos + 1);
392
		}
393
		else {
394
			$tag = $fulltag;
395
		}
396
397
		$ret = [];
398
		if ($ns) {
399
			$ret["ns"] = $ns;
400
		}
401
		$ret["tag"] = $tag;
402
403
		return $ret;
404
	}
405
406
	/**
407
	 * Logs a StartTag to SLog.
408
	 *
409
	 * @param mixed $tag
410
	 * @param mixed $nocontent
411
	 */
412
	private function logStartTag($tag, $nocontent) {
413
		$spaces = str_repeat(" ", count($this->logStack));
414
		if ($nocontent) {
415
			SLog::Write(LOGLEVEL_WBXML, "O " . $spaces . " <{$tag}/>");
416
		}
417
		else {
418
			array_push($this->logStack, $tag);
419
			SLog::Write(LOGLEVEL_WBXML, "O " . $spaces . " <{$tag}>");
420
		}
421
	}
422
423
	/**
424
	 * Logs a EndTag to SLog.
425
	 */
426
	private function logEndTag() {
427
		$spaces = str_repeat(" ", count($this->logStack));
428
		$tag = array_pop($this->logStack);
429
		SLog::Write(LOGLEVEL_WBXML, "O " . $spaces . "</{$tag}>");
430
	}
431
432
	/**
433
	 * Logs content to SLog.
434
	 *
435
	 * @param string $content
436
	 */
437
	private function logContent($content) {
438
		$spaces = str_repeat(" ", count($this->logStack));
439
		SLog::Write(LOGLEVEL_WBXML, "O " . $spaces . $content);
440
	}
441
442
	/**
443
	 * Processes the multipart response.
444
	 */
445
	private function processMultipart() {
446
		SLog::Write(LOGLEVEL_DEBUG, sprintf("WBXMLEncoder->processMultipart() with %d parts to be processed", $this->getBodypartsCount()));
447
		$len = ob_get_length();
448
		$buffer = ob_get_clean();
449
		$nrBodyparts = $this->getBodypartsCount();
450
		$blockstart = (($nrBodyparts + 1) * 2) * 4 + 4;
451
452
		fwrite($this->_out, pack("iii", $nrBodyparts + 1, $blockstart, $len));
453
454
		foreach ($this->bodyparts as $i => $bp) {
455
			$blockstart = $blockstart + $len;
456
			$len = fstat($bp);
457
			$len = $len['size'] ?? 0;
458
			if ($len == 0) {
459
				SLog::Write(LOGLEVEL_WARN, sprintf("WBXMLEncoder->processMultipart(): the length of the body part at position %d is 0", $i));
460
			}
461
			fwrite($this->_out, pack("ii", $blockstart, $len));
462
		}
463
464
		fwrite($this->_out, $buffer);
465
466
		foreach ($this->bodyparts as $bp) {
467
			stream_copy_to_stream($bp, $this->_out);
468
			fclose($bp);
469
		}
470
	}
471
472
	/**
473
	 * Writes the sent WBXML data to the log if it is not bigger than 512K.
474
	 */
475
	private function writeLog() {
476
		if (ob_get_length() === false) {
477
			$data = "output buffer disabled";
478
		}
479
		elseif (ob_get_length() < 524288) {
480
			$data = base64_encode(ob_get_contents());
481
		}
482
		else {
483
			$data = "more than 512K of data";
484
		}
485
		SLog::Write(LOGLEVEL_WBXML, "WBXML-OUT: " . $data, false);
486
	}
487
}
488