SyncCollections::current()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

250
		if (!$spa->/** @scrutinizer ignore-call */ HasFolderId()) {
Loading history...
251
			return false;
252
		}
253
254
		$this->collections[$spa->GetFolderId()] = $spa;
255
256
		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

256
		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

256
		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...
257
		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

257
		if ($spa->/** @scrutinizer ignore-call */ HasLastSyncTime() && $spa->GetLastSyncTime() > $this->lastSyncTime) {
Loading history...
258
			$this->lastSyncTime = $spa->GetLastSyncTime();
259
260
			// use SyncParameters PolicyKey as reference if available
261
			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

261
			if ($spa->/** @scrutinizer ignore-call */ HasReferencePolicyKey()) {
Loading history...
262
				$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

262
				/** @scrutinizer ignore-call */ 
263
    $this->refPolicyKey = $spa->GetReferencePolicyKey();
Loading history...
263
			}
264
265
			// use SyncParameters LifeTime as reference if available
266
			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

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

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

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

845
		/** @scrutinizer ignore-call */ 
846
  $class = $spa->GetContentClass();
Loading history...
846
		if ($class == "Calendar" && str_starts_with((string) $spa->GetFolderId(), DeviceManager::FLD_ORIGIN_GAB)) {
847
			$class = "GAB";
848
		}
849
850
		return $class;
851
	}
852
853
	/**
854
	 * Simple Iterator Interface implementation to traverse through collections.
855
	 */
856
857
	/**
858
	 * Rewind the Iterator to the first element.
859
	 */
860
	public function rewind(): void {
861
		reset($this->collections);
862
	}
863
864
	/**
865
	 * Returns the current element.
866
	 */
867
	public function current(): mixed {
868
		return current($this->collections);
869
	}
870
871
	/**
872
	 * Return the key of the current element.
873
	 */
874
	public function key(): mixed {
875
		return key($this->collections);
876
	}
877
878
	/**
879
	 * Move forward to next element.
880
	 */
881
	public function next(): void {
882
		next($this->collections);
883
	}
884
885
	/**
886
	 * Checks if current position is valid.
887
	 */
888
	public function valid(): bool {
889
		return key($this->collections) !== null && key($this->collections) !== false;
890
	}
891
892
	/**
893
	 * Gets the StateManager from the DeviceManager
894
	 * if it's not available.
895
	 */
896
	private function loadStateManager() {
897
		if (!isset($this->stateManager)) {
898
			$this->stateManager = GSync::GetDeviceManager()->GetStateManager();
899
		}
900
	}
901
902
	/**
903
	 * Remove folder statistics from a SyncParameter object.
904
	 *
905
	 * @param SyncParameters $spa
906
	 */
907
	private function invalidateFolderStat($spa) {
908
		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

908
		if ($spa->/** @scrutinizer ignore-call */ HasFolderStat()) {
Loading history...
909
			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

909
			SLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->invalidateFolderStat(): removing folder stat '%s' for folderid '%s'", $spa->/** @scrutinizer ignore-call */ GetFolderStat(), $spa->GetFolderId()));
Loading history...
910
			$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

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