Streamer::jsonSerialize()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 7
nop 0
dl 0
loc 11
rs 10
c 1
b 0
f 0
nc 3
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-2024 grommunio GmbH
6
 *
7
 * This file handles streaming of WBXML SyncObjects. It must be subclassed so
8
 * the internals of the object can be specified via $mapping. Basically we
9
 * set/read the object variables of the subclass according to the mappings
10
 */
11
12
class Streamer implements JsonSerializable {
13
	public const STREAMER_VAR = 1;
14
	public const STREAMER_ARRAY = 2;
15
	public const STREAMER_TYPE = 3;
16
	public const STREAMER_PROP = 4;
17
	public const STREAMER_RONOTIFY = 5;
18
	public const STREAMER_VALUEMAP = 20;
19
	public const STREAMER_TYPE_DATE = 1;
20
	public const STREAMER_TYPE_HEX = 2;
21
	public const STREAMER_TYPE_DATE_DASHES = 3;
22
	public const STREAMER_TYPE_STREAM = 4; // deprecated
23
	public const STREAMER_TYPE_IGNORE = 5;
24
	public const STREAMER_TYPE_SEND_EMPTY = 6;
25
	public const STREAMER_TYPE_NO_CONTAINER = 7;
26
	public const STREAMER_TYPE_COMMA_SEPARATED = 8;
27
	public const STREAMER_TYPE_SEMICOLON_SEPARATED = 9;
28
	public const STREAMER_TYPE_MULTIPART = 10;
29
	public const STREAMER_TYPE_STREAM_ASBASE64 = 11;
30
	public const STREAMER_TYPE_STREAM_ASPLAIN = 12;
31
	public const STREAMER_PRIVATE = 13;
32
	public const STRIP_PRIVATE_DATA = 1;
33
	public const STRIP_PRIVATE_SUBSTITUTE = 'Private';
34
35
	protected $mapping;
36
	public $flags;
37
	public $content;
38
39
	/**
40
	 * Constructor.
41
	 *
42
	 * @param array $mapping internal mapping of variables
43
	 */
44
	public function __construct($mapping) {
45
		$this->mapping = $mapping;
46
		$this->flags = false;
47
	}
48
49
	/**
50
	 * Return the streamer mapping for this object.
51
	 */
52
	public function GetMapping() {
53
		return $this->mapping;
54
	}
55
56
	/**
57
	 * Decodes the WBXML from a WBXMLdecoder until we reach the same depth level of WBXML.
58
	 * This means that if there are multiple objects at this level, then only the first is
59
	 * decoded SubOjects are auto-instantiated and decoded using the same functionality.
60
	 *
61
	 * @param WBXMLDecoder $decoder
62
	 */
63
	public function Decode(&$decoder) {
64
		WBXMLDecoder::ResetInWhile("decodeMain");
65
		while (WBXMLDecoder::InWhile("decodeMain")) {
66
			$entity = $decoder->getElement();
67
68
			if ($entity[EN_TYPE] == EN_TYPE_STARTTAG) {
69
				if (!($entity[EN_FLAGS] & EN_FLAGS_CONTENT)) {
70
					$map = $this->mapping[$entity[EN_TAG]];
71
					if (isset($map[self::STREAMER_ARRAY])) {
72
						$this->{$map[self::STREAMER_VAR]} = [];
73
					}
74
					elseif (isset($map[self::STREAMER_PROP]) && $map[self::STREAMER_PROP] == self::STREAMER_TYPE_SEND_EMPTY) {
75
						$this->{$map[self::STREAMER_VAR]} = "1";
76
					}
77
					elseif (!isset($map[self::STREAMER_TYPE])) {
78
						$this->{$map[self::STREAMER_VAR]} = "";
79
					}
80
					elseif ($map[self::STREAMER_TYPE] == self::STREAMER_TYPE_DATE || $map[self::STREAMER_TYPE] == self::STREAMER_TYPE_DATE_DASHES) {
81
						$this->{$map[self::STREAMER_VAR]} = "";
82
					}
83
84
					continue;
85
				}
86
				// Found a start tag
87
				if (!isset($this->mapping[$entity[EN_TAG]])) {
88
					// This tag shouldn't be here, abort
89
					SLog::Write(LOGLEVEL_WBXMLSTACK, sprintf("Tag '%s' unexpected in type XML type '%s'", $entity[EN_TAG], get_class($this)));
90
91
					return false;
92
				}
93
94
				$map = $this->mapping[$entity[EN_TAG]];
95
96
				// Handle an array
97
				if (isset($map[self::STREAMER_ARRAY])) {
98
					WBXMLDecoder::ResetInWhile("decodeArray");
99
					while (WBXMLDecoder::InWhile("decodeArray")) {
100
						$streamertype = $map[self::STREAMER_TYPE] ?? false;
101
						// do not get start tag for an array without a container
102
						if (!(isset($map[self::STREAMER_PROP]) && $map[self::STREAMER_PROP] == self::STREAMER_TYPE_NO_CONTAINER)) {
103
							// are there multiple possibilities for element encapsulation tags?
104
							if (is_array($map[self::STREAMER_ARRAY])) {
105
								$encapTagsTypes = $map[self::STREAMER_ARRAY];
106
							}
107
							else {
108
								// set $streamertype to null if the element is a single string (e.g. category)
109
								$encapTagsTypes = [$map[self::STREAMER_ARRAY] => isset($map[self::STREAMER_TYPE]) ? $map[self::STREAMER_TYPE] : null];
110
							}
111
112
							// Identify the used tag
113
							$streamertype = false;
114
							foreach ($encapTagsTypes as $tag => $type) {
115
								if ($decoder->getElementStartTag($tag)) {
116
									$streamertype = $type;
117
								}
118
							}
119
							if ($streamertype === false) {
120
								break;
121
							}
122
						}
123
						if ($streamertype) {
124
							$decoded = new $streamertype();
125
							$decoded->Decode($decoder);
126
						}
127
						else {
128
							$decoded = $decoder->getElementContent();
129
						}
130
131
						if (!isset($this->{$map[self::STREAMER_VAR]})) {
132
							$this->{$map[self::STREAMER_VAR]} = [$decoded];
133
						}
134
						else {
135
							array_push($this->{$map[self::STREAMER_VAR]}, $decoded);
136
						}
137
138
						if (!$decoder->getElementEndTag()) { // end tag of a container element
139
							return false;
140
						}
141
142
						if (isset($map[self::STREAMER_PROP]) && $map[self::STREAMER_PROP] == self::STREAMER_TYPE_NO_CONTAINER) {
143
							$e = $decoder->peek();
144
							// go back to the initial while if another block of no container elements is found
145
							if ($e[EN_TYPE] == EN_TYPE_STARTTAG) {
146
								continue 2;
147
							}
148
							// break on end tag because no container elements block end is reached
149
							if ($e[EN_TYPE] == EN_TYPE_ENDTAG) {
150
								break;
151
							}
152
							if (empty($e)) {
153
								break;
154
							}
155
						}
156
					}
157
					// do not get end tag for an array without a container
158
					if (!(isset($map[self::STREAMER_PROP]) && $map[self::STREAMER_PROP] == self::STREAMER_TYPE_NO_CONTAINER)) {
159
						if (!$decoder->getElementEndTag()) { // end tag of container
160
							return false;
161
						}
162
					}
163
				}
164
				else { // Handle single value
165
					if (isset($map[self::STREAMER_TYPE])) {
166
						// Complex type, decode recursively
167
						if ($map[self::STREAMER_TYPE] == self::STREAMER_TYPE_DATE || $map[self::STREAMER_TYPE] == self::STREAMER_TYPE_DATE_DASHES) {
168
							$decoded = Utils::ParseDate($decoder->getElementContent());
169
							if (!$decoder->getElementEndTag()) {
170
								return false;
171
							}
172
						}
173
						elseif ($map[self::STREAMER_TYPE] == self::STREAMER_TYPE_HEX) {
174
							$decoded = hex2bin($decoder->getElementContent());
0 ignored issues
show
Bug introduced by
It seems like $decoder->getElementContent() can also be of type boolean; however, parameter $string of hex2bin() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

174
							$decoded = hex2bin(/** @scrutinizer ignore-type */ $decoder->getElementContent());
Loading history...
175
							if (!$decoder->getElementEndTag()) {
176
								return false;
177
							}
178
						}
179
						// explode comma or semicolon strings into arrays
180
						elseif ($map[self::STREAMER_TYPE] == self::STREAMER_TYPE_COMMA_SEPARATED || $map[self::STREAMER_TYPE] == self::STREAMER_TYPE_SEMICOLON_SEPARATED) {
181
							$glue = ($map[self::STREAMER_TYPE] == self::STREAMER_TYPE_COMMA_SEPARATED) ? ", " : "; ";
182
							$decoded = explode($glue, $decoder->getElementContent());
0 ignored issues
show
Bug introduced by
It seems like $decoder->getElementContent() can also be of type boolean; however, parameter $string of explode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

182
							$decoded = explode($glue, /** @scrutinizer ignore-type */ $decoder->getElementContent());
Loading history...
183
							if (!$decoder->getElementEndTag()) {
184
								return false;
185
							}
186
						}
187
						elseif ($map[self::STREAMER_TYPE] == self::STREAMER_TYPE_STREAM_ASPLAIN) {
188
							$decoded = StringStreamWrapper::Open($decoder->getElementContent());
189
							if (!$decoder->getElementEndTag()) {
190
								return false;
191
							}
192
						}
193
						else {
194
							$subdecoder = new $map[self::STREAMER_TYPE]();
195
							if ($subdecoder->Decode($decoder) === false) {
196
								return false;
197
							}
198
199
							$decoded = $subdecoder;
200
201
							if (!$decoder->getElementEndTag()) {
202
								SLog::Write(LOGLEVEL_WBXMLSTACK, sprintf("No end tag for '%s'", $entity[EN_TAG]));
203
204
								return false;
205
							}
206
						}
207
					}
208
					else {
209
						// Simple type, just get content
210
						$decoded = $decoder->getElementContent();
211
212
						if ($decoded === false) {
213
							// the tag is declared to have content, but no content is available.
214
							// set an empty content
215
							$decoded = "";
216
						}
217
218
						if (!$decoder->getElementEndTag()) {
219
							SLog::Write(LOGLEVEL_WBXMLSTACK, sprintf("Unable to get end tag for '%s'", $entity[EN_TAG]));
220
221
							return false;
222
						}
223
					}
224
					// $decoded now contains data object (or string)
225
					$this->{$map[self::STREAMER_VAR]} = $decoded;
226
				}
227
			}
228
			elseif ($entity[EN_TYPE] == EN_TYPE_ENDTAG) {
229
				$decoder->ungetElement($entity);
230
231
				break;
232
			}
233
			else {
234
				SLog::Write(LOGLEVEL_WBXMLSTACK, "Unexpected content in type");
235
236
				break;
237
			}
238
		}
239
	}
240
241
	/**
242
	 * Encodes this object and any subobjects - output is ordered according to mapping.
243
	 *
244
	 * @param WBXMLEncoder $encoder
245
	 */
246
	public function Encode(&$encoder) {
247
		// A return value if anything was streamed. We need for empty tags.
248
		$streamed = false;
249
		foreach ($this->mapping as $tag => $map) {
250
			if (isset($this->{$map[self::STREAMER_VAR]})) {
251
				// Variable is available
252
				if (is_object($this->{$map[self::STREAMER_VAR]})) {
253
					// Subobjects can do their own encoding
254
					if ($this->{$map[self::STREAMER_VAR]} instanceof Streamer) {
255
						$encoder->startTag($tag);
256
						$res = $this->{$map[self::STREAMER_VAR]}->Encode($encoder);
257
						$encoder->endTag();
258
						// nothing was streamed in previous encode but it should be streamed empty anyway
259
						if (!$res && isset($map[self::STREAMER_PROP]) && $map[self::STREAMER_PROP] == self::STREAMER_TYPE_SEND_EMPTY) {
260
							$encoder->startTag($tag, false, true);
261
						}
262
					}
263
					else {
264
						SLog::Write(LOGLEVEL_ERROR, sprintf("Streamer->Encode(): parameter '%s' of object %s is not of type Streamer", $map[self::STREAMER_VAR], get_class($this)));
265
					}
266
				}
267
				// Array of objects
268
				elseif (isset($map[self::STREAMER_ARRAY])) {
269
					if (empty($this->{$map[self::STREAMER_VAR]}) && isset($map[self::STREAMER_PROP]) && $map[self::STREAMER_PROP] == self::STREAMER_TYPE_SEND_EMPTY) {
270
						$encoder->startTag($tag, false, true);
271
					}
272
					else {
273
						// Outputs array container (eg Attachments)
274
						// Do not output start and end tag when type is STREAMER_TYPE_NO_CONTAINER
275
						if (!isset($map[self::STREAMER_PROP]) || $map[self::STREAMER_PROP] != self::STREAMER_TYPE_NO_CONTAINER) {
276
							$encoder->startTag($tag);
277
						}
278
279
						foreach ($this->{$map[self::STREAMER_VAR]} as $element) {
280
							if (is_object($element)) {
281
								// find corresponding encapsulation tag for element
282
								if (!is_array($map[self::STREAMER_ARRAY])) {
283
									$eltag = $map[self::STREAMER_ARRAY];
284
								}
285
								else {
286
									$eltag = array_search(get_class($element), $map[self::STREAMER_ARRAY]);
287
								}
288
								$encoder->startTag($eltag); // Outputs object container (eg Attachment)
289
								$element->Encode($encoder);
290
								$encoder->endTag();
291
							}
292
							else {
293
								// Do not output empty items. Not sure if we should output an empty tag with $encoder->startTag($map[self::STREAMER_ARRAY], false, true);
294
								if (strlen($element) > 0) {
295
									$encoder->startTag($map[self::STREAMER_ARRAY]);
296
									$encoder->content($element);
297
									$encoder->endTag();
298
									$streamed = true;
299
								}
300
							}
301
						}
302
303
						if (!isset($map[self::STREAMER_PROP]) || $map[self::STREAMER_PROP] != self::STREAMER_TYPE_NO_CONTAINER) {
304
							$encoder->endTag();
305
						}
306
					}
307
				}
308
				else {
309
					if (isset($map[self::STREAMER_TYPE]) && $map[self::STREAMER_TYPE] == self::STREAMER_TYPE_IGNORE) {
310
						continue;
311
					}
312
313
					if ($encoder->getMultipart() && isset($map[self::STREAMER_PROP]) && $map[self::STREAMER_PROP] == self::STREAMER_TYPE_MULTIPART) {
314
						$encoder->addBodypartStream($this->{$map[self::STREAMER_VAR]});
315
						$encoder->startTag(SYNC_ITEMOPERATIONS_PART);
316
						$encoder->content($encoder->getBodypartsCount());
317
						$encoder->endTag();
318
319
						continue;
320
					}
321
322
					// Simple type
323
					if (!isset($map[self::STREAMER_TYPE]) && strlen($this->{$map[self::STREAMER_VAR]}) == 0) {
324
						// send empty tags
325
						if (isset($map[self::STREAMER_PROP]) && $map[self::STREAMER_PROP] == self::STREAMER_TYPE_SEND_EMPTY) {
326
							$encoder->startTag($tag, false, true);
327
						}
328
329
						// Do not output empty items. See above: $encoder->startTag($tag, false, true);
330
						continue;
331
					}
332
					$encoder->startTag($tag);
333
334
					if (isset($map[self::STREAMER_TYPE]) && ($map[self::STREAMER_TYPE] == self::STREAMER_TYPE_DATE || $map[self::STREAMER_TYPE] == self::STREAMER_TYPE_DATE_DASHES)) {
335
						if ($this->{$map[self::STREAMER_VAR]} != 0) { // don't output 1-1-1970
336
							$encoder->content(Utils::FormatDate($this->{$map[self::STREAMER_VAR]}, $map[self::STREAMER_TYPE]));
337
						}
338
					}
339
					elseif (isset($map[self::STREAMER_TYPE]) && $map[self::STREAMER_TYPE] == self::STREAMER_TYPE_HEX) {
340
						$encoder->content(strtoupper(bin2hex($this->{$map[self::STREAMER_VAR]})));
341
					}
342
					elseif (isset($map[self::STREAMER_TYPE]) && $map[self::STREAMER_TYPE] == self::STREAMER_TYPE_STREAM_ASPLAIN) {
343
						$encoder->contentStream($this->{$map[self::STREAMER_VAR]}, false);
344
					}
345
					elseif (isset($map[self::STREAMER_TYPE]) && ($map[self::STREAMER_TYPE] == self::STREAMER_TYPE_STREAM_ASBASE64 || $map[self::STREAMER_TYPE] == self::STREAMER_TYPE_STREAM)) {
346
						$encoder->contentStream($this->{$map[self::STREAMER_VAR]}, true);
347
					}
348
					// implode comma or semicolon arrays into a string
349
					elseif (isset($map[self::STREAMER_TYPE]) && is_array($this->{$map[self::STREAMER_VAR]}) &&
350
						($map[self::STREAMER_TYPE] == self::STREAMER_TYPE_COMMA_SEPARATED || $map[self::STREAMER_TYPE] == self::STREAMER_TYPE_SEMICOLON_SEPARATED)) {
351
						$glue = ($map[self::STREAMER_TYPE] == self::STREAMER_TYPE_COMMA_SEPARATED) ? ", " : "; ";
352
						$encoder->content(implode($glue, $this->{$map[self::STREAMER_VAR]}));
353
					}
354
					else {
355
						$encoder->content($this->{$map[self::STREAMER_VAR]});
356
					}
357
					$encoder->endTag();
358
					$streamed = true;
359
				}
360
			}
361
		}
362
		// Output our own content
363
		if (isset($this->content)) {
364
			$encoder->content($this->content);
365
		}
366
367
		return $streamed;
368
	}
369
370
	/**
371
	 * Removes not necessary data from the object.
372
	 *
373
	 * @param mixed $flags
374
	 *
375
	 * @return bool
376
	 */
377
	public function StripData($flags = 0) {
378
		foreach ($this->mapping as $k => $v) {
379
			if (isset($this->{$v[self::STREAMER_VAR]})) {
380
				if (is_object($this->{$v[self::STREAMER_VAR]}) && method_exists($this->{$v[self::STREAMER_VAR]}, "StripData")) {
381
					$this->{$v[self::STREAMER_VAR]}->StripData($flags);
382
				}
383
				elseif (isset($v[self::STREAMER_ARRAY]) && !empty($this->{$v[self::STREAMER_VAR]})) {
384
					foreach ($this->{$v[self::STREAMER_VAR]} as $element) {
385
						if (is_object($element) && method_exists($element, "StripData")) {
386
							$element->StripData($flags);
387
						}
388
						elseif ($flags === Streamer::STRIP_PRIVATE_DATA && isset($v[self::STREAMER_PRIVATE])) {
389
							if ($v[self::STREAMER_PRIVATE] !== true) {
390
								$this->{$v[self::STREAMER_VAR]} = $v[self::STREAMER_PRIVATE];
391
							}
392
							else {
393
								unset($this->{$v[self::STREAMER_VAR]});
394
							}
395
						}
396
					}
397
				}
398
				elseif ($flags === Streamer::STRIP_PRIVATE_DATA && isset($v[self::STREAMER_PRIVATE])) {
399
					if ($v[self::STREAMER_PRIVATE] !== true) {
400
						$this->{$v[self::STREAMER_VAR]} = $v[self::STREAMER_PRIVATE];
401
					}
402
					else {
403
						unset($this->{$v[self::STREAMER_VAR]});
404
					}
405
				}
406
			}
407
		}
408
		if ($flags === 0) {
409
			unset($this->mapping);
410
		}
411
412
		return true;
413
	}
414
415
	/**
416
	 * Returns SyncObject's streamer variable names.
417
	 *
418
	 * @return array
419
	 */
420
	public function GetStreamerVars() {
421
		$streamerVars = [];
422
		foreach ($this->mapping as $v) {
423
			$streamerVars[] = $v[self::STREAMER_VAR];
424
		}
425
426
		return $streamerVars;
427
	}
428
429
	/**
430
	 * JsonSerializable interface method.
431
	 *
432
	 * Serializes the object to a value that can be serialized natively by json_encode()
433
	 *
434
	 */
435
	public function jsonSerialize(): mixed {
436
		$data = [];
437
		foreach ($this->mapping as $k => $v) {
438
			if (isset($this->{$v[self::STREAMER_VAR]})) {
439
				$data[$v[self::STREAMER_VAR]] = $this->{$v[self::STREAMER_VAR]};
440
			}
441
		}
442
443
		return [
444
			'gsSyncStateClass' => get_class($this),
445
			'data' => $data,
446
		];
447
	}
448
449
	/**
450
	 * Restores the object from a value provided by json_decode.
451
	 *
452
	 * @param $stdObj stdClass Object
453
	 */
454
	public function jsonDeserialize($stdObj) {
455
		foreach ($stdObj->data as $k => $v) {
456
			if (is_object($v) && isset($v->gsSyncStateClass)) {
457
				$this->{$k} = new $v->gsSyncStateClass();
458
				$this->{$k}->jsonDeserialize($v);
459
			}
460
			else {
461
				$this->{$k} = $v;
462
			}
463
		}
464
	}
465
}
466