SyncCollections   F
last analyzed

Complexity

Total Complexity 149

Size/Duplication

Total Lines 899
Duplicated Lines 0 %

Importance

Changes 4
Bugs 2 Features 0
Metric Value
eloc 311
c 4
b 2
f 0
dl 0
loc 899
rs 2
wmc 149

34 Methods

Rating   Name   Duplication   Size   Complexity  
A GetLifetime() 0 10 4
A GetLastSyncTime() 0 2 1
A SetLifetime() 0 4 1
A SaveCollection() 0 23 6
A AddCollection() 0 25 6
A valid() 0 2 2
A __construct() 0 1 1
A InvalidatePingableFlags() 0 19 4
A HasCollections() 0 2 1
A loadStateManager() 0 3 2
A GetWaitedSeconds() 0 2 1
A GetCollection() 0 6 2
A next() 0 2 1
B CountChange() 0 48 8
A GetGlobalWindowSize() 0 18 5
B CountChanges() 0 17 7
A AddParameter() 0 13 3
A getPingClass() 0 7 3
B LoadAllCollections() 0 28 8
A key() 0 2 1
A GetCollectionCount() 0 2 1
A GetParameter() 0 10 3
C countHierarchyChange() 0 49 12
A PingableFolders() 0 8 3
A SetStateManager() 0 2 1
A current() 0 2 1
A rewind() 0 2 1
A SetGlobalWindowSize() 0 4 1
A WaitedForChanges() 0 4 1
F CheckForChanges() 0 190 44
B LoadCollection() 0 60 10
A invalidateFolderStat() 0 10 2
A GetReferencePolicyKey() 0 2 1
A GetChangedFolderIds() 0 2 1

How to fix   Complexity   

Complex Class

Complex classes like SyncCollections 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 SyncCollections, 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-2024 grommunio GmbH
6
 *
7
 * This is basically a list of synched folders with its respective
8
 * SyncParameters, while some additional parameters which are not stored
9
 * there can be kept here.
10
 *
11
 * The class also provides CheckForChanges which is basically a loop through
12
 * all collections checking for changes.
13
 *
14
 * SyncCollections is used for Sync (with and without heartbeat)
15
 * and Ping connections.
16
 *
17
 * To check for changes in Heartbeat and Ping requeste the same
18
 * sync states as for the default synchronization are used.
19
 */
