Issues (1513)

lib/wbxml/wbxmlencoder.php (7 issues)

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
 * WBXMLEncoder encodes to Wap Binary XML
8
 */
9
10
class WBXMLEncoder extends WBXMLDefs {
11
	private $_dtd;
12
	private $_out;
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;
24
	private $multipart; // the content is multipart
25
	private $bodyparts;
26
27
	public function __construct($output, $multipart = false) {
28
		$this->log = SLog::IsWbxmlDebugEnabled();
29
		$this->_out = $output;
30
31
		// reverse-map the DTD
32
		foreach ($this->dtd["namespaces"] as $nsid => $nsname) {
33
			$this->_dtd["namespaces"][$nsname] = $nsid;
34
		}
35
36
		foreach ($this->dtd["codes"] as $cp => $value) {
37
			$this->_dtd["codes"][$cp] = [];
38
			foreach ($this->dtd["codes"][$cp] as $tagid => $tagname) {
39
				$this->_dtd["codes"][$cp][$tagname] = $tagid;
40
			}
41
		}
42
		$this->_stack = [];
43
		$this->multipart = $multipart;
44
		$this->bodyparts = [];
45
	}
46
47
	/**
48
	 * Puts the WBXML header on the stream.
49
	 */
50
	public function startWBXML() {
51
		if ($this->multipart) {
52
			header("Content-Type: application/vnd.ms-sync.multipart");
53
			SLog::Write(LOGLEVEL_DEBUG, "WBXMLEncoder->startWBXML() type: vnd.ms-sync.multipart");
54
		}
55
		else {
56
			header("Content-Type: application/vnd.ms-sync.wbxml");
57
			SLog::Write(LOGLEVEL_DEBUG, "WBXMLEncoder->startWBXML() type: vnd.ms-sync.wbxml");
58
		}
59
60
		$this->outByte(0x03); // WBXML 1.3
61
		$this->outMBUInt(0x01); // Public ID 1
62
		$this->outMBUInt(106); // UTF-8
63
		$this->outMBUInt(0x00); // string table length (0)
64
	}
65
66
	/**
67
	 * Puts a StartTag on the output stack.
68
	 */
69
	public function startTag($tag, $attributes = false, $nocontent = false) {
0 ignored issues
show
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

69
	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...
70
		$stackelem = [];
71
72
		if (!$nocontent) {
73
			$stackelem['tag'] = $tag;
74
			$stackelem['nocontent'] = $nocontent;
75
			$stackelem['sent'] = false;
76
77
			array_push($this->_stack, $stackelem);
78
79
			// If 'nocontent' is specified, then apparently the user wants to force
80
			// output of an empty tag, and we therefore output the stack here
81
		}
82
		else {
83
			$this->_outputStack();
84
			$this->_startTag($tag, $nocontent);
85
		}
86
	}
87
88
	/**
89
	 * Puts an EndTag on the stack.
90
	 */
91
	public function endTag() {
92
		$stackelem = array_pop($this->_stack);
93
94
		// Only output end tags for items that have had a start tag sent
95
		if ($stackelem['sent']) {
96
			$this->_endTag();
97
98
			if (count($this->_stack) == 0) {
99
				SLog::Write(LOGLEVEL_DEBUG, "WBXMLEncoder->endTag() WBXML output completed");
100
			}
101
			if (count($this->_stack) == 0 && $this->multipart == true) {
102
				$this->processMultipart();
103
			}
104
			if (count($this->_stack) == 0) {
105
				$this->writeLog();
106
			}
107
		}
108
	}
109
110
	/**
111
	 * Puts content on the output stack.
112
	 *
113
	 * @param string $content
114
	 */
115
	public function content($content) {
116
		// We need to filter out any \0 chars because it's the string terminator in WBXML. We currently
117
		// cannot send \0 characters within the XML content anywhere.
118
		$content = str_replace("\0", "", $content);
119
		if ("x" . $content == "x") {
120
			return;
121
		}
122
		$this->_outputStack();
123
		$this->_content($content);
124
	}
125
126
	/**
127
	 * Puts content of a stream on the output stack AND closes it.
128
	 *
129
	 * @param resource $stream
130
	 * @param bool     $asBase64 if true, the data will be encoded as base64, default: false
131
	 * @param bool     $opaque   if true, output the opaque data, default: false
132
	 */
133
	public function contentStream($stream, $asBase64 = false, $opaque = false) {
134
		// Do not append filters to opaque data as it might contain null char
135
		if (!$asBase64 && !$opaque) {
136
			stream_filter_register('replacenullchar', 'ReplaceNullcharFilter');
137
			$rnc_filter = stream_filter_append($stream, 'replacenullchar');
138
		}
139
140
		$this->_outputStack();
141
		$this->_contentStream($stream, $asBase64, $opaque);
142
143
		if (!$asBase64 && !$opaque) {
144
			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...
145
		}
146
147
		fclose($stream);
148
	}
149
150
	/**
151
	 * Gets the value of multipart.
152
	 *
153
	 * @return bool
154
	 */
155
	public function getMultipart() {
156
		return $this->multipart;
157
	}
158
159
	/**
160
	 * Adds a bodypart.
161
	 *
162
	 * @param Stream $bp
163
	 */
164
	public function addBodypartStream($bp) {
165
		if (!is_resource($bp)) {
0 ignored issues
show
The condition is_resource($bp) is always false.
Loading history...
166
			throw new WBXMLException("WBXMLEncoder->addBodypartStream(): trying to add a " . gettype($bp) . " instead of a stream");
167
		}
168
		if ($this->multipart) {
169
			$this->bodyparts[] = $bp;
170
		}
171
	}
172
173
	/**
174
	 * Gets the number of bodyparts.
175
	 *
176
	 * @return int
177
	 */
178
	public function getBodypartsCount() {
179
		return count($this->bodyparts);
180
	}
181
182
	/*----------------------------------------------------------------------------------------------------------
183
	 * Private WBXMLEncoder stuff
184
	 */
185
186
	/**
187
	 * Output any tags on the stack that haven't been output yet.
188
	 */
189
	private function _outputStack() {
190
		$stackCount = count($this->_stack);
191
		for ($i = 0; $i < $stackCount; ++$i) {
192
			if (!$this->_stack[$i]['sent']) {
193
				$this->_startTag($this->_stack[$i]['tag'], $this->_stack[$i]['nocontent']);
194
				$this->_stack[$i]['sent'] = true;
195
			}
196
		}
197
	}
198
199
	/**
200
	 * Outputs an actual start tag.
201
	 *
202
	 * @param mixed $tag
203
	 * @param mixed $nocontent
204
	 */
205
	private function _startTag($tag, $nocontent = false) {
206
		if ($this->log) {
207
			$this->logStartTag($tag, $nocontent);
208
		}
209
210
		$mapping = $this->getMapping($tag);
211
212
		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...
$mapping is a non-empty array, thus ! $mapping is always false.
Loading history...
213
			return false;
214
		}
215
216
		if ($this->_tagcp != $mapping["cp"]) {
217
			$this->outSwitchPage($mapping["cp"]);
218
			$this->_tagcp = $mapping["cp"];
219
		}
220
221
		$code = $mapping["code"];
222
223
		if (!isset($nocontent) || !$nocontent) {
224
			$code |= 0x40;
225
		}
226
227
		$this->outByte($code);
228
	}
