1<?php
2/***********************************************
3* File      :   devicemanager.php
4* Project   :   Z-Push
5* Descr     :   Manages device relevant data, provisioning,
6*               loop detection and device states.
7*               The DeviceManager uses a IStateMachine
8*               implementation with IStateMachine::DEVICEDATA
9*               to save device relevant data.
10*
11* Created   :   11.04.2011
12*
13* Copyright 2007 - 2016 Zarafa Deutschland GmbH
14*
15* This program is free software: you can redistribute it and/or modify
16* it under the terms of the GNU Affero General Public License, version 3,
17* as published by the Free Software Foundation.
18*
19* This program is distributed in the hope that it will be useful,
20* but WITHOUT ANY WARRANTY; without even the implied warranty of
21* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22* GNU Affero General Public License for more details.
23*
24* You should have received a copy of the GNU Affero General Public License
25* along with this program.  If not, see <http://www.gnu.org/licenses/>.
26*
27* Consult LICENSE file for details
28************************************************/
29
30class DeviceManager {
31    // broken message indicators
32    const MSG_BROKEN_UNKNOWN = 1;
33    const MSG_BROKEN_CAUSINGLOOP = 2;
34    const MSG_BROKEN_SEMANTICERR = 4;
35
36    const FLD_SYNC_INITIALIZED = 1;
37    const FLD_SYNC_INPROGRESS = 2;
38    const FLD_SYNC_COMPLETED = 4;
39
40    // new types need to be added to Request::HEX_EXTENDED2 filter
41    const FLD_ORIGIN_USER = "U";
42    const FLD_ORIGIN_CONFIG = "C";
43    const FLD_ORIGIN_SHARED = "S";
44    const FLD_ORIGIN_GAB = "G";
45    const FLD_ORIGIN_IMPERSONATED = "I";
46
47    const FLD_FLAGS_NONE = 0;
48    const FLD_FLAGS_SENDASOWNER = 1;
49    const FLD_FLAGS_TRACKSHARENAME = 2;
50    const FLD_FLAGS_CALENDARREMINDERS = 4;
51    const FLD_FLAGS_NOREADONLYNOTIFY = 8;
52
53    private $device;
54    private $deviceHash;
55    private $saveDevice;
56    private $statemachine;
57    private $stateManager;
58    private $incomingData = 0;
59    private $outgoingData = 0;
60
61    private $windowSize;
62    private $latestFolder;
63
64    private $loopdetection;
65    private $hierarchySyncRequired;
66    private $additionalFoldersHash;
67
68    /**
69     * Constructor
70     *
71     * @access public
72     */
73    public function __construct() {
74        $this->statemachine = ZPush::GetStateMachine();
75        $this->deviceHash = false;
76        $this->devid = Request::GetDeviceID();
77        $this->saveDevice = true;
78        $this->windowSize = array();
79        $this->latestFolder = false;
80        $this->hierarchySyncRequired = false;
81
82        // only continue if deviceid is set
83        if ($this->devid) {
84            $this->device = new ASDevice($this->devid, Request::GetDeviceType(), Request::GetGETUser(), Request::GetUserAgent());
85            $this->loadDeviceData();
86
87            ZPush::GetTopCollector()->SetUserAgent($this->device->GetDeviceUserAgent());
88        }
89        else
90            throw new FatalNotImplementedException("Can not proceed without a device id.");
91
92        $this->loopdetection = new LoopDetection();
93        $this->loopdetection->ProcessLoopDetectionInit();
94        $this->loopdetection->ProcessLoopDetectionPreviousConnectionFailed();
95
96        $this->stateManager = new StateManager();
97        $this->stateManager->SetDevice($this->device);
98
99        $this->additionalFoldersHash = $this->getAdditionalFoldersHash();
100
101        if ($this->IsKoe() && $this->device->GetKoeVersion() !== false) {
102            ZLog::Write(LOGLEVEL_DEBUG, sprintf("KOE: %s / %s / %s", $this->device->GetKoeVersion(), $this->device->GetKoeBuild(), strftime("%Y-%m-%d %H:%M", $this->device->GetKoeBuildDate())));
103            ZLog::Write(LOGLEVEL_DEBUG, sprintf("KOE Capabilities: %s ", count($this->device->GetKoeCapabilities()) ? implode(',', $this->device->GetKoeCapabilities()) : 'unknown'));
104            ZLog::Write(LOGLEVEL_DEBUG, sprintf("KOE Last confirmed access: %s (may be up to 7h old)", ($this->device->GetKoeLastAccess() ? strftime("%Y-%m-%d %H:%M", $this->device->GetKoeLastAccess()) : 'unknown')));
105        }
106    }
107
108    /**
109     * Load another different device.
110     * @param ASDevice $asDevice
111     */
112    public function SetDevice($asDevice) {
113        $this->device = $asDevice;
114        $this->loadDeviceData();
115        $this->stateManager->SetDevice($this->device);
116    }
117
118    /**
119     * Returns the StateManager for the current device
120     *
121     * @access public
122     * @return StateManager
123     */
124    public function GetStateManager() {
125        return $this->stateManager;
126    }
127
128    /**----------------------------------------------------------------------------------------------------------
129     * Device operations
130     */
131
132    /**
133     * Announces amount of transmitted data to the DeviceManager
134     *
135     * @param int           $datacounter
136     *
137     * @access public
138     * @return boolean
139     */
140    public function SentData($datacounter) {
141        // TODO save this somewhere
142        $this->incomingData = Request::GetContentLength();
143        $this->outgoingData = $datacounter;
144    }
145
146    /**
147     * Called at the end of the request
148     * Statistics about received/sent data is saved here
149     *
150     * @access public
151     * @return boolean
152     */
153    public function Save() {
154        // TODO save other stuff
155
156        // check if previousily ignored messages were synchronized for the current folder
157        // on multifolder operations of AS14 this is done by setLatestFolder()
158        if ($this->latestFolder !== false)
159            $this->checkBrokenMessages($this->latestFolder);
160
161        // update the user agent and AS version on the device
162        $this->device->SetUserAgent(Request::GetUserAgent());
163        $this->device->SetASVersion(Request::GetProtocolVersion());
164
165        // update data from the OL plugin (if available)
166        if (Request::HasKoeStats()) {
167            $this->device->SetKoeVersion(Request::GetKoeVersion());
168            $this->device->SetKoeBuild(Request::GetKoeBuild());
169            $this->device->SetKoeBuildDate(Request::GetKoeBuildDate());
170            $this->device->SetKoeCapabilities(Request::GetKoeCapabilities());
171            // update KOE last access time if it's at least 6h old
172            if ($this->device->GetKoeLastAccess() < time() - 21600) {
173                $this->device->SetKoeLastAccess(time());
174            }
175        }
176
177        // data to be saved
178        $data = $this->device->GetData();
179        if ($data && Request::IsValidDeviceID() && $this->saveDevice) {
180            ZLog::Write(LOGLEVEL_DEBUG, "DeviceManager->Save(): Device data changed");
181
182            try {
183                // check if this is the first time the device data is saved and it is authenticated. If so, link the user to the device id
184                if ($this->device->IsNewDevice() && RequestProcessor::isUserAuthenticated()) {
185                    ZLog::Write(LOGLEVEL_INFO, sprintf("Linking device ID '%s' to user '%s'", $this->devid, $this->device->GetDeviceUser()));
186                    $this->statemachine->LinkUserDevice($this->device->GetDeviceUser(), $this->devid);
187                }
188
189                if (RequestProcessor::isUserAuthenticated() || $this->device->GetForceSave() ) {
190                    $this->statemachine->SetState($data, $this->devid, IStateMachine::DEVICEDATA);
191                    ZLog::Write(LOGLEVEL_DEBUG, "DeviceManager->Save(): Device data saved");
192                }
193            }
194            catch (StateNotFoundException $snfex) {
195                ZLog::Write(LOGLEVEL_ERROR, "DeviceManager->Save(): Exception: ". $snfex->getMessage());
196            }
197        }
198
199        // remove old search data
200        $oldpid = $this->loopdetection->ProcessLoopDetectionGetOutdatedSearchPID();
201        if ($oldpid) {
202            ZPush::GetBackend()->GetSearchProvider()->TerminateSearch($oldpid);
203        }
204
205        // we terminated this process
206        if ($this->loopdetection)
207            $this->loopdetection->ProcessLoopDetectionTerminate();
208
209        return true;
210    }
211
212    /**
213     * Sets if the AS Device should automatically be saved when terminating the request.
214     *
215     * @param boolean $doSave
216     *
217     * @access public
218     * @return void
219     */
220    public function DoAutomaticASDeviceSaving($doSave) {
221        ZLog::Write(LOGLEVEL_DEBUG, "DeviceManager->DoAutomaticASDeviceSaving(): save automatically: ". Utils::PrintAsString($doSave));
222        $this->saveDevice = $doSave;
223    }
224
225    /**
226     * Newer mobiles send extensive device informations with the Settings command
227     * These informations are saved in the ASDevice
228     *
229     * @param SyncDeviceInformation     $deviceinformation
230     *
231     * @access public
232     * @return boolean
233     */
234    public function SaveDeviceInformation($deviceinformation) {
235        ZLog::Write(LOGLEVEL_DEBUG, "Saving submitted device information");
236
237        // set the user agent
238        if (isset($deviceinformation->useragent))
239            $this->device->SetUserAgent($deviceinformation->useragent);
240
241        // save other informations
242        foreach (array("model", "imei", "friendlyname", "os", "oslanguage", "phonenumber", "mobileoperator", "enableoutboundsms") as $info) {
243            if (isset($deviceinformation->$info) && $deviceinformation->$info != "") {
244                $this->device->__set("device".$info, $deviceinformation->$info);
245            }
246        }
247        return true;
248    }
249
250    /**----------------------------------------------------------------------------------------------------------
251     * Provisioning operations
252     */
253
254    /**
255     * Checks if the sent policykey matches the latest policykey
256     * saved for the device
257     *
258     * @param string        $policykey
259     * @param boolean       $noDebug        (opt) by default, debug message is shown
260     * @param boolean       $checkPolicies  (opt) by default check if the provisioning policies changed
261     *
262     * @access public
263     * @return boolean
264     */
265    public function ProvisioningRequired($policykey, $noDebug = false, $checkPolicies = true) {
266        $this->loadDeviceData();
267
268        // check if a remote wipe is required
269        if ($this->device->GetWipeStatus() > SYNC_PROVISION_RWSTATUS_OK) {
270            ZLog::Write(LOGLEVEL_INFO, sprintf("DeviceManager->ProvisioningRequired('%s'): YES, remote wipe requested", $policykey));
271            return true;
272        }
273
274        $p = ( ($this->device->GetWipeStatus() != SYNC_PROVISION_RWSTATUS_NA && $policykey != $this->device->GetPolicyKey()) ||
275              Request::WasPolicyKeySent() && $this->device->GetPolicyKey() == ASDevice::UNDEFINED );
276
277        if (!$noDebug || $p)
278            ZLog::Write(LOGLEVEL_DEBUG, sprintf("DeviceManager->ProvisioningRequired('%s') saved device key '%s': %s", $policykey, $this->device->GetPolicyKey(), Utils::PrintAsString($p)));
279
280        if ($checkPolicies) {
281            $policyHash = $this->GetProvisioningObject()->GetPolicyHash();
282            if ($this->device->hasPolicyhash() && $this->device->getPolicyhash() != $policyHash) {
283                $p = true;
284                ZLog::Write(LOGLEVEL_INFO, sprintf("DeviceManager->ProvisioningRequired(): saved policy hash '%s' changed '%s'. Provisioning required.", $this->device->getPolicyhash(), $policyHash));
285            }
286        }
287
288        return $p;
289    }
290
291    /**
292     * Generates a new Policykey
293     *
294     * @access public
295     * @return int
296     */
297    public function GenerateProvisioningPolicyKey() {
298        return mt_rand(100000000, 999999999);
299    }
300
301    /**
302     * Attributes a provisioned policykey to a device
303     *
304     * @param int           $policykey
305     *
306     * @access public
307     * @return boolean      status
308     */
309    public function SetProvisioningPolicyKey($policykey) {
310        ZLog::Write(LOGLEVEL_DEBUG, sprintf("DeviceManager->SetPolicyKey('%s')", $policykey));
311        return $this->device->SetPolicyKey($policykey);
312    }
313
314    /**
315     * Builds a Provisioning SyncObject with policies
316     *
317     * @param boolean   $logPolicies  optional, determines if the policies and values should be logged. Default: false
318     *
319     * @access public
320     * @return SyncProvisioning
321     */
322    public function GetProvisioningObject($logPolicies = false) {
323        $policyName = $this->getPolicyName();
324        $p = SyncProvisioning::GetObjectWithPolicies($this->getProvisioningPolicies($policyName), $logPolicies);
325        $p->PolicyName = $policyName;
326        return $p;
327    }
328
329    /**
330     * Returns the status of the remote wipe policy
331     *
332     * @access public
333     * @return int          returns the current status of the device - SYNC_PROVISION_RWSTATUS_*
334     */
335    public function GetProvisioningWipeStatus() {
336        return $this->device->GetWipeStatus();
337    }
338
339    /**
340     * Updates the status of the remote wipe
341     *
342     * @param int           $status - SYNC_PROVISION_RWSTATUS_*
343     *
344     * @access public
345     * @return boolean      could fail if trying to update status to a wipe status which was not requested before
346     */
347    public function SetProvisioningWipeStatus($status) {
348        ZLog::Write(LOGLEVEL_DEBUG, sprintf("DeviceManager->SetProvisioningWipeStatus() change from '%d' to '%d'",$this->device->GetWipeStatus(), $status));
349
350        if ($status > SYNC_PROVISION_RWSTATUS_OK && !($this->device->GetWipeStatus() > SYNC_PROVISION_RWSTATUS_OK)) {
351            ZLog::Write(LOGLEVEL_ERROR, "Not permitted to update remote wipe status to a higher value as remote wipe was not initiated!");
352            return false;
353        }
354        $this->device->SetWipeStatus($status);
355        return true;
356    }
357
358    /**
359     * Saves the policy hash and name in device's state.
360     *
361     * @param SyncProvisioning  $provisioning
362     *
363     * @access public
364     * @return void
365     */
366    public function SavePolicyHashAndName($provisioning) {
367        // save policies' hash and name
368        $this->device->SetPolicyname($provisioning->PolicyName);
369        $this->device->SetPolicyhash($provisioning->GetPolicyHash());
370        ZLog::Write(LOGLEVEL_DEBUG, sprintf("DeviceManager->SavePolicyHashAndName(): Set policy: %s with hash: %s", $this->device->GetPolicyname(), $this->device->GetPolicyhash()));
371    }
372
373
374    /**----------------------------------------------------------------------------------------------------------
375     * LEGACY AS 1.0 and WRAPPER operations
376     */
377
378    /**
379     * Returns a wrapped Importer & Exporter to use the
380     * HierarchyChache
381     *
382     * @see ChangesMemoryWrapper
383     * @access public
384     * @return object           HierarchyCache
385     */
386    public function GetHierarchyChangesWrapper() {
387        return $this->device->GetHierarchyCache();
388    }
389
390    /**
391     * Initializes the HierarchyCache for legacy syncs
392     * this is for AS 1.0 compatibility:
393     *      save folder information synched with GetHierarchy()
394     *
395     * @param string    $folders            Array with folder information
396     *
397     * @access public
398     * @return boolean
399     */
400    public function InitializeFolderCache($folders) {
401        $this->stateManager->SetDevice($this->device);
402        return $this->stateManager->InitializeFolderCache($folders);
403    }
404
405    /**
406     * Returns the ActiveSync folder type for a FolderID
407     *
408     * @param string    $folderid
409     *
410     * @access public
411     * @return int/boolean        boolean if no type is found
412     */
413    public function GetFolderTypeFromCacheById($folderid) {
414        return $this->device->GetFolderType($folderid);
415    }
416
417    /**
418     * Returns a FolderID of default classes
419     * this is for AS 1.0 compatibility:
420     *      this information was made available during GetHierarchy()
421     *
422     * @param string    $class              The class requested
423     *
424     * @access public
425     * @return string
426     * @throws NoHierarchyCacheAvailableException
427     */
428    public function GetFolderIdFromCacheByClass($class) {
429        $folderidforClass = false;
430        // look at the default foldertype for this class
431        $type = ZPush::getDefaultFolderTypeFromFolderClass($class);
432
433        if ($type && $type > SYNC_FOLDER_TYPE_OTHER && $type < SYNC_FOLDER_TYPE_USER_MAIL) {
434            $folderids = $this->device->GetAllFolderIds();
435            foreach ($folderids as $folderid) {
436                if ($type == $this->device->GetFolderType($folderid)) {
437                    $folderidforClass = $folderid;
438                    break;
439                }
440            }
441
442            // Old Palm Treos always do initial sync for calendar and contacts, even if they are not made available by the backend.
443            // We need to fake these folderids, allowing a fake sync/ping, even if they are not supported by the backend
444            // if the folderid would be available, they would already be returned in the above statement
445            if ($folderidforClass == false && ($type == SYNC_FOLDER_TYPE_APPOINTMENT || $type == SYNC_FOLDER_TYPE_CONTACT))
446                $folderidforClass = SYNC_FOLDER_TYPE_DUMMY;
447        }
448
449        ZLog::Write(LOGLEVEL_DEBUG, sprintf("DeviceManager->GetFolderIdFromCacheByClass('%s'): '%s' => '%s'", $class, $type, $folderidforClass));
450        return $folderidforClass;
451    }
452
453    /**
454     * Returns a FolderClass for a FolderID which is known to the mobile
455     *
456     * @param string    $folderid
457     *
458     * @access public
459     * @return int
460     * @throws NoHierarchyCacheAvailableException, NotImplementedException
461     */
462    public function GetFolderClassFromCacheByID($folderid) {
463        //TODO check if the parent folder exists and is also beeing synchronized
464        $typeFromCache = $this->device->GetFolderType($folderid);
465        if ($typeFromCache === false)
466            throw new NoHierarchyCacheAvailableException(sprintf("Folderid '%s' is not fully synchronized on the device", $folderid));
467
468        $class = ZPush::GetFolderClassFromFolderType($typeFromCache);
469        if ($class === false)
470            throw new NotImplementedException(sprintf("Folderid '%s' is saved to be of type '%d' but this type is not implemented", $folderid, $typeFromCache));
471
472        return $class;
473    }
474
475    /**
476     * Returns the backend folder id of the KOE GAB folder.
477     * This comes either from the configuration or from the device data.
478     *
479     * @return string|boolean   returns false if not set or found
480     */
481    public function GetKoeGabBackendFolderId() {
482        $gabid = false;
483        if (KOE_CAPABILITY_GAB) {
484            if (KOE_GAB_FOLDERID) {
485                $gabid = KOE_GAB_FOLDERID;
486            }
487            else if (KOE_GAB_STORE && KOE_GAB_NAME) {
488                $gabid = $this->device->GetKoeGabBackendFolderId();
489            }
490        }
491        return $gabid;
492    }
493
494    /**
495     * Returns the additional folders as SyncFolder objects.
496     *
497     * @access public
498     * @return array of SyncFolder with backendids as keys
499     */
500    public function GetAdditionalUserSyncFolders() {
501        $folders = array();
502
503        // In impersonated stores, no additional folders will be synchronized
504        if (Request::GetImpersonatedUser()) {
505            return $folders;
506        }
507
508        foreach($this->device->GetAdditionalFolders() as $df) {
509            if (!isset($df['flags'])) {
510                $df['flags'] = 0;
511                ZLog::Write(LOGLEVEL_WARN, sprintf("DeviceManager->GetAdditionalUserSyncFolders(): Additional folder '%s' has no flags. Please run 'z-push-admin -a fixstates' to fix this issue.", $df['name']));
512            }
513            if (!isset($df['parentid'])) {
514                $df['parentid'] = '0';
515                ZLog::Write(LOGLEVEL_WARN, sprintf("DeviceManager->GetAdditionalUserSyncFolders(): Additional folder '%s' has no parentid. Please run 'z-push-admin -a fixstates' to fix this issue.", $df['name']));
516            }
517
518            $folder = $this->BuildSyncFolderObject($df['store'], $df['folderid'], $df['parentid'], $df['name'], $df['type'], $df['flags'], DeviceManager::FLD_ORIGIN_SHARED);
519            $folders[$folder->BackendId] = $folder;
520        }
521
522        // ZO-40: add KOE GAB folder
523        if (KOE_CAPABILITY_GAB && $this->IsKoe() && KOE_GAB_STORE != "" && KOE_GAB_NAME != "") {
524            // if KOE_GAB_FOLDERID is set, use it
525            if (KOE_GAB_FOLDERID != "") {
526                $folder = $this->BuildSyncFolderObject(KOE_GAB_STORE, KOE_GAB_FOLDERID, '0', KOE_GAB_NAME, SYNC_FOLDER_TYPE_USER_APPOINTMENT, 0, DeviceManager::FLD_ORIGIN_GAB);
527                $folders[$folder->BackendId] = $folder;
528            }
529            else {
530                // get the GAB id from the device and from the backend
531                $deviceGabId = $this->device->GetKoeGabBackendFolderId();
532                if (!ZPush::GetBackend()->Setup(KOE_GAB_STORE)) {
533                    ZLog::Write(LOGLEVEL_WARN, sprintf("DeviceManager->GetAdditionalUserSyncFolders(): setup for store '%s' failed. Unable to search for KOE GAB folder.", KOE_GAB_STORE));
534                }
535                else {
536                    $backendGabId = ZPush::GetBackend()->GetKoeGabBackendFolderId(KOE_GAB_NAME);
537                    if ($deviceGabId !== $backendGabId) {
538                        ZLog::Write(LOGLEVEL_DEBUG, sprintf("DeviceManager->GetAdditionalUserSyncFolders(): Backend found different KOE GAB backend folderid: '%s'. Updating ASDevice.", $backendGabId));
539                        $this->device->SetKoeGabBackendFolderId($backendGabId);
540                    }
541
542                    if ($backendGabId) {
543                        $folders[$backendGabId] = $this->BuildSyncFolderObject(KOE_GAB_STORE, $backendGabId, '0', KOE_GAB_NAME, SYNC_FOLDER_TYPE_USER_APPOINTMENT, 0, DeviceManager::FLD_ORIGIN_GAB);
544                    }
545                }
546            }
547        }
548        return $folders;
549    }
550
551    /**
552     * Get the store of an additional folder.
553     *
554     * @param string    $folderid
555     *
556     * @access public
557     * @return boolean|string
558     */
559    public function GetAdditionalUserSyncFolder($folderid) {
560        // is this the KOE GAB folder?
561        if ($folderid && $folderid === $this->GetKoeGabBackendFolderId()) {
562            return KOE_GAB_STORE;
563        }
564
565        $f = $this->device->GetAdditionalFolder($folderid);
566        if ($f) {
567            return $f;
568        }
569
570        return false;
571    }
572
573    /**
574     * Checks if the message should be streamed to a mobile
575     * Should always be called before a message is sent to the mobile
576     * Returns true if there is something wrong and the content could break the
577     * synchronization
578     *
579     * @param string        $id         message id
580     * @param SyncObject    &$message   the method could edit the message to change the flags
581     *
582     * @access public
583     * @return boolean          returns true if the message should NOT be send!
584     */
585    public function DoNotStreamMessage($id, &$message) {
586        $folderid = $this->getLatestFolder();
587
588        if (isset($message->parentid))
589            $folder = $message->parentid;
590
591        // message was identified to be causing a loop
592        if ($this->loopdetection->IgnoreNextMessage(true, $id, $folderid)) {
593            $this->AnnounceIgnoredMessage($folderid, $id, $message, self::MSG_BROKEN_CAUSINGLOOP);
594            return true;
595        }
596
597        // message is semantically incorrect
598        if (!$message->Check(true)) {
599            $this->AnnounceIgnoredMessage($folderid, $id, $message, self::MSG_BROKEN_SEMANTICERR);
600            return true;
601        }
602
603        // check if this message is broken
604        if ($this->device->HasIgnoredMessage($folderid, $id)) {
605            // reset the flags so the message is always streamed with <Add>
606            $message->flags = false;
607
608            // track the broken message in the loop detection
609            $this->loopdetection->SetBrokenMessage($folderid, $id);
610        }
611        return false;
612    }
613
614    /**
615     * Removes device information about a broken message as it is been removed from the mobile.
616     *
617     * @param string        $id         message id
618     *
619     * @access public
620     * @return boolean
621     */
622    public function RemoveBrokenMessage($id) {
623        $folderid = $this->getLatestFolder();
624        if ($this->device->RemoveIgnoredMessage($folderid, $id)) {
625            ZLog::Write(LOGLEVEL_INFO, sprintf("DeviceManager->RemoveBrokenMessage('%s', '%s'): cleared data about previously ignored message", $folderid, $id));
626            return true;
627        }
628        return false;
629    }
630
631    /**
632     * Amount of items to me synchronized
633     *
634     * @param string    $folderid
635     * @param string    $type
636     * @param int       $queuedmessages;
637     * @access public
638     * @return int
639     */
640    public function GetWindowSize($folderid, $uuid, $statecounter, $queuedmessages) {
641        if (isset($this->windowSize[$folderid]))
642            $items = $this->windowSize[$folderid];
643        else
644            $items = WINDOW_SIZE_MAX; // 512 by default
645
646        $this->setLatestFolder($folderid);
647
648        // detect if this is a loop condition
649        $loop = $this->loopdetection->Detect($folderid, $uuid, $statecounter, $items, $queuedmessages);
650        if ($loop !== false) {
651            if ($loop === true) {
652                $items = ($items == 0) ? 0: 1+($this->loopdetection->IgnoreNextMessage(false)?1:0) ;
653            }
654            else {
655                // we got a new suggested window size
656                $items = $loop;
657                ZLog::Write(LOGLEVEL_DEBUG, sprintf("Mobile loop pre stage detected! Forcing smaller window size of %d before entering loop detection mode", $items));
658            }
659        }
660
661        if ($items >= 0 && $items <= 2)
662            ZLog::Write(LOGLEVEL_WARN, sprintf("Mobile loop detected! Messages sent to the mobile will be restricted to %d items in order to identify the conflict", $items));
663
664        return $items;
665    }
666
667    /**
668     * Sets the amount of items the device is requesting
669     *
670     * @param string    $folderid
671     * @param int       $maxItems
672     *
673     * @access public
674     * @return boolean
675     */
676    public function SetWindowSize($folderid, $maxItems) {
677        $this->windowSize[$folderid] = $maxItems;
678
679        return true;
680    }
681
682     /**
683     * Sets the supported fields transmitted by the device for a certain folder
684     *
685     * @param string    $folderid
686     * @param array     $fieldlist          supported fields
687     *
688     * @access public
689     * @return boolean
690     */
691    public function SetSupportedFields($folderid, $fieldlist) {
692        return $this->device->SetSupportedFields($folderid, $fieldlist);
693    }
694
695    /**
696     * Gets the supported fields transmitted previousely by the device
697     * for a certain folder
698     *
699     * @param string    $folderid
700     *
701     * @access public
702     * @return array/boolean
703     */
704    public function GetSupportedFields($folderid) {
705        return $this->device->GetSupportedFields($folderid);
706    }
707
708    /**
709     * Returns the maximum filter type for a folder.
710     * This might be limited globally, per device or per folder.
711     *
712     * @param string    $folderid
713     *
714     * @access public
715     * @return int
716     */
717    public function GetFilterType($folderid, $backendFolderId) {
718        global $specialSyncFilter;
719        // either globally configured SYNC_FILTERTIME_MAX or ALL (no limit)
720        $maxAllowed = (defined('SYNC_FILTERTIME_MAX') && SYNC_FILTERTIME_MAX > SYNC_FILTERTYPE_ALL) ? SYNC_FILTERTIME_MAX : SYNC_FILTERTYPE_ALL;
721
722        // TODO we could/should check for a specific value for the folder, if it's available
723        $maxDevice = $this->device->GetSyncFilterType();
724
725        // ALL has a value of 0, all limitations have higher integer values, see SYNC_FILTERTYPE_ALL definition
726        if ($maxDevice !== false && $maxDevice > SYNC_FILTERTYPE_ALL && ($maxAllowed == SYNC_FILTERTYPE_ALL || $maxDevice < $maxAllowed)) {
727            $maxAllowed = $maxDevice;
728        }
729
730        if (is_array($specialSyncFilter)) {
731            $store = ZPush::GetAdditionalSyncFolderStore($backendFolderId);
732            // the store is only available when this is a shared folder (but might also be statically configured)
733            if ($store) {
734                $origin = Utils::GetFolderOriginFromId($folderid);
735                // do not limit when the owner or impersonated user is synching!
736                if ($origin == DeviceManager::FLD_ORIGIN_USER || $origin == DeviceManager::FLD_ORIGIN_IMPERSONATED) {
737                    ZLog::Write(LOGLEVEL_DEBUG, "Not checking for specific sync limit as this is the owner/impersonated user.");
738                }
739                else {
740                    $spKey = false;
741                    $spFilter = false;
742                    // 1. step: check if there is a general limitation for the store
743                    if (array_key_exists($store, $specialSyncFilter)) {
744                        $spFilter = $specialSyncFilter[$store];
745                        ZLog::Write(LOGLEVEL_DEBUG, sprintf("Limit sync due to configured limitation on the store: '%s': %s",$store, $spFilter));
746                    }
747
748                    // 2. step: check if there is a limitation for the hashed ID (for shared/configured stores)
749                    $spKey= $store .'/'. $folderid;
750                    if (array_key_exists($spKey, $specialSyncFilter)) {
751                        $spFilter = $specialSyncFilter[$spKey];
752                        ZLog::Write(LOGLEVEL_DEBUG, sprintf("Limit sync due to configured limitation on the folder: '%s': %s", $spKey, $spFilter));
753                    }
754
755                    // 3. step: check if there is a limitation for the backendId
756                    $spKey= $store .'/'. $backendFolderId;
757                    if (array_key_exists($spKey, $specialSyncFilter)) {
758                        $spFilter = $specialSyncFilter[$spKey];
759                        ZLog::Write(LOGLEVEL_DEBUG, sprintf("Limit sync due to configured limitation on the folder: '%s': %s", $spKey, $spFilter));
760                    }
761                    if ($spFilter) {
762                        $maxAllowed = $spFilter;
763                    }
764                }
765            }
766        }
767
768        return $maxAllowed;
769    }
770
771    /**
772     * Removes all linked states of a specific folder.
773     * During next request the folder is resynchronized.
774     *
775     * @param string    $folderid
776     *
777     * @access public
778     * @return boolean
779     */
780    public function ForceFolderResync($folderid) {
781        ZLog::Write(LOGLEVEL_INFO, sprintf("DeviceManager->ForceFolderResync('%s'): folder resync", $folderid));
782
783        // delete folder states
784        StateManager::UnLinkState($this->device, $folderid);
785
786        return true;
787    }
788
789    /**
790     * Removes all linked states from a device.
791     * During next requests a full resync is triggered.
792     *
793     * @access public
794     * @return boolean
795     */
796    public function ForceFullResync() {
797        ZLog::Write(LOGLEVEL_INFO, "Full device resync requested");
798
799        // delete all other uuids
800        foreach ($this->device->GetAllFolderIds() as $folderid)
801            $uuid = StateManager::UnLinkState($this->device, $folderid);
802
803        // delete hierarchy states
804        StateManager::UnLinkState($this->device, false);
805
806        return true;
807    }
808
809    /**
810     * Indicates if the hierarchy should be resynchronized based on the general folder state and
811     * if additional folders changed.
812     *
813     * @access public
814     * @return boolean
815     */
816    public function IsHierarchySyncRequired() {
817        $this->loadDeviceData();
818
819        if ($this->loopdetection->ProcessLoopDetectionIsHierarchySyncAdvised()) {
820            return true;
821        }
822
823        // if the hash of the additional folders changed, we have to sync the hierarchy
824        if ($this->additionalFoldersHash != $this->getAdditionalFoldersHash()) {
825            $this->hierarchySyncRequired = true;
826        }
827
828        // check if a hierarchy sync might be necessary
829        if ($this->device->GetFolderUUID(false) === false)
830            $this->hierarchySyncRequired = true;
831
832        return $this->hierarchySyncRequired;
833    }
834
835    private function getAdditionalFoldersHash() {
836        return md5(serialize($this->device->GetAdditionalFolders()));
837    }
838
839    /**
840     * Indicates if a full hierarchy resync should be triggered due to loops
841     *
842     * @access public
843     * @return boolean
844     */
845    public function IsHierarchyFullResyncRequired() {
846        // do not check for loop detection, if the foldersync is not yet complete
847        if ($this->GetFolderSyncComplete() === false) {
848            ZLog::Write(LOGLEVEL_INFO, "DeviceManager->IsHierarchyFullResyncRequired(): aborted, as exporting of folders has not yet completed");
849            return false;
850        }
851        // check for potential process loops like described in ZP-5
852        return $this->loopdetection->ProcessLoopDetectionIsHierarchyResyncRequired();
853    }
854
855    /**
856     * Indicates if an Outlook via ActiveSync is connected.
857     *
858     * @access public
859     * @return boolean
860     */
861    public function IsKoe() {
862        if (Request::IsOutlook() && ($this->device->GetKoeVersion() !== false || Request::HasKoeStats())) {
863            return true;
864        }
865        return false;
866    }
867
868    /**
869     * Indicates if the KOE client supports a feature.
870     *
871     * @param string $feature
872     *
873     * @access public
874     * @return boolean
875     */
876    public function HasKoeFeature($feature) {
877        $capabilities = $this->device->GetKoeCapabilities();
878        // in a settings request the capabilities might not yet be stored in the device
879        if (empty($capabilities)) {
880            $capabilities = Request::GetKoeCapabilities();
881        }
882        return in_array($feature, $capabilities);
883    }
884
885    /**
886     * Indicates if the connected device is Outlook + KOE and supports the
887     * secondary contact folder synchronization.
888     *
889     *  @access public
890     *  @return boolean
891     */
892    public function IsKoeSupportingSecondaryContacts() {
893        return defined('KOE_CAPABILITY_SECONDARYCONTACTS') && KOE_CAPABILITY_SECONDARYCONTACTS && $this->IsKoe() && $this->HasKoeFeature('secondarycontacts');
894    }
895
896    /**
897     * Adds an Exceptions to the process tracking
898     *
899     * @param Exception     $exception
900     *
901     * @access public
902     * @return boolean
903     */
904    public function AnnounceProcessException($exception) {
905        return $this->loopdetection->ProcessLoopDetectionAddException($exception);
906    }
907
908    /**
909     * Adds a non-ok status for a folderid to the process tracking.
910     * On 'false' a hierarchy status is assumed
911     *
912     * @access public
913     * @return boolean
914     */
915    public function AnnounceProcessStatus($folderid, $status) {
916        return $this->loopdetection->ProcessLoopDetectionAddStatus($folderid, $status);
917    }
918
919    /**
920     * Announces that the current process is a push connection to the process loop
921     * detection and to the Top collector
922     *
923     * @access public
924     * @return boolean
925     */
926    public function AnnounceProcessAsPush() {
927        ZLog::Write(LOGLEVEL_DEBUG, "Announce process as PUSH connection");
928
929        return $this->loopdetection->ProcessLoopDetectionSetAsPush() && ZPush::GetTopCollector()->SetAsPushConnection();
930    }
931
932    /**
933     * Checks if the given counter for a certain uuid+folderid was already exported or modified.
934     * This is called when a heartbeat request found changes to make sure that the same
935     * changes are not exported twice, as during the heartbeat there could have been a normal
936     * sync request.
937     *
938     * @param string $folderid          folder id
939     * @param string $uuid              synkkey
940     * @param string $counter           synckey counter
941     *
942     * @access public
943     * @return boolean                  indicating if an uuid+counter were exported (with changes) before
944     */
945    public function CheckHearbeatStateIntegrity($folderid, $uuid, $counter) {
946        return $this->loopdetection->IsSyncStateObsolete($folderid, $uuid, $counter);
947    }
948
949    /**
950     * Marks a syncstate as obsolete for Heartbeat, as e.g. an import was started using it.
951     *
952     * @param string $folderid          folder id
953     * @param string $uuid              synkkey
954     * @param string $counter           synckey counter
955     *
956     * @access public
957     * @return
958     */
959    public function SetHeartbeatStateIntegrity($folderid, $uuid, $counter) {
960        return $this->loopdetection->SetSyncStateUsage($folderid, $uuid, $counter);
961    }
962
963    /**
964     * Checks the data integrity of the data in the hierarchy cache and the data of the content data (synchronized folders).
965     * If a folder is deleted, the sync states could still be on the server (and being loaded by PING) while
966     * the folder is not being synchronized anymore. See also https://jira.z-hub.io/browse/ZP-1077
967     *
968     * @access public
969     * @return boolean
970     */
971    public function CheckFolderData() {
972        ZLog::Write(LOGLEVEL_DEBUG, "DeviceManager->CheckFolderData() checking integrity of hierarchy cache with synchronized folders");
973
974        $hc = $this->device->GetHierarchyCache();
975        $notInCache = array();
976        foreach ($this->device->GetAllFolderIds() as $folderid) {
977            $uuid = $this->device->GetFolderUUID($folderid);
978            if ($uuid) {
979                // has a UUID but is not in the cache?! This is deleted, remove the states.
980                if (! $hc->GetFolder($folderid)) {
981                    ZLog::Write(LOGLEVEL_WARN, sprintf("DeviceManager->CheckFolderData(): Folder '%s' has sync states but is not in the hierarchy cache. Removing states.", $folderid));
982                    StateManager::UnLinkState($this->device, $folderid);
983                }
984            }
985        }
986        return true;
987    }
988
989    /**
990     * Sets the current status of the folder
991     *
992     * @param string     $folderid          folder id
993     * @param int        $statusflag        current status: DeviceManager::FLD_SYNC_INITIALIZED, DeviceManager::FLD_SYNC_INPROGRESS, DeviceManager::FLD_SYNC_COMPLETED
994     *
995     * @access public
996     * @return
997     */
998    public function SetFolderSyncStatus($folderid, $statusflag) {
999        $currentStatus = $this->device->GetFolderSyncStatus($folderid);
1000
1001        // status available or just initialized
1002        if (isset($currentStatus[ASDevice::FOLDERSYNCSTATUS]) || $statusflag == self::FLD_SYNC_INITIALIZED) {
1003            // only update if there is a change
1004	        if ((!$currentStatus || $statusflag !== $currentStatus[ASDevice::FOLDERSYNCSTATUS]) && $statusflag != self::FLD_SYNC_COMPLETED) {
1005                $this->device->SetFolderSyncStatus($folderid, array(ASDevice::FOLDERSYNCSTATUS => $statusflag));
1006                ZLog::Write(LOGLEVEL_DEBUG, sprintf("SetFolderSyncStatus(): set %s for %s", $statusflag, $folderid));
1007            }
1008            // if completed, remove the status
1009            else if ($statusflag == self::FLD_SYNC_COMPLETED) {
1010                $this->device->SetFolderSyncStatus($folderid, false);
1011                ZLog::Write(LOGLEVEL_DEBUG, sprintf("SetFolderSyncStatus(): completed for %s", $folderid));
1012            }
1013        }
1014
1015        return true;
1016    }
1017
1018    /**
1019     * Indicates if a folder is synchronizing by the saved status.
1020     *
1021     * @param string     $folderid          folder id
1022     *
1023     * @access public
1024     * @return boolean
1025     */
1026    public function HasFolderSyncStatus($folderid) {
1027        $currentStatus = $this->device->GetFolderSyncStatus($folderid);
1028
1029        // status available ?
1030        $hasStatus = isset($currentStatus[ASDevice::FOLDERSYNCSTATUS]);
1031        if ($hasStatus) {
1032            ZLog::Write(LOGLEVEL_DEBUG, sprintf("HasFolderSyncStatus(): saved folder status for %s: %s", $folderid, $currentStatus[ASDevice::FOLDERSYNCSTATUS]));
1033        }
1034
1035        return $hasStatus;
1036    }
1037
1038    /**
1039     * Returns the indicator if the FolderSync was completed successfully  (all folders synchronized)
1040     *
1041     * @access public
1042     * @return boolean
1043     */
1044    public function GetFolderSyncComplete() {
1045        return $this->device->GetFolderSyncComplete();
1046    }
1047
1048    /**
1049     * Sets if the FolderSync was completed successfully (all folders synchronized)
1050     *
1051     * @param boolean   $complete   indicating if all folders were sent
1052     *
1053     * @access public
1054     * @return boolean
1055     */
1056    public function SetFolderSyncComplete($complete, $user = false, $devid = false) {
1057        $this->device->SetFolderSyncComplete($complete);
1058    }
1059
1060    /**
1061     * Removes the Loop detection data for a user & device
1062     *
1063     * @param string    $user
1064     * @param string    $devid
1065     *
1066     * @access public
1067     * @return boolean
1068     */
1069    public function ClearLoopDetectionData($user, $devid) {
1070        if ($user == false || $devid == false) {
1071            return false;
1072        }
1073        ZLog::Write(LOGLEVEL_DEBUG, sprintf("DeviceManager->ClearLoopDetectionData(): clearing data for user '%s' and device '%s'", $user, $devid));
1074        return $this->loopdetection->ClearData($user, $devid);
1075    }
1076
1077    /**
1078     * Indicates if the device needs an AS version update
1079     *
1080     * @access public
1081     * @return boolean
1082     */
1083    public function AnnounceASVersion() {
1084        $latest = ZPush::GetSupportedASVersion();
1085        $announced = $this->device->GetAnnouncedASversion();
1086        $this->device->SetAnnouncedASversion($latest);
1087
1088        return ($announced != $latest);
1089    }
1090
1091    /**
1092     * Returns the User Agent. This data is consolidated with data from Request::GetUserAgent()
1093     * and the data saved in the ASDevice.
1094     *
1095     * @access public
1096     * @return string
1097     */
1098    public function GetUserAgent() {
1099        return $this->device->GetDeviceUserAgent();
1100    }
1101
1102    /**
1103     * Returns the backend folder id from the AS folderid known to the mobile.
1104     * If the id is not known, it's returned as is.
1105     *
1106     * @param mixed     $folderid
1107     *
1108     * @access public
1109     * @return int/boolean  returns false if the type is not set
1110     */
1111    public function GetBackendIdForFolderId($folderid) {
1112        $backendId = $this->device->GetFolderBackendId($folderid);
1113        if (!$backendId) {
1114            ZLog::Write(LOGLEVEL_DEBUG, sprintf("DeviceManager->GetBackendIdForFolderId(): no backend-folderid available for '%s', returning as is.", $folderid));
1115            return $folderid;
1116        }
1117        ZLog::Write(LOGLEVEL_DEBUG, sprintf("DeviceManager->GetBackendIdForFolderId(): folderid %s => %s", $folderid, $backendId));
1118        return $backendId;
1119    }
1120
1121    /**
1122     * Gets the AS folderid for a backendFolderId.
1123     * If there is no known AS folderId a new one is being created.
1124     *
1125     * @param string    $backendid              Backend folder id
1126     * @param boolean   $generateNewIdIfNew     Generates a new AS folderid for the case the backend folder is not known yet, default: false.
1127     * @param string    $folderOrigin           Folder type is one of   'U' (user)
1128     *                                                                  'C' (configured)
1129     *                                                                  'S' (shared)
1130     *                                                                  'G' (global address book)
1131     *                                                                  'I' (impersonated)
1132     * @param string    $folderName             Folder name of the backend folder
1133     *
1134     * @access public
1135     * @return string/boolean  returns false if there is folderid known for this backendid and $generateNewIdIfNew is not set or false.
1136     */
1137    public function GetFolderIdForBackendId($backendid, $generateNewIdIfNew = false, $folderOrigin = self::FLD_ORIGIN_USER, $folderName = null) {
1138        if (!in_array($folderOrigin, array(DeviceManager::FLD_ORIGIN_CONFIG, DeviceManager::FLD_ORIGIN_GAB, DeviceManager::FLD_ORIGIN_SHARED, DeviceManager::FLD_ORIGIN_USER, DeviceManager::FLD_ORIGIN_IMPERSONATED))) {
1139            ZLog::Write(LOGLEVEL_WARN, sprintf("ASDevice->GetFolderIdForBackendId(): folder type '%' is unknown in DeviceManager", $folderOrigin));
1140        }
1141        return $this->device->GetFolderIdForBackendId($backendid, $generateNewIdIfNew, $folderOrigin, $folderName);
1142    }
1143
1144
1145    /**----------------------------------------------------------------------------------------------------------
1146     * private DeviceManager methods
1147     */
1148
1149    /**
1150     * Loads devicedata from the StateMachine and loads it into the device
1151     *
1152     * @access public
1153     * @return boolean
1154     */
1155    private function loadDeviceData() {
1156        if (!Request::IsValidDeviceID())
1157            return false;
1158        try {
1159            $deviceHash = $this->statemachine->GetStateHash($this->devid, IStateMachine::DEVICEDATA);
1160            if ($deviceHash != $this->deviceHash) {
1161                if ($this->deviceHash)
1162                    ZLog::Write(LOGLEVEL_DEBUG, "DeviceManager->loadDeviceData(): Device data was changed, reloading");
1163                $this->device->SetData($this->statemachine->GetState($this->devid, IStateMachine::DEVICEDATA));
1164                $this->deviceHash = $deviceHash;
1165            }
1166        }
1167        catch (StateNotFoundException $snfex) {
1168            $this->hierarchySyncRequired = true;
1169        }
1170        catch (UnavailableException $uaex) {
1171            // This is temporary and can be ignored e.g. in PING - see https://jira.z-hub.io/browse/ZP-1054
1172            // If the hash was not available before we treat it like a StateNotFoundException.
1173            if ($this->deviceHash === false) {
1174                $this->hierarchySyncRequired = true;
1175            }
1176        }
1177        return true;
1178    }
1179
1180    /**
1181     * Called when a SyncObject is not being streamed to the mobile.
1182     * The user can be informed so he knows about this issue
1183     *
1184     * @param string        $folderid   id of the parent folder (may be false if unknown)
1185     * @param string        $id         message id
1186     * @param SyncObject    $message    the broken message
1187     * @param string        $reason     (self::MSG_BROKEN_UNKNOWN, self::MSG_BROKEN_CAUSINGLOOP, self::MSG_BROKEN_SEMANTICERR)
1188     *
1189     * @access public
1190     * @return boolean
1191     */
1192    public function AnnounceIgnoredMessage($folderid, $id, SyncObject $message, $reason = self::MSG_BROKEN_UNKNOWN) {
1193        if ($folderid === false)
1194            $folderid = $this->getLatestFolder();
1195
1196        $class = get_class($message);
1197
1198        $brokenMessage = new StateObject();
1199        $brokenMessage->id = $id;
1200        $brokenMessage->folderid = $folderid;
1201        $brokenMessage->ASClass = $class;
1202        $brokenMessage->folderid = $folderid;
1203        $brokenMessage->reasonCode = $reason;
1204        $brokenMessage->reasonString = 'unknown cause';
1205        $brokenMessage->timestamp = time();
1206        $brokenMessage->asobject = $message;
1207        $brokenMessage->reasonString = ZLog::GetLastMessage(LOGLEVEL_WARN);
1208
1209        $this->device->AddIgnoredMessage($brokenMessage);
1210
1211        ZLog::Write(LOGLEVEL_ERROR, sprintf("Ignored broken message (%s). Reason: '%s' Folderid: '%s' message id '%s'", $class, $reason, $folderid, $id));
1212        return true;
1213    }
1214
1215    /**
1216     * Called when a SyncObject was streamed to the mobile.
1217     * If the message could not be sent before this data is obsolete
1218     *
1219     * @param string        $folderid   id of the parent folder
1220     * @param string        $id         message id
1221     *
1222     * @access public
1223     * @return boolean          returns true if the message was ignored before
1224     */
1225    private function announceAcceptedMessage($folderid, $id) {
1226        if ($this->device->RemoveIgnoredMessage($folderid, $id)) {
1227            ZLog::Write(LOGLEVEL_INFO, sprintf("DeviceManager->announceAcceptedMessage('%s', '%s'): cleared previously ignored message as message is sucessfully streamed",$folderid, $id));
1228            return true;
1229        }
1230        return false;
1231    }
1232
1233    /**
1234     * Checks if there were broken messages streamed to the mobile.
1235     * If the sync completes/continues without further erros they are marked as accepted
1236     *
1237     * @param string    $folderid       folderid which is to be checked
1238     *
1239     * @access private
1240     * @return boolean
1241     */
1242    private function checkBrokenMessages($folderid) {
1243        // check for correctly synchronized messages of the folder
1244        foreach($this->loopdetection->GetSyncedButBeforeIgnoredMessages($folderid) as $okID) {
1245            $this->announceAcceptedMessage($folderid, $okID);
1246        }
1247        return true;
1248    }
1249
1250    /**
1251     * Setter for the latest folder id
1252     * on multi-folder operations of AS 14 this is used to set the new current folder id
1253     *
1254     * @param string    $folderid       the current folder
1255     *
1256     * @access private
1257     * @return boolean
1258     */
1259    private function setLatestFolder($folderid) {
1260        // this is a multi folder operation
1261        // check on ignoredmessages before discaring the folderid
1262        if ($this->latestFolder !== false)
1263            $this->checkBrokenMessages($this->latestFolder);
1264
1265        $this->latestFolder = $folderid;
1266
1267        return true;
1268    }
1269
1270    /**
1271     * Getter for the latest folder id
1272     *
1273     * @access private
1274     * @return string    $folderid       the current folder
1275     */
1276    private function getLatestFolder() {
1277        return $this->latestFolder;
1278    }
1279
1280    /**
1281     * Loads Provisioning policies from the policies file.
1282     *
1283     * @param string    $policyName     The name of the policy
1284     *
1285     * @access private
1286     * @return array
1287     */
1288    private function getProvisioningPolicies($policyName) {
1289        $policies = ZPush::GetPolicies();
1290
1291        if (!isset($policies[$policyName]) && $policyName != ASDevice::DEFAULTPOLICYNAME) {
1292            ZLog::Write(LOGLEVEL_WARN, sprintf("The '%s' policy is configured, but it is not available in the policies' file. Please check %s file. Loading default policy.", $policyName, PROVISIONING_POLICYFILE));
1293            return $policies[ASDevice::DEFAULTPOLICYNAME];
1294        }
1295        ZLog::Write(LOGLEVEL_DEBUG, sprintf("DeviceManager->getProvisioningPolicies(): loaded '%s' policy.", $policyName));
1296
1297        // Always load default policies, so that if a policy extends a default policy it doesn't have to copy all the values
1298        if ($policyName != ASDevice::DEFAULTPOLICYNAME) {
1299            $policies[$policyName] = array_replace_recursive($policies[ASDevice::DEFAULTPOLICYNAME], $policies[$policyName]);
1300        }
1301        return $policies[$policyName];
1302    }
1303
1304    /**
1305     * Gets the policy name set in the backend or in device data.
1306     *
1307     * @access private
1308     * @return string
1309     */
1310    private function getPolicyName() {
1311        $policyName = ZPush::GetBackend()->GetUserPolicyName();
1312        $policyName = ((!empty($policyName) && $policyName !== false) ? $policyName : ASDevice::DEFAULTPOLICYNAME);
1313        ZLog::Write(LOGLEVEL_DEBUG, sprintf("DeviceManager->getPolicyName(): determined policy name: '%s'", $policyName));
1314        return $policyName;
1315    }
1316
1317    /**
1318     * Generates and SyncFolder object and returns it.
1319     *
1320     * @param string    $store
1321     * @param string    $folderid
1322     * @param string    $name
1323     * @param int       $type
1324     * @param int       $flags
1325     * @param string    $folderOrigin
1326     *
1327     * @access public
1328     * @returns SyncFolder
1329     */
1330    public function BuildSyncFolderObject($store, $folderid, $parentid, $name, $type, $flags, $folderOrigin) {
1331        $folder = new SyncFolder();
1332        $folder->BackendId = $folderid;
1333        $folder->serverid = $this->GetFolderIdForBackendId($folder->BackendId, true, $folderOrigin, $name);
1334        $folder->parentid = $this->GetFolderIdForBackendId($parentid);
1335        $folder->displayname = $name;
1336        $folder->type = $type;
1337        // save store as custom property which is not streamed directly to the device
1338        $folder->NoBackendFolder = true;
1339        $folder->Store = $store;
1340        $folder->Flags = $flags;
1341
1342        // adjust additional folders so it matches not yet processed KOE type UNKNOWN folders
1343        $synctype = $this->device->GetFolderType($folder->serverid);
1344        if ($this->IsKoeSupportingSecondaryContacts() && $synctype !== $folder->type && $synctype == SYNC_FOLDER_TYPE_UNKNOWN) {
1345            ZLog::Write(LOGLEVEL_DEBUG, "DeviceManager->BuildSyncFolderObject(): Modifying additional folder so it matches an unprocessed KOE folder");
1346            $folder = Utils::ChangeFolderToTypeUnknownForKoe($folder);
1347        }
1348        return $folder;
1349    }
1350
1351    /**
1352     * Returns the device id.
1353     *
1354     * @access public
1355     * @return string
1356     */
1357    public function GetDevid() {
1358        return $this->devid;
1359    }
1360}
1361