Total Complexity | 149 |
Total Lines | 899 |
Duplicated Lines | 0 % |
Changes | 4 | ||
Bugs | 2 | Features | 0 |
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 |
||
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) { |
||
|
|||
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)) { |
||
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) { |
||
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) { |
||
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); |
||
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 |
||
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()) { |
||
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())); |
||
256 | if ($spa->HasLastSyncTime() && $spa->GetLastSyncTime() > $this->lastSyncTime) { |
||
257 | $this->lastSyncTime = $spa->GetLastSyncTime(); |
||
258 | |||
259 | // use SyncParameters PolicyKey as reference if available |
||
260 | if ($spa->HasReferencePolicyKey()) { |
||
261 | $this->refPolicyKey = $spa->GetReferencePolicyKey(); |
||
262 | } |
||
263 | |||
264 | // use SyncParameters LifeTime as reference if available |
||
265 | if ($spa->HasReferenceLifetime()) { |
||
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() { |
||
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() { |
||
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) { |
||
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); |
||
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)) { |
||
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(); |
||
849 | if ($class == "Calendar" && strpos($spa->GetFolderId(), DeviceManager::FLD_ORIGIN_GAB) === 0) { |
||
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 { |
||
893 | } |
||
894 | |||
895 | /** |
||
896 | * Gets the StateManager from the DeviceManager |
||
897 | * if it's not available. |
||
898 | */ |
||
899 | private function loadStateManager() { |
||
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()) { |
||
912 | SLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->invalidateFolderStat(): removing folder stat '%s' for folderid '%s'", $spa->GetFolderStat(), $spa->GetFolderId())); |
||
913 | $spa->DelFolderStat(); |
||
914 | $this->SaveCollection($spa); |
||
915 | |||
916 | return true; |
||
917 | } |
||
918 | |||
919 | return false; |
||
920 | } |
||
921 | } |
||
922 |