Streamer   F
last analyzed

Complexity

Total Complexity 121

Size/Duplication

Total Lines 446
Duplicated Lines 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 230
c 2
b 1
f 0
dl 0
loc 446
rs 2
wmc 121

8 Methods

Rating   Name   Duplication   Size   Complexity  
A jsonDeserialize() 0 8 4
A GetMapping() 0 2 1
F Decode() 0 174 47
D Encode() 0 122 46
A GetStreamerVars() 0 7 2
A jsonSerialize() 0 11 3
C StripData() 0 36 17
A __construct() 0 2 1

How to fix   Complexity   

Complex Class

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

172
							$decoded = hex2bin(/** @scrutinizer ignore-type */ $decoder->getElementContent());
Loading history...
173
							if (!$decoder->getElementEndTag()) {
174
								return false;
175
							}
176
						}
177
						// explode comma or semicolon strings into arrays
178
						elseif ($map[self::STREAMER_TYPE] == self::STREAMER_TYPE_COMMA_SEPARATED || $map[self::STREAMER_TYPE] == self::STREAMER_TYPE_SEMICOLON_SEPARATED) {
179
							$glue = ($map[self::STREAMER_TYPE] == self::STREAMER_TYPE_COMMA_SEPARATED) ? ", " : "; ";
180
							$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

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