229
230
	/**
231
	 * Outputs actual data.
232
	 *
233
	 * @param string $content
234
	 */
235
	private function _content($content) {
236
		if ($this->log) {
237
			$this->logContent($content);
238
		}
239
		$this->outByte(self::WBXML_STR_I);
240
		$this->outTermStr($content);
241
	}
242
243
	/**
244
	 * Outputs actual data coming from a stream, optionally encoded as base64.
245
	 *
246
	 * @param resource $stream
247
	 * @param bool     $asBase64
248
	 * @param mixed    $opaque
249
	 */
250
	private function _contentStream($stream, $asBase64, $opaque) {
251
		$stat = fstat($stream);
252
		// write full stream, including the finalizing terminator to the output stream (stuff outTermStr() would do)
253
		if ($opaque) {
254
			$this->outByte(self::WBXML_OPAQUE);
255
			$this->outMBUInt($stat['size']);
256
		}
257
		else {
258
			$this->outByte(self::WBXML_STR_I);
259
		}
260
261
		if ($asBase64) {
262
			$out_filter = stream_filter_append($this->_out, 'convert.base64-encode');
263
		}
264
		$written = stream_copy_to_stream($stream, $this->_out);
265
		if ($asBase64) {
266
			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...
267
		}
268
		if (!$opaque) {
269
			fwrite($this->_out, chr(0));
270
		}
271
272
		if ($this->log) {
273
			// data is out, do some logging
274
			$this->logContent(sprintf("<<< written %d of %d bytes of %s data >>>", $written, $stat['size'], $asBase64 ? "base64 encoded" : "plain"));
275
		}
276
	}