20
21
class SyncCollections implements Iterator {
22
	public const ERROR_NO_COLLECTIONS = 1;
23
	public const ERROR_WRONG_HIERARCHY = 2;
24
	public const OBSOLETE_CONNECTION = 3;
25
	public const HIERARCHY_CHANGED = 4;
26
27
	private $stateManager;
28
29
	private $collections = [];
30
	private $addparms = [];
31
	private $changes = [];
32
	private $saveData = true;
33
34
	private $refPolicyKey = false;
35
	private $refLifetime = false;
36
37
	private $globalWindowSize;
38
	private $lastSyncTime;
39
40
	private $waitingTime = 0;
41
	private $hierarchyExporterChecked = false;
42
	private $loggedGlobalWindowSizeOverwrite = false;
43
44
	/**
45
	 * Invalidates all pingable flags for all folders.
46
	 *
47
	 * @return bool
48
	 */
49
	public static function InvalidatePingableFlags() {
50
		SLog::Write(LOGLEVEL_DEBUG, "SyncCollections::InvalidatePingableFlags(): Invalidating now");
51
52
		try {
53
			$sc = new SyncCollections();
54
			$sc->LoadAllCollections();
55
			foreach ($sc as $folderid => $spa) {
56
				if ($spa->GetPingableFlag() == true) {
57
					$spa->DelPingableFlag();
58
					$sc->SaveCollection($spa);
59
				}
60
			}
61
62
			return true;
63
		}
64
		catch (GSyncException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
65
		}
66
67
		return false;
68
	}
69
70
	/**
71
	 * Constructor.
72
	 */
73
	public function __construct() {}
74
75
	/**
76
	 * Sets the StateManager for this object
77
	 * If this is not done and a method needs it, the StateManager will be
78
	 * requested from the DeviceManager.
79
	 *
80
	 * @param StateManager $statemanager
81
	 */
82
	public function SetStateManager($statemanager) {
83
		$this->stateManager = $statemanager;
84
	}
85
86
	/**
87
	 * Loads all collections known for the current device.
88
	 *
89
	 * @param bool $overwriteLoaded  (opt) overwrites Collection with saved state if set to true
90
	 * @param bool $loadState        (opt) indicates if the collection sync state should be loaded, default false
91
	 * @param bool $checkPermissions (opt) if set to true each folder will pass
92
	 *                               through a backend->Setup() to check permissions.
93
	 *                               If this fails a StatusException will be thrown.
94
	 * @param bool $loadHierarchy    (opt) if the hierarchy sync states should be loaded, default false
95
	 * @param bool $confirmedOnly    (opt) indicates if only confirmed states should be loaded, default: false
96
	 *
97
	 * @return bool
98
	 *
99
	 * @throws StatusException       with SyncCollections::ERROR_WRONG_HIERARCHY if permission check fails
100
	 * @throws StateInvalidException if the sync state can not be found or relation between states is invalid ($loadState = true)
101
	 */
102
	public function LoadAllCollections($overwriteLoaded = false, $loadState = false, $checkPermissions = false, $loadHierarchy = false, $confirmedOnly = false) {
103
		$this->loadStateManager();
104
105
		// this operation should not remove old state counters
106
		$this->stateManager->DoNotDeleteOldStates();
107
108
		$invalidStates = false;
109
		foreach ($this->stateManager->GetSynchedFolders() as $folderid) {
110
			if ($overwriteLoaded === false && isset($this->collections[$folderid])) {
111
				continue;
112
			}
113
114
			// Load Collection!
115
			if (!$this->LoadCollection($folderid, $loadState, $checkPermissions, $confirmedOnly)) {
116
				$invalidStates = true;
117
			}
118
		}
119
120
		// load the hierarchy data - there are no permissions to verify so we just set it to false
121
		if ($loadHierarchy && !$this->LoadCollection(false, $loadState, false, false)) {
0 ignored issues
show
Bug introduced by
false of type false is incompatible with the type string expected by parameter $folderid of SyncCollections::LoadCollection(). ( Ignorable by Annotation )

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

121
		if ($loadHierarchy && !$this->LoadCollection(/** @scrutinizer ignore-type */ false, $loadState, false, false)) {
Loading history...
122
			throw new StatusException("Invalid states found while loading hierarchy data. Forcing hierarchy sync");
123
		}
124
125
		if ($invalidStates) {
126
			throw new StateInvalidException("Invalid states found while loading collections. Forcing sync");
127
		}
128
129
		return true;
130
	}
131
132
	/**
133
	 * Loads all collections known for the current device.
134
	 *
135
	 * @param string $folderid         folder id to be loaded
136
	 * @param bool   $loadState        (opt) indicates if the collection sync state should be loaded, default true
137
	 * @param bool   $checkPermissions (opt) if set to true each folder will pass
138
	 *                                 through a backend->Setup() to check permissions.
139
	 *                                 If this fails a StatusException will be thrown.
140
	 * @param bool   $confirmedOnly    (opt) indicates if only confirmed states should be loaded, default: false
141
	 *
142
	 * @return bool
143
	 *
144
	 * @throws StatusException       with SyncCollections::ERROR_WRONG_HIERARCHY if permission check fails
145
	 * @throws StateInvalidException if the sync state can not be found or relation between states is invalid ($loadState = true)
146
	 */
147
	public function LoadCollection($folderid, $loadState = false, $checkPermissions = false, $confirmedOnly = false) {
148
		$this->loadStateManager();
149
150
		try {
151
			// Get SyncParameters for the folder from the state
152
			$spa = $this->stateManager->GetSynchedFolderState($folderid, !$loadState);
153
154
			// TODO remove resync of folders
155
			// this forces a resync of all states
156
			if (!$spa instanceof SyncParameters) {
157
				throw new StateInvalidException("Saved state are not of type SyncParameters");
158
			}
159
160
			if ($spa->GetUuidCounter() == 0) {
0 ignored issues
show
Bug introduced by
The method GetUuidCounter() does not exist on SyncParameters. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

160
			if ($spa->/** @scrutinizer ignore-call */ GetUuidCounter() == 0) {
Loading history...
161
				SLog::Write(LOGLEVEL_DEBUG, "SyncCollections->LoadCollection(): Found collection with move state only, ignoring.");
162
163
				return true;
164
			}
165
		}
166
		catch (StateInvalidException $sive) {
167
			// in case there is something wrong with the state, just stop here
168
			// later when trying to retrieve the SyncParameters nothing will be found
169
170
			if ($folderid === false) {
0 ignored issues
show
introduced by
The condition $folderid === false is always false.
Loading history...
171
				throw new StatusException(sprintf("SyncCollections->LoadCollection(): could not get FOLDERDATA state of the hierarchy uuid: %s", $spa->GetUuid()), self::ERROR_WRONG_HIERARCHY);
172
			}
173
174
			// we also generate a fake change, so a sync on this folder is triggered
175
			$this->changes[$folderid] = 1;
176
177
			return false;
178
		}
179
180
		// if this is an additional folder the backend has to be setup correctly
181
		if ($checkPermissions === true && !GSync::GetBackend()->Setup(GSync::GetAdditionalSyncFolderStore($spa->GetBackendFolderId()))) {
182
			throw new StatusException(sprintf("SyncCollections->LoadCollection(): could not Setup() the backend for folder id %s/%s", $spa->GetFolderId(), $spa->GetBackendFolderId()), self::ERROR_WRONG_HIERARCHY);
0 ignored issues
show
Bug introduced by
The method GetFolderId() does not exist on SyncParameters. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

182
			throw new StatusException(sprintf("SyncCollections->LoadCollection(): could not Setup() the backend for folder id %s/%s", $spa->/** @scrutinizer ignore-call */ GetFolderId(), $spa->GetBackendFolderId()), self::ERROR_WRONG_HIERARCHY);
Loading history...
183
		}
184
185
		// add collection to object
186
		$addStatus = $this->AddCollection($spa);
187
188
		// load the latest known syncstate if requested
189
		if ($addStatus && $loadState === true) {
190
			try {
191
				// make sure the hierarchy cache is loaded when we are loading hierarchy states
192
				$this->addparms[$folderid]["state"] = $this->stateManager->GetSyncState($spa->GetLatestSyncKey($confirmedOnly), $folderid === false);
193
			}
194
			catch (StateNotFoundException $snfe) {
195
				// if we can't find the state, first we should try a sync of that folder, so
196
				// we generate a fake change, so a sync on this folder is triggered
197
				$this->changes[$folderid] = 1;
198
199
				// make sure this folder is fully synched on next Sync request
200
				$this->invalidateFolderStat($spa);
201
202
				return false;
203
			}
204
		}
205
206
		return $addStatus;
207
	}
208
209
	/**
210
	 * Saves a SyncParameters Object.
211
	 *
212
	 * @param SyncParamerts $spa
0 ignored issues
show
Bug introduced by
The type SyncParamerts 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...
213
	 *
214
	 * @return bool
215
	 */
216
	public function SaveCollection($spa) {
217
		if (!$this->saveData || !$spa->HasFolderId()) {
218
			return false;
219
		}
220
221
		if ($spa->IsDataChanged()) {
222
			$this->loadStateManager();
223
			SLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->SaveCollection(): Data of folder '%s' changed", $spa->GetFolderId()));
224
225
			// save new windowsize
226
			if (isset($this->globalWindowSize)) {
227
				$spa->SetWindowSize($this->globalWindowSize);
228
			}
229
230
			// update latest lifetime
231
			if (isset($this->refLifetime)) {
232
				$spa->SetReferenceLifetime($this->refLifetime);
233
			}
234
235
			return $this->stateManager->SetSynchedFolderState($spa);
236
		}
237
238
		return false;
239
	}
240
241
	/**
242
	 * Adds a SyncParameters object to the current list of collections.
243
	 *
244
	 * @param SyncParameters $spa
245
	 *
246
	 * @return bool
247
	 */
248
	public function AddCollection($spa) {
249
		if (!$spa->HasFolderId()) {
0 ignored issues
show
Bug introduced by
The method HasFolderId() does not exist on SyncParameters. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

249
		if (!$spa->/** @scrutinizer ignore-call */ HasFolderId()) {
Loading history...
250
			return false;
251
		}
252
253
		$this->collections[$spa->GetFolderId()] = $spa;
254
255
		SLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->AddCollection(): Folder id '%s' : ref. Lifetime '%s', last sync at '%s'", $spa->GetFolderId(), $spa->GetReferenceLifetime(), $spa->GetLastSyncTime()));
0 ignored issues
show
Bug introduced by
The method GetLastSyncTime() does not exist on SyncParameters. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

255
		SLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->AddCollection(): Folder id '%s' : ref. Lifetime '%s', last sync at '%s'", $spa->GetFolderId(), $spa->GetReferenceLifetime(), $spa->/** @scrutinizer ignore-call */ GetLastSyncTime()));
Loading history...
Bug introduced by
The method GetReferenceLifetime() does not exist on SyncParameters. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

255
		SLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->AddCollection(): Folder id '%s' : ref. Lifetime '%s', last sync at '%s'", $spa->GetFolderId(), $spa->/** @scrutinizer ignore-call */ GetReferenceLifetime(), $spa->GetLastSyncTime()));
Loading history...
256
		if ($spa->HasLastSyncTime() && $spa->GetLastSyncTime() > $this->lastSyncTime) {
0 ignored issues
show
Bug introduced by
The method HasLastSyncTime() does not exist on SyncParameters. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

256
		if ($spa->/** @scrutinizer ignore-call */ HasLastSyncTime() && $spa->GetLastSyncTime() > $this->lastSyncTime) {
Loading history...
257
			$this->lastSyncTime = $spa->GetLastSyncTime();
258
259
			// use SyncParameters PolicyKey as reference if available
260
			if ($spa->HasReferencePolicyKey()) {
0 ignored issues
show
Bug introduced by
The method HasReferencePolicyKey() does not exist on SyncParameters. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

260
			if ($spa->/** @scrutinizer ignore-call */ HasReferencePolicyKey()) {
Loading history...
261
				$this->refPolicyKey = $spa->GetReferencePolicyKey();
0 ignored issues
show
Bug introduced by
The method GetReferencePolicyKey() does not exist on SyncParameters. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

261
				/** @scrutinizer ignore-call */ 
262
    $this->refPolicyKey = $spa->GetReferencePolicyKey();
Loading history...
262
			}
263
264
			// use SyncParameters LifeTime as reference if available
265
			if ($spa->HasReferenceLifetime()) {
0 ignored issues
show
Bug introduced by
The method HasReferenceLifetime() does not exist on SyncParameters. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

265
			if ($spa->/** @scrutinizer ignore-call */ HasReferenceLifetime()) {
Loading history...
266
				$this->refLifetime = $spa->GetReferenceLifetime();
267
			}
268
269
			SLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->AddCollection(): Updated reference PolicyKey '%s', reference Lifetime '%s', Last sync at '%s'", $this->refPolicyKey, $this->refLifetime, $this->lastSyncTime));
270
		}
271
272
		return true;
273
	}
274
275
	/**
276
	 * Returns a previousily added or loaded SyncParameters object for a folderid.
277
	 *
278
	 * @param mixed $folderid
279
	 *
280
	 * @return bool|SyncParameters false if no SyncParameters object is found for folderid
281
	 */
282
	public function GetCollection($folderid) {
283
		if (isset($this->collections[$folderid])) {
284
			return $this->collections[$folderid];
285
		}
286
287
		return false;
288
	}
289
290
	/**
291
	 * Indicates if there are any loaded CPOs.
292
	 *
293
	 * @return bool
294
	 */
295
	public function HasCollections() {
296
		return !empty($this->collections);
297
	}
298
299
	/**
300
	 * Indicates the amount of collections loaded.
301
	 *
302
	 * @return int
303
	 */
304
	public function GetCollectionCount() {
305
		return count($this->collections);
306
	}
307
308
	/**
309
	 * Add a non-permanent key/value pair for a SyncParameters object.
310
	 *
311
	 * @param SyncParameters $spa   target SyncParameters
312
	 * @param string         $key
313
	 * @param mixed          $value
314
	 *
315
	 * @return bool
316
	 */
317
	public function AddParameter($spa, $key, $value) {
318
		if (!$spa->HasFolderId()) {
319
			return false;
320
		}
321
322
		$folderid = $spa->GetFolderId();
323
		if (!isset($this->addparms[$folderid])) {
324
			$this->addparms[$folderid] = [];
325
		}
326
327
		$this->addparms[$folderid][$key] = $value;
328
329
		return true;
330
	}
331
332
	/**
333
	 * Returns a previousily set non-permanent value for a SyncParameters object.
334
	 *
335
	 * @param SyncParameters $spa target SyncParameters
336
	 * @param string         $key
337
	 *
338
	 * @return mixed returns 'null' if nothing set
339
	 */
340
	public function GetParameter($spa, $key) {
341
		if (!$spa->HasFolderId()) {
342
			return null;
343
		}
344
345
		if (isset($this->addparms[$spa->GetFolderId()], $this->addparms[$spa->GetFolderId()][$key])) {
346
			return $this->addparms[$spa->GetFolderId()][$key];
347
		}
348
349
		return null;
350
	}
351
352
	/**
353
	 * Returns the latest known PolicyKey to be used as reference.
354
	 *
355
	 * @return bool|int returns false if nothing found in collections
356
	 */
357
	public function GetReferencePolicyKey() {
358
		return $this->refPolicyKey;
359
	}
360
361
	/**
362
	 * Sets a global window size which should be used for all collections
363
	 * in a case of a heartbeat and/or partial sync.
364
	 *
365
	 * @param int $windowsize
366
	 *
367
	 * @return bool
368
	 */
369
	public function SetGlobalWindowSize($windowsize) {
370
		$this->globalWindowSize = $windowsize;
371
372
		return true;
373
	}
374
375
	/**
376
	 * Returns the global window size of items to be exported in total over all
377
	 * requested collections.
378
	 *
379
	 * @return bool|int returns requested windows size, 512 (max) or the
380
	 *                  value of config SYNC_MAX_ITEMS if it is lower
381
	 */
382
	public function GetGlobalWindowSize() {
383
		// take the requested global windowsize or the max 512 if not defined
384
		if (isset($this->globalWindowSize)) {
385
			$globalWindowSize = $this->globalWindowSize;
386
		}
387
		else {
388
			$globalWindowSize = WINDOW_SIZE_MAX; // 512 by default
389
		}
390
391
		if (defined("SYNC_MAX_ITEMS") && $globalWindowSize > SYNC_MAX_ITEMS) {
392
			if (!$this->loggedGlobalWindowSizeOverwrite) {
393
				SLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->GetGlobalWindowSize() overwriting requested global window size of %d by %d forced in configuration.", $globalWindowSize, SYNC_MAX_ITEMS));
394
				$this->loggedGlobalWindowSizeOverwrite = true;
395
			}
396
			$globalWindowSize = SYNC_MAX_ITEMS;
397
		}
398
399
		return $globalWindowSize;
400
	}
401
402
	/**
403
	 * Sets the lifetime for heartbeat or ping connections.
404
	 *
405
	 * @param int $lifetime time in seconds
406
	 *
407
	 * @return bool
408
	 */
409
	public function SetLifetime($lifetime) {
410
		$this->refLifetime = $lifetime;
411
412
		return true;
413
	}
414
415
	/**
416
	 * Sets the lifetime for heartbeat or ping connections
417
	 * previousily set or saved in a collection.
418
	 *
419
	 * @return int returns PING_HIGHER_BOUND_LIFETIME as default if nothing set or not available.
420
	 *             If PING_HIGHER_BOUND_LIFETIME is not set, returns 600.
421
	 */
422
	public function GetLifetime() {
423
		if (!isset($this->refLifetime) || $this->refLifetime === false) {
424
			if (PING_HIGHER_BOUND_LIFETIME !== false) {
0 ignored issues
show
introduced by
The condition PING_HIGHER_BOUND_LIFETIME !== false is always false.
Loading history...
425
				return PING_HIGHER_BOUND_LIFETIME;
426
			}
427
428
			return 600;
429
		}
430
431
		return $this->refLifetime;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->refLifetime returns the type true which is incompatible with the documented return type integer.
Loading history...
432
	}
433
434
	/**
435
	 * Returns the timestamp of the last synchronization for all
436
	 * loaded collections.
437
	 *
438
	 * @return int timestamp
439
	 */
440
	public function GetLastSyncTime() {
441
		return $this->lastSyncTime;
442
	}
443
444
	/**
445
	 * Checks if the currently known collections for changes for $lifetime seconds.
446
	 * If the backend provides a ChangesSink the sink will be used.
447
	 * If not every $interval seconds an exporter will be configured for each
448
	 * folder to perform GetChangeCount().
449
	 *
450
	 * @param int  $lifetime     (opt) total lifetime to wait for changes / default 600s
451
	 * @param int  $interval     (opt) time between blocking operations of sink or polling / default 30s
452
	 * @param bool $onlyPingable (opt) only check for folders which have the PingableFlag
453
	 *
454
	 * @return bool indicating if changes were found
455
	 *
456
	 * @throws StatusException with code SyncCollections::ERROR_NO_COLLECTIONS if no collections available
457
	 *                         with code SyncCollections::ERROR_WRONG_HIERARCHY if there were errors getting changes
458
	 */
459
	public function CheckForChanges($lifetime = 600, $interval = 30, $onlyPingable = false) {
460
		$classes = [];
461
		foreach ($this->collections as $folderid => $spa) {
462
			if ($onlyPingable && $spa->GetPingableFlag() !== true || !$folderid) {
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($onlyPingable && $spa->...== true) || ! $folderid, Probably Intended Meaning: $onlyPingable && ($spa->...== true || ! $folderid)
Loading history...
463
				continue;
464
			}
465
466
			$class = $this->getPingClass($spa);
467
468
			if (!isset($classes[$class])) {
469
				$classes[$class] = 0;
470
			}
471
			++$classes[$class];
472
		}
473
		if (empty($classes)) {
474
			$checkClasses = "policies only";
475
		}
476
		elseif (array_sum($classes) > 4) {
477
			$checkClasses = "";
478
			foreach ($classes as $class => $count) {
479
				if ($count == 1) {
480
					$checkClasses .= sprintf("%s ", $class);
481
				}
482
				else {
483
					$checkClasses .= sprintf("%s(%d) ", $class, $count);
484
				}
485
			}
486
		}
487
		else {
488
			$checkClasses = implode(" ", array_keys($classes));
489
		}
490
491
		$pingTracking = new PingTracking();
492
		$this->changes = [];
493
494
		GSync::GetDeviceManager()->AnnounceProcessAsPush();
495
		GSync::GetTopCollector()->AnnounceInformation(sprintf("lifetime %ds", $lifetime), true);
496
		SLog::Write(LOGLEVEL_INFO, sprintf("SyncCollections->CheckForChanges(): Waiting for %s changes... (lifetime %d seconds)", (empty($classes)) ? 'policy' : 'store', $lifetime));
497
498
		// use changes sink where available
499
		$changesSink = GSync::GetBackend()->HasChangesSink();
500
501
		// create changessink and check folder stats if there are folders to Ping
502
		if (!empty($classes)) {
503
			// initialize all possible folders
504
			foreach ($this->collections as $folderid => $spa) {
505
				if (($onlyPingable && $spa->GetPingableFlag() !== true) || !$folderid) {
506
					continue;
507
				}
508
509
				$backendFolderId = $spa->GetBackendFolderId();
510
511
				// get the user store if this is a additional folder
512
				$store = GSync::GetAdditionalSyncFolderStore($backendFolderId);
513
514
				// initialize sink if no immediate changes were found so far
515
				if ($changesSink && empty($this->changes)) {
516
					GSync::GetBackend()->Setup($store);
517
					if (!GSync::GetBackend()->ChangesSinkInitialize($backendFolderId)) {
518
						throw new StatusException(sprintf("Error initializing ChangesSink for folder id %s/%s", $folderid, $backendFolderId), self::ERROR_WRONG_HIERARCHY);
519
					}
520
				}
521
522
				// check if the folder stat changed since the last sync, if so generate a change for it (only on first run)
523
				$currentFolderStat = GSync::GetBackend()->GetFolderStat($store, $backendFolderId);
524
				if ($this->waitingTime == 0 &&
525
					GSync::GetBackend()->HasFolderStats() &&
526
					$currentFolderStat !== false &&
527
					$spa->IsExporterRunRequired($currentFolderStat, true) &&
528
					!$this->CountChange($spa->GetFolderId()) &&
529
					array_key_exists($spa->GetFolderId(), $this->changes)
530
				) {
531
					SLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->CheckForChanges(): Initial check indicates changes on folder '%s', but they are not relevant", $spa->GetFolderId()));
532
					unset($this->changes[$spa->GetFolderId()]);
533
				}
534
			}
535
		}
536
537
		if (!empty($this->changes)) {
538
			SLog::Write(LOGLEVEL_DEBUG, "SyncCollections->CheckForChanges(): Using ChangesSink but found changes verifying the folder stats");
539
540
			return true;
541
		}
542
543
		// wait for changes
544
		$started = time();
545
		$endat = time() + $lifetime;
546
547
		// always use policy key from the request if it was sent
548
		$policyKey = $this->GetReferencePolicyKey();
549
		if (Request::WasPolicyKeySent() && Request::GetPolicyKey() != 0) {
550
			SLog::Write(LOGLEVEL_DEBUG, sprintf("refpolkey:'%s', sent polkey:'%s'", $policyKey, Request::GetPolicyKey()));
551
			$policyKey = Request::GetPolicyKey();
552
		}
553
		while (($now = time()) < $endat) {
554
			// how long are we waiting for changes
555
			$this->waitingTime = $now - $started;
556
557
			$nextInterval = $interval;
558
			// we should not block longer than the lifetime
559
			if ($endat - $now < $nextInterval) {
560
				$nextInterval = $endat - $now;
561
			}
562
563
			// Check if provisioning is necessary
564
			// if a PolicyKey was sent use it. If not, compare with the ReferencePolicyKey
565
			if (PROVISIONING === true && $policyKey !== false && GSync::GetProvisioningManager()->ProvisioningRequired($policyKey, true, false)) {
566
				// the hierarchysync forces provisioning
567
				throw new StatusException("SyncCollections->CheckForChanges(): Policies or PolicyKey changed. Provisioning required.", self::ERROR_WRONG_HIERARCHY);
568
			}
569
570
			// Check if a hierarchy sync is necessary
571
			if ($this->countHierarchyChange()) {
572
				throw new StatusException("SyncCollections->CheckForChanges(): HierarchySync required.", self::HIERARCHY_CHANGED);
573
			}
574
575
			// Force interruption of the request if we use more than 50 MB of memory
576
			if (memory_get_peak_usage(true) > 52428800) {
577
				GSync::GetTopCollector()->AnnounceInformation(sprintf("Forced timeout after %ds (high memory usage)", $now - $started), true);
578
579
				throw new StatusException(sprintf("SyncCollections->CheckForChanges(): Timeout forced after %ss from %ss as process used too much memory", $now - $started, $lifetime), self::OBSOLETE_CONNECTION);
580
			}
581
582
			// Check if there are newer requests
583
			// If so, this process should be terminated if more than 60 secs to go
584
			if ($pingTracking->DoForcePingTimeout()) {
585
				// do not update CPOs because another process has already read them!
586
				$this->saveData = false;
587
588
				// more than 60 secs to go?
589
				if (($now + 60) < $endat) {
590
					GSync::GetTopCollector()->AnnounceInformation(sprintf("Forced timeout after %ds", $now - $started), true);
591
592
					throw new StatusException(sprintf("SyncCollections->CheckForChanges(): Timeout forced after %ss from %ss due to other process", $now - $started, $lifetime), self::OBSOLETE_CONNECTION);
593
				}
594
			}
595
596
			// Use changes sink if available
597
			if ($changesSink) {
598
				GSync::GetTopCollector()->AnnounceInformation(sprintf("Sink %d/%ds on %s", $now - $started, $lifetime, $checkClasses));
599
				$notifications = GSync::GetBackend()->ChangesSink($nextInterval);
600
601
				// how long are we waiting for changes
602
				$this->waitingTime = time() - $started;
603
604
				$validNotifications = false;
605
				foreach ($notifications as $backendFolderId) {
606
					// Check hierarchy notifications
607
					if ($backendFolderId === IBackend::HIERARCHYNOTIFICATION) {
608
						// wait two seconds before validating this notification, because it could potentially be made by the mobile and we need some time to update the states.
609
						sleep(2);
610
						// check received hierarchy notifications by exporting
611
						if ($this->countHierarchyChange(true)) {
612
							throw new StatusException("SyncCollections->CheckForChanges(): HierarchySync required.", self::HIERARCHY_CHANGED);
613
						}
614
					}
615
					else {
616
						// the backend will notify on the backend folderid
617
						$folderid = GSync::GetDeviceManager()->GetFolderIdForBackendId($backendFolderId);
618
619
						// check if the notification on the folder is within our filter
620
						if ($this->CountChange($folderid)) {
621
							SLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->CheckForChanges(): Notification received on folder '%s'", $folderid));
622
							$validNotifications = true;
623
							$this->waitingTime = time() - $started;
624
						}
625
						else {
626
							SLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->CheckForChanges(): Notification received on folder '%s', but it is not relevant", $folderid));
627
						}
628
					}
629
				}
630
				if ($validNotifications) {
631
					return true;
632
				}
633
			}
634
			// use polling mechanism
635
			else {
636
				GSync::GetTopCollector()->AnnounceInformation(sprintf("Polling %d/%ds on %s", $now - $started, $lifetime, $checkClasses));
637
				if ($this->CountChanges($onlyPingable)) {
638
					SLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->CheckForChanges(): Found changes polling"));
639
640
					return true;
641
				}
642
643
				sleep($nextInterval);
644
			} // end polling
645
		} // end wait for changes
646
		SLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->CheckForChanges(): no changes found after %ds", time() - $started));
647
648
		return false;
649
	}
650
651
	/**
652
	 * Checks if the currently known collections for
653
	 * changes performing Exporter->GetChangeCount().
654
	 *
655
	 * @param bool $onlyPingable (opt) only check for folders which have the PingableFlag
656
	 *
657
	 * @return bool indicating if changes were found or not
658
	 */
659
	public function CountChanges($onlyPingable = false) {
660
		$changesAvailable = false;
661
		foreach ($this->collections as $folderid => $spa) {
662
			if ($onlyPingable && $spa->GetPingableFlag() !== true) {
663
				continue;
664
			}
665
666
			if (isset($this->addparms[$spa->GetFolderId()]["status"]) && $this->addparms[$spa->GetFolderId()]["status"] != SYNC_STATUS_SUCCESS) {
667
				continue;
668
			}
669
670
			if ($this->CountChange($folderid)) {
671
				$changesAvailable = true;
672
			}
673
		}
674
675
		return $changesAvailable;
676
	}
677
678
	/**
679
	 * Checks a folder for changes performing Exporter->GetChangeCount().
680
	 *
681
	 * @param string $folderid counts changes for a folder
682
	 *
683
	 * @return bool indicating if changes were found or not
684
	 */
685
	public function CountChange($folderid) {
686
		$spa = $this->GetCollection($folderid);
687
688
		if (!$spa) {
689
			SLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->CountChange(): Could not get SyncParameters object from cache for folderid '%s' to verify notification. Ignoring.", $folderid));
690
691
			return false;
692
		}
693
694
		$backendFolderId = GSync::GetDeviceManager()->GetBackendIdForFolderId($folderid);
695
		// switch user store if this is a additional folder (additional true -> do not debug)
696
		GSync::GetBackend()->Setup(GSync::GetAdditionalSyncFolderStore($backendFolderId, true));
697
		$changecount = false;
698
699
		try {
700
			$exporter = GSync::GetBackend()->GetExporter($backendFolderId);
701
			if ($exporter !== false && isset($this->addparms[$folderid]["state"])) {
702
				$importer = false;
703
				$exporter->Config($this->addparms[$folderid]["state"], BACKEND_DISCARD_DATA);
704
				$exporter->ConfigContentParameters($spa->GetCPO());
705
				$ret = $exporter->InitializeExporter($importer);
706
707
				if ($ret !== false) {
708
					$changecount = $exporter->GetChangeCount();
709
				}
710
			}
711
		}
712
		catch (StatusException $ste) {
713
			if ($ste->getCode() == SYNC_STATUS_FOLDERHIERARCHYCHANGED) {
714
				SLog::Write(LOGLEVEL_WARN, "SyncCollections->CountChange(): exporter can not be re-configured due to state error, emulating change in folder to force Sync.");
715
				$this->changes[$folderid] = 1;
716
				// make sure this folder is fully synched on next Sync request
717
				$this->invalidateFolderStat($spa);
0 ignored issues
show
Bug introduced by
It seems like $spa can also be of type true; however, parameter $spa of SyncCollections::invalidateFolderStat() does only seem to accept SyncParameters, 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

717
				$this->invalidateFolderStat(/** @scrutinizer ignore-type */ $spa);
Loading history...
718
719
				return true;
720
			}
721
722
			throw new StatusException("SyncCollections->CountChange(): exporter can not be re-configured.", self::ERROR_WRONG_HIERARCHY, null, LOGLEVEL_WARN);
723
		}
724
725
		// start over if exporter can not be configured atm
726
		if ($changecount === false) {
727
			SLog::Write(LOGLEVEL_WARN, "SyncCollections->CountChange(): no changes received from Exporter.");
728
		}
729
730
		$this->changes[$folderid] = $changecount;
731
732
		return $changecount > 0;
733
	}
734
735
	/**
736
	 * Checks the hierarchy for changes.
737
	 *
738
	 * @param bool       export changes, default: false
739
	 * @param mixed $exportChanges
740
	 *
741
	 * @return bool indicating if changes were found or not
742
	 */
743
	private function countHierarchyChange($exportChanges = false) {
744
		$folderid = false;
745
746
		// Check with device manager if the hierarchy should be reloaded.
747
		// New additional folders are loaded here.
748
		if (GSync::GetDeviceManager()->IsHierarchySyncRequired()) {
749
			SLog::Write(LOGLEVEL_DEBUG, "SyncCollections->countHierarchyChange(): DeviceManager says HierarchySync is required.");
750
751
			return true;
752
		}
753
754
		$changecount = false;
755
		if ($exportChanges || $this->hierarchyExporterChecked === false) {
756
			try {
757
				// if this is a validation (not first run), make sure to load the hierarchy data again
758
				if ($this->hierarchyExporterChecked === true && !$this->LoadCollection(false, true, false)) {
0 ignored issues
show
Bug introduced by
false of type false is incompatible with the type string expected by parameter $folderid of SyncCollections::LoadCollection(). ( Ignorable by Annotation )

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

758
				if ($this->hierarchyExporterChecked === true && !$this->LoadCollection(/** @scrutinizer ignore-type */ false, true, false)) {
Loading history...
759
					throw new StatusException("Invalid states found while re-loading hierarchy data.");
760
				}
761
762
				$changesMem = GSync::GetDeviceManager()->GetHierarchyChangesWrapper();
763
				// the hierarchyCache should now fully be initialized - check for changes in the additional folders
764
				$changesMem->Config(GSync::GetAdditionalSyncFolders(false));
765
766
				// reset backend to the main store
767
				GSync::GetBackend()->Setup(false);
768
				$exporter = GSync::GetBackend()->GetExporter();
769
				if ($exporter !== false && isset($this->addparms[$folderid]["state"])) {
770
					$exporter->Config($this->addparms[$folderid]["state"]);
771
					$ret = $exporter->InitializeExporter($changesMem);
772
					while (is_array($exporter->Synchronize()));
773
774
					if ($ret !== false) {
775
						$changecount = $changesMem->GetChangeCount();
776
					}
777
778
					$this->hierarchyExporterChecked = true;
779
				}
780
			}
781
			catch (StatusException $ste) {
782
				throw new StatusException("SyncCollections->countHierarchyChange(): exporter can not be re-configured.", self::ERROR_WRONG_HIERARCHY, null, LOGLEVEL_WARN);
783
			}
784
785
			// start over if exporter can not be configured atm
786
			if ($changecount === false) {
787
				SLog::Write(LOGLEVEL_WARN, "SyncCollections->countHierarchyChange(): no changes received from Exporter.");
788
			}
789
		}
790
791
		return $changecount > 0;
792
	}
793
794
	/**
795
	 * Returns an array with all folderid and the amount of changes found.
796
	 *
797
	 * @return array
798
	 */
799
	public function GetChangedFolderIds() {
800
		return $this->changes;
801
	}
802
803
	/**
804
	 * Indicates if there are folders which are pingable.
805
	 *
806
	 * @return bool
807
	 */
808
	public function PingableFolders() {
809
		foreach ($this->collections as $folderid => $spa) {
810
			if ($spa->GetPingableFlag() == true) {
811
				return true;
812
			}
813
		}
814
815
		return false;
816
	}
817
818
	/**
819
	 * Indicates if the process did wait in a sink, polling or before running a
820
	 * regular export to find changes.
821
	 *
822
	 * @return bool
823
	 */
824
	public function WaitedForChanges() {
825
		SLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->WaitedForChanges: waited for %d seconds", $this->waitingTime));
826
827
		return $this->waitingTime > 0;
828
	}
829
830
	/**
831
	 * Indicates how many seconds the process did wait in a sink, polling or before running a
832
	 * regular export to find changes.
833
	 *
834
	 * @return int
835
	 */
836
	public function GetWaitedSeconds() {
837
		return $this->waitingTime;
838
	}
839
840
	/**
841
	 * Returns how the current folder should be called in the PING comment.
842
	 *
843
	 * @param SyncParameters $spa
844
	 *
845
	 * @return string
846
	 */
847
	private function getPingClass($spa) {
848
		$class = $spa->GetContentClass();
0 ignored issues
show
Bug introduced by
The method GetContentClass() does not exist on SyncParameters. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

848
		/** @scrutinizer ignore-call */ 
849
  $class = $spa->GetContentClass();
Loading history...
849
		if ($class == "Calendar" && strpos($spa->GetFolderId(), DeviceManager::FLD_ORIGIN_GAB) === 0) {
0 ignored issues
show
Bug introduced by
It seems like $spa->GetFolderId() can also be of type boolean; however, parameter $haystack of strpos() 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

849
		if ($class == "Calendar" && strpos(/** @scrutinizer ignore-type */ $spa->GetFolderId(), DeviceManager::FLD_ORIGIN_GAB) === 0) {
Loading history...
850
			$class = "GAB";
851
		}
852
853
		return $class;
854
	}
855
856
	/**
857
	 * Simple Iterator Interface implementation to traverse through collections.
858
	 */
859
860
	/**
861
	 * Rewind the Iterator to the first element.
862
	 */
863
	public function rewind(): void {
864
		reset($this->collections);
865
	}
866
867
	/**
868
	 * Returns the current element.
869
	 */
870
	public function current(): mixed {
871
		return current($this->collections);
872
	}
873
874
	/**
875
	 * Return the key of the current element.
876
	 */
877
	public function key(): mixed {
878
		return key($this->collections);
879
	}
880
881
	/**
882
	 * Move forward to next element.
883
	 */
884
	public function next(): void {
885
		next($this->collections);
886
	}
887
888
	/**
889
	 * Checks if current position is valid.
890
	 */
891
	public function valid(): bool {
892
		return key($this->collections) !== null && key($this->collections) !== false;
893
	}
894
895
	/**
896
	 * Gets the StateManager from the DeviceManager
897
	 * if it's not available.
898
	 */
899
	private function loadStateManager() {
900
		if (!isset($this->stateManager)) {
901
			$this->stateManager = GSync::GetDeviceManager()->GetStateManager();
902
		}
903
	}
904
905
	/**
906
	 * Remove folder statistics from a SyncParameter object.
907
	 *
908
	 * @param SyncParameters $spa
909
	 */
910
	private function invalidateFolderStat($spa) {
911
		if ($spa->HasFolderStat()) {
0 ignored issues
show
Bug introduced by
The method HasFolderStat() does not exist on SyncParameters. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

911
		if ($spa->/** @scrutinizer ignore-call */ HasFolderStat()) {
Loading history...
912
			SLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->invalidateFolderStat(): removing folder stat '%s' for folderid '%s'", $spa->GetFolderStat(), $spa->GetFolderId()));
0 ignored issues
show
Bug introduced by
The method GetFolderStat() does not exist on SyncParameters. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

912
			SLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->invalidateFolderStat(): removing folder stat '%s' for folderid '%s'", $spa->/** @scrutinizer ignore-call */ GetFolderStat(), $spa->GetFolderId()));
Loading history...
913
			$spa->DelFolderStat();
0 ignored issues
show
Bug introduced by
The method DelFolderStat() does not exist on SyncParameters. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

913
			$spa->/** @scrutinizer ignore-call */ 
914
         DelFolderStat();
Loading history...
914
			$this->SaveCollection($spa);
915
916
			return true;
917
		}
918
919
		return false;
920
	}
921
}
922