277
278
	/**
279
	 * Outputs an actual end tag.
280
	 */
281
	private function _endTag() {
282
		if ($this->log) {
283
			$this->logEndTag();
284
		}
285
		$this->outByte(self::WBXML_END);
286
	}
287
288
	/**
289
	 * Outputs a byte.
290
	 */
291
	private function outByte($byte) {
292
		fwrite($this->_out, chr($byte));
293
	}
294
295
	/**
296
	 * Output the multibyte integers to the stream.
297
	 *
298
	 * A multi-byte integer consists of a series of octets,
299
	 * where the most significant bit is the continuation flag
300
	 * and the remaining seven bits are a scalar value.
301
	 * The octets are arranged in a big-endian order,
302
	 * eg, the most significant seven bits are transmitted first.
303
	 *
304
	 * @see https://www.w3.org/1999/06/NOTE-wbxml-19990624/#_Toc443384895
305
	 *
306
	 * @param int $uint
307
	 */
308
	private function outMBUInt($uint) {
309
		if ($uint == 0x0) {
310
			return $this->outByte($uint);
0 ignored issues
show
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...
311
		}
312
313
		$out = '';
314
315
		for ($i = 0; $uint != 0; ++$i) {
316
			$byte = $uint & 0x7F;
317
			$uint = $uint >> 7;
318
			if ($i == 0) {
319
				$out = chr($byte) . $out;
320
			}
321
			else {
322
				$out = chr($byte | 0x80) . $out;
323
			}
324
		}
325
		fwrite($this->_out, $out);
326
	}
327
328
	/**
329
	 * Outputs content with string terminator.
330
	 */
331
	private function outTermStr($content) {
332
		fwrite($this->_out, $content);
333
		fwrite($this->_out, chr(0));
334
	}
335
336
	/**
337
	 * Switches the codepage.
338
	 */
339
	private function outSwitchPage($page) {
340
		$this->outByte(self::WBXML_SWITCH_PAGE);
341
		$this->outByte($page);
342
	}
343
344
	/**
345
	 * Get the mapping for a tag.
346
	 *
347
	 * @return array
348
	 */
349
	private function getMapping($tag) {
350
		$mapping = [];
351
352
		$split = $this->splitTag($tag);
353
354
		if (isset($split["ns"])) {
355
			$cp = $this->_dtd["namespaces"][$split["ns"]];
356
		}
357
		else {
358
			$cp = 0;
359
		}
360
361
		$code = $this->_dtd["codes"][$cp][$split["tag"]];
362
363
		$mapping["cp"] = $cp;
364
		$mapping["code"] = $code;
365
366
		return $mapping;
367
	}
368
369
	/**
370
	 * Split a tag from a the fulltag (namespace + tag).
371
	 *
372
	 * @return array keys: 'ns' (namespace), 'tag' (tag)
373
	 */
374
	private function splitTag($fulltag) {
375
		$ns = false;
376
		$pos = strpos($fulltag, chr(58)); // chr(58) == ':'
377
378
		if ($pos) {
379
			$ns = substr($fulltag, 0, $pos);
380
			$tag = substr($fulltag, $pos + 1);
381
		}
382
		else {
383
			$tag = $fulltag;
384
		}
385
386
		$ret = [];
387
		if ($ns) {
388
			$ret["ns"] = $ns;
389
		}
390
		$ret["tag"] = $tag;
391
392
		return $ret;
393
	}
394
395
	/**
396
	 * Logs a StartTag to SLog.
397
	 */
398
	private function logStartTag($tag, $nocontent) {
399
		$spaces = str_repeat(" ", count($this->logStack));
400
		if ($nocontent) {
401
			SLog::Write(LOGLEVEL_WBXML, "O " . $spaces . " <{$tag}/>");
402
		}
403
		else {
404
			array_push($this->logStack, $tag);
405
			SLog::Write(LOGLEVEL_WBXML, "O " . $spaces . " <{$tag}>");
406
		}
407
	}
408
409
	/**
410
	 * Logs a EndTag to SLog.
411
	 */
412
	private function logEndTag() {
413
		$spaces = str_repeat(" ", count($this->logStack));
414
		$tag = array_pop($this->logStack);
415
		SLog::Write(LOGLEVEL_WBXML, "O " . $spaces . "</{$tag}>");
416
	}
417
418
	/**
419
	 * Logs content to SLog.
420
	 *
421
	 * @param string $content
422
	 */
423
	private function logContent($content) {
424
		$spaces = str_repeat(" ", count($this->logStack));
425
		SLog::Write(LOGLEVEL_WBXML, "O " . $spaces . $content);
426
	}
427
428
	/**
429
	 * Processes the multipart response.
430
	 */
431
	private function processMultipart() {
432
		SLog::Write(LOGLEVEL_DEBUG, sprintf("WBXMLEncoder->processMultipart() with %d parts to be processed", $this->getBodypartsCount()));
433
		$len = ob_get_length();
434
		$buffer = ob_get_clean();
435
		$nrBodyparts = $this->getBodypartsCount();
436
		$blockstart = (($nrBodyparts + 1) * 2) * 4 + 4;
437
438
		fwrite($this->_out, pack("iii", $nrBodyparts + 1, $blockstart, $len));
439
440
		foreach ($this->bodyparts as $i => $bp) {
441
			$blockstart = $blockstart + $len;
442
			$len = fstat($bp);
443
			$len = (isset($len['size'])) ? $len['size'] : 0;
444
			if ($len == 0) {
445
				SLog::Write(LOGLEVEL_WARN, sprintf("WBXMLEncoder->processMultipart(): the length of the body part at position %d is 0", $i));
446
			}
447
			fwrite($this->_out, pack("ii", $blockstart, $len));
448
		}
449
450
		fwrite($this->_out, $buffer);
451
452
		foreach ($this->bodyparts as $bp) {
453
			stream_copy_to_stream($bp, $this->_out);
454
			fclose($bp);
455
		}
456
	}
457
458
	/**
459
	 * Writes the sent WBXML data to the log if it is not bigger than 512K.
460
	 */
461
	private function writeLog() {
462
		if (ob_get_length() === false) {
463
			$data = "output buffer disabled";
464
		}
465
		elseif (ob_get_length() < 524288) {
466
			$data = base64_encode(ob_get_contents());
467
		}
468
		else {
469
			$data = "more than 512K of data";
470
		}
471
		SLog::Write(LOGLEVEL_WBXML, "WBXML-OUT: " . $data, false);
472
	}
473
}
474