Back to index

d-push  2.0
devicemanager.php
Go to the documentation of this file.
00001 <?php
00002 /***********************************************
00003 * File      :   devicemanager.php
00004 * Project   :   Z-Push
00005 * Descr     :   Manages device relevant data, provisioning,
00006 *               loop detection and device states.
00007 *               The DeviceManager uses a IStateMachine
00008 *               implementation with IStateMachine::DEVICEDATA
00009 *               to save device relevant data.
00010 *
00011 * Created   :   11.04.2011
00012 *
00013 * Copyright 2007 - 2011 Zarafa Deutschland GmbH
00014 *
00015 * This program is free software: you can redistribute it and/or modify
00016 * it under the terms of the GNU Affero General Public License, version 3,
00017 * as published by the Free Software Foundation with the following additional
00018 * term according to sec. 7:
00019 *
00020 * According to sec. 7 of the GNU Affero General Public License, version 3,
00021 * the terms of the AGPL are supplemented with the following terms:
00022 *
00023 * "Zarafa" is a registered trademark of Zarafa B.V.
00024 * "Z-Push" is a registered trademark of Zarafa Deutschland GmbH
00025 * The licensing of the Program under the AGPL does not imply a trademark license.
00026 * Therefore any rights, title and interest in our trademarks remain entirely with us.
00027 *
00028 * However, if you propagate an unmodified version of the Program you are
00029 * allowed to use the term "Z-Push" to indicate that you distribute the Program.
00030 * Furthermore you may use our trademarks where it is necessary to indicate
00031 * the intended purpose of a product or service provided you use it in accordance
00032 * with honest practices in industrial or commercial matters.
00033 * If you want to propagate modified versions of the Program under the name "Z-Push",
00034 * you may only do so if you have a written permission by Zarafa Deutschland GmbH
00035 * (to acquire a permission please contact Zarafa at trademark@zarafa.com).
00036 *
00037 * This program is distributed in the hope that it will be useful,
00038 * but WITHOUT ANY WARRANTY; without even the implied warranty of
00039 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
00040 * GNU Affero General Public License for more details.
00041 *
00042 * You should have received a copy of the GNU Affero General Public License
00043 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
00044 *
00045 * Consult LICENSE file for details
00046 ************************************************/
00047 
00048 class DeviceManager {
00049     // stream up to 100 messages to the client by default
00050     const DEFAULTWINDOWSIZE = 100;
00051 
00052     // broken message indicators
00053     const MSG_BROKEN_UNKNOWN = 1;
00054     const MSG_BROKEN_CAUSINGLOOP = 2;
00055     const MSG_BROKEN_SEMANTICERR = 4;
00056 
00057     private $device;
00058     private $deviceHash;
00059     private $statemachine;
00060     private $stateManager;
00061     private $incomingData = 0;
00062     private $outgoingData = 0;
00063 
00064     private $windowSize;
00065     private $latestFolder;
00066 
00067     private $loopdetection;
00068     private $hierarchySyncRequired;
00069 
00075     public function DeviceManager() {
00076         $this->statemachine = ZPush::GetStateMachine();
00077         $this->deviceHash = false;
00078         $this->devid = Request::GetDeviceID();
00079         $this->windowSize = array();
00080         $this->latestFolder = false;
00081         $this->hierarchySyncRequired = false;
00082 
00083         // only continue if deviceid is set
00084         if ($this->devid) {
00085             $this->device = new ASDevice($this->devid, Request::GetDeviceType(), Request::GetGETUser(), Request::GetUserAgent());
00086             $this->loadDeviceData();
00087 
00088             ZPush::GetTopCollector()->SetUserAgent($this->device->GetDeviceUserAgent());
00089         }
00090         else
00091             throw new FatalNotImplementedException("Can not proceed without a device id.");
00092 
00093         $this->loopdetection = new LoopDetection();
00094         $this->loopdetection->ProcessLoopDetectionInit();
00095         $this->loopdetection->ProcessLoopDetectionPreviousConnectionFailed();
00096 
00097         $this->stateManager = new StateManager();
00098         $this->stateManager->SetDevice($this->device);
00099     }
00100 
00107     public function GetStateManager() {
00108         return $this->stateManager;
00109     }
00110 
00123     public function SentData($datacounter) {
00124         // TODO save this somewhere
00125         $this->incomingData = Request::GetContentLength();
00126         $this->outgoingData = $datacounter;
00127     }
00128 
00136     public function Save() {
00137         // TODO save other stuff
00138 
00139         // check if previousily ignored messages were synchronized for the current folder
00140         // on multifolder operations of AS14 this is done by setLatestFolder()
00141         if ($this->latestFolder !== false)
00142             $this->checkBrokenMessages($this->latestFolder);
00143 
00144         // update the user agent and AS version on the device
00145         $this->device->SetUserAgent(Request::GetUserAgent());
00146         $this->device->SetASVersion(Request::GetProtocolVersion());
00147 
00148         // data to be saved
00149         $data = $this->device->GetData();
00150         if ($data && Request::IsValidDeviceID()) {
00151             ZLog::Write(LOGLEVEL_DEBUG, "DeviceManager->Save(): Device data changed");
00152 
00153             try {
00154                 // 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
00155                 if ($this->device->IsNewDevice() && RequestProcessor::isUserAuthenticated()) {
00156                     ZLog::Write(LOGLEVEL_INFO, sprintf("Linking device ID '%s' to user '%s'", $this->devid, $this->device->GetDeviceUser()));
00157                     $this->statemachine->LinkUserDevice($this->device->GetDeviceUser(), $this->devid);
00158                 }
00159 
00160                 if (RequestProcessor::isUserAuthenticated() || $this->device->GetForceSave() ) {
00161                     $this->statemachine->SetState($data, $this->devid, IStateMachine::DEVICEDATA);
00162                     ZLog::Write(LOGLEVEL_DEBUG, "DeviceManager->Save(): Device data saved");
00163                 }
00164             }
00165             catch (StateNotFoundException $snfex) {
00166                 ZLog::Write(LOGLEVEL_ERROR, "DeviceManager->Save(): Exception: ". $snfex->getMessage());
00167             }
00168         }
00169 
00170         // remove old search data
00171         $oldpid = $this->loopdetection->ProcessLoopDetectionGetOutdatedSearchPID();
00172         if ($oldpid) {
00173             ZPush::GetBackend()->GetSearchProvider()->TerminateSearch($oldpid);
00174         }
00175 
00176         // we terminated this process
00177         if ($this->loopdetection)
00178             $this->loopdetection->ProcessLoopDetectionTerminate();
00179 
00180         return true;
00181     }
00182 
00192     public function SaveDeviceInformation($deviceinformation) {
00193         ZLog::Write(LOGLEVEL_DEBUG, "Saving submitted device information");
00194 
00195         // set the user agent
00196         if (isset($deviceinformation->useragent))
00197             $this->device->SetUserAgent($deviceinformation->useragent);
00198 
00199         // save other informations
00200         foreach (array("model", "imei", "friendlyname", "os", "oslanguage", "phonenumber", "mobileoperator", "enableoutboundsms") as $info) {
00201             if (isset($deviceinformation->$info) && $deviceinformation->$info != "") {
00202                 $this->device->__set("device".$info, $deviceinformation->$info);
00203             }
00204         }
00205         return true;
00206     }
00207 
00222     public function ProvisioningRequired($policykey, $noDebug = false) {
00223         $this->loadDeviceData();
00224 
00225         // check if a remote wipe is required
00226         if ($this->device->GetWipeStatus() > SYNC_PROVISION_RWSTATUS_OK) {
00227             ZLog::Write(LOGLEVEL_INFO, sprintf("DeviceManager->ProvisioningRequired('%s'): YES, remote wipe requested", $policykey));
00228             return true;
00229         }
00230 
00231         $p = ( ($this->device->GetWipeStatus() != SYNC_PROVISION_RWSTATUS_NA && $policykey != $this->device->GetPolicyKey()) ||
00232               Request::WasPolicyKeySent() && $this->device->GetPolicyKey() == ASDevice::UNDEFINED );
00233         if (!$noDebug || $p)
00234             ZLog::Write(LOGLEVEL_DEBUG, sprintf("DeviceManager->ProvisioningRequired('%s') saved device key '%s': %s", $policykey, $this->device->GetPolicyKey(), Utils::PrintAsString($p)));
00235         return $p;
00236     }
00237 
00244     public function GenerateProvisioningPolicyKey() {
00245         return mt_rand(100000000, 999999999);
00246     }
00247 
00256     public function SetProvisioningPolicyKey($policykey) {
00257         ZLog::Write(LOGLEVEL_DEBUG, sprintf("DeviceManager->SetPolicyKey('%s')", $policykey));
00258         return $this->device->SetPolicyKey($policykey);
00259     }
00260 
00267     public function GetProvisioningObject() {
00268         $p = new SyncProvisioning();
00269         // TODO load systemwide Policies
00270         $p->Load($this->device->GetPolicies());
00271         return $p;
00272     }
00273 
00280     public function GetProvisioningWipeStatus() {
00281         return $this->device->GetWipeStatus();
00282     }
00283 
00292     public function SetProvisioningWipeStatus($status) {
00293         ZLog::Write(LOGLEVEL_DEBUG, sprintf("DeviceManager->SetProvisioningWipeStatus() change from '%d' to '%d'",$this->device->GetWipeStatus(), $status));
00294 
00295         if ($status > SYNC_PROVISION_RWSTATUS_OK && !($this->device->GetWipeStatus() > SYNC_PROVISION_RWSTATUS_OK)) {
00296             ZLog::Write(LOGLEVEL_ERROR, "Not permitted to update remote wipe status to a higher value as remote wipe was not initiated!");
00297             return false;
00298         }
00299         $this->device->SetWipeStatus($status);
00300         return true;
00301     }
00302 
00303 
00316     public function GetHierarchyChangesWrapper() {
00317         return $this->device->GetHierarchyCache();
00318     }
00319 
00330     public function InitializeFolderCache($folders) {
00331         $this->stateManager->SetDevice($this->device);
00332         return $this->stateManager->InitializeFolderCache($folders);
00333     }
00334 
00346     public function GetFolderIdFromCacheByClass($class) {
00347         $folderidforClass = false;
00348         // look at the default foldertype for this class
00349         $type = ZPush::getDefaultFolderTypeFromFolderClass($class);
00350 
00351         if ($type && $type > SYNC_FOLDER_TYPE_OTHER && $type < SYNC_FOLDER_TYPE_USER_MAIL) {
00352             $folderids = $this->device->GetAllFolderIds();
00353             foreach ($folderids as $folderid) {
00354                 if ($type == $this->device->GetFolderType($folderid)) {
00355                     $folderidforClass = $folderid;
00356                     break;
00357                 }
00358             }
00359 
00360             // Old Palm Treos always do initial sync for calendar and contacts, even if they are not made available by the backend.
00361             // We need to fake these folderids, allowing a fake sync/ping, even if they are not supported by the backend
00362             // if the folderid would be available, they would already be returned in the above statement
00363             if ($folderidforClass == false && ($type == SYNC_FOLDER_TYPE_APPOINTMENT || $type == SYNC_FOLDER_TYPE_CONTACT))
00364                 $folderidforClass = SYNC_FOLDER_TYPE_DUMMY;
00365         }
00366 
00367         ZLog::Write(LOGLEVEL_DEBUG, sprintf("DeviceManager->GetFolderIdFromCacheByClass('%s'): '%s' => '%s'", $class, $type, $folderidforClass));
00368         return $folderidforClass;
00369     }
00370 
00380     public function GetFolderClassFromCacheByID($folderid) {
00381         //TODO check if the parent folder exists and is also beeing synchronized
00382         $typeFromCache = $this->device->GetFolderType($folderid);
00383         if ($typeFromCache === false)
00384             throw new NoHierarchyCacheAvailableException(sprintf("Folderid '%s' is not fully synchronized on the device", $folderid));
00385 
00386         $class = ZPush::GetFolderClassFromFolderType($typeFromCache);
00387         if ($class === false)
00388             throw new NotImplementedException(sprintf("Folderid '%s' is saved to be of type '%d' but this type is not implemented", $folderid, $typeFromCache));
00389 
00390         return $class;
00391     }
00392 
00405     public function DoNotStreamMessage($id, &$message) {
00406         $folderid = $this->getLatestFolder();
00407 
00408         if (isset($message->parentid))
00409             $folder = $message->parentid;
00410 
00411         // message was identified to be causing a loop
00412         if ($this->loopdetection->IgnoreNextMessage(true, $id, $folderid)) {
00413             $this->AnnounceIgnoredMessage($folderid, $id, $message, self::MSG_BROKEN_CAUSINGLOOP);
00414             return true;
00415         }
00416 
00417         // message is semantically incorrect
00418         if (!$message->Check(true)) {
00419             $this->AnnounceIgnoredMessage($folderid, $id, $message, self::MSG_BROKEN_SEMANTICERR);
00420             return true;
00421         }
00422 
00423         // check if this message is broken
00424         if ($this->device->HasIgnoredMessage($folderid, $id)) {
00425             // reset the flags so the message is always streamed with <Add>
00426             $message->flags = false;
00427 
00428             // track the broken message in the loop detection
00429             $this->loopdetection->SetBrokenMessage($folderid, $id);
00430         }
00431         return false;
00432     }
00433 
00443     public function GetWindowSize($folderid, $type, $uuid, $statecounter, $queuedmessages) {
00444         if (isset($this->windowSize[$folderid]))
00445             $items = $this->windowSize[$folderid];
00446         else
00447             $items = self::DEFAULTWINDOWSIZE;
00448 
00449         $this->setLatestFolder($folderid);
00450 
00451         // detect if this is a loop condition
00452         if ($this->loopdetection->Detect($folderid, $type, $uuid, $statecounter, $items, $queuedmessages))
00453             $items = ($items == 0) ? 0: 1+($this->loopdetection->IgnoreNextMessage(false)?1:0) ;
00454 
00455         if ($items >= 0 && $items <= 2)
00456             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));
00457 
00458         return $items;
00459     }
00460 
00470     public function SetWindowSize($folderid, $maxItems) {
00471         $this->windowSize[$folderid] = $maxItems;
00472 
00473         return true;
00474     }
00475 
00485     public function SetSupportedFields($folderid, $fieldlist) {
00486         return $this->device->SetSupportedFields($folderid, $fieldlist);
00487     }
00488 
00498     public function GetSupportedFields($folderid) {
00499         return $this->device->GetSupportedFields($folderid);
00500     }
00501 
00511     public function ForceFolderResync($folderid) {
00512         ZLog::Write(LOGLEVEL_INFO, sprintf("DeviceManager->ForceFolderResync('%s'): folder resync", $folderid));
00513 
00514         // delete folder states
00515         StateManager::UnLinkState($this->device, $folderid);
00516 
00517         return true;
00518     }
00519 
00527     public function ForceFullResync() {
00528         ZLog::Write(LOGLEVEL_INFO, "Full device resync requested");
00529 
00530         // delete hierarchy states
00531         StateManager::UnLinkState($this->device, false);
00532 
00533         // delete all other uuids
00534         foreach ($this->device->GetAllFolderIds() as $folderid)
00535             $uuid = StateManager::UnLinkState($this->device, $folderid);
00536 
00537         return true;
00538     }
00539 
00547     public function IsHierarchySyncRequired() {
00548         // check if a hierarchy sync might be necessary
00549         if ($this->device->GetFolderUUID(false) === false)
00550             $this->hierarchySyncRequired = true;
00551 
00552         return $this->hierarchySyncRequired;
00553     }
00554 
00561     public function IsHierarchyFullResyncRequired() {
00562         // check for potential process loops like described in ZP-5
00563         return $this->loopdetection->ProcessLoopDetectionIsHierarchyResyncRequired();
00564     }
00565 
00574     public function AnnounceProcessException($exception) {
00575         return $this->loopdetection->ProcessLoopDetectionAddException($exception);
00576     }
00577 
00585     public function AnnounceProcessStatus($folderid, $status) {
00586         return $this->loopdetection->ProcessLoopDetectionAddStatus($folderid, $status);
00587     }
00588 
00602     public function CheckHearbeatStateIntegrity($folderid, $uuid, $counter) {
00603         return $this->loopdetection->IsSyncStateObsolete($folderid, $uuid, $counter);
00604     }
00605 
00612     public function AnnounceASVersion() {
00613         $latest = ZPush::GetSupportedASVersion();
00614         $announced = $this->device->GetAnnouncedASversion();
00615         $this->device->SetAnnouncedASversion($latest);
00616 
00617         return ($announced != $latest);
00618     }
00619 
00630     private function loadDeviceData() {
00631         if (!Request::IsValidDeviceID())
00632             return false;
00633         try {
00634             $deviceHash = $this->statemachine->GetStateHash($this->devid, IStateMachine::DEVICEDATA);
00635             if ($deviceHash != $this->deviceHash) {
00636                 if ($this->deviceHash)
00637                     ZLog::Write(LOGLEVEL_DEBUG, "DeviceManager->loadDeviceData(): Device data was changed, reloading");
00638                 $this->device->SetData($this->statemachine->GetState($this->devid, IStateMachine::DEVICEDATA));
00639                 $this->deviceHash = $deviceHash;
00640             }
00641         }
00642         catch (StateNotFoundException $snfex) {
00643             $this->hierarchySyncRequired = true;
00644         }
00645         return true;
00646     }
00647 
00660     public function AnnounceIgnoredMessage($folderid, $id, SyncObject $message, $reason = self::MSG_BROKEN_UNKNOWN) {
00661         if ($folderid === false)
00662             $folderid = $this->getLatestFolder();
00663 
00664         $class = get_class($message);
00665 
00666         $brokenMessage = new StateObject();
00667         $brokenMessage->id = $id;
00668         $brokenMessage->folderid = $folderid;
00669         $brokenMessage->ASClass = $class;
00670         $brokenMessage->folderid = $folderid;
00671         $brokenMessage->reasonCode = $reason;
00672         $brokenMessage->reasonString = 'unknown cause';
00673         $brokenMessage->timestamp = time();
00674         $brokenMessage->asobject = $message;
00675         $brokenMessage->reasonString = ZLog::GetLastMessage(LOGLEVEL_WARN);
00676 
00677         $this->device->AddIgnoredMessage($brokenMessage);
00678 
00679         ZLog::Write(LOGLEVEL_ERROR, sprintf("Ignored broken message (%s). Reason: '%s' Folderid: '%s' message id '%s'", $class, $reason, $folderid, $id));
00680         return true;
00681     }
00682 
00693     private function announceAcceptedMessage($folderid, $id) {
00694         if ($this->device->RemoveIgnoredMessage($folderid, $id)) {
00695             ZLog::Write(LOGLEVEL_INFO, sprintf("DeviceManager->announceAcceptedMessage('%s', '%s'): cleared previosily ignored message as message is sucessfully streamed",$folderid, $id));
00696             return true;
00697         }
00698         return false;
00699     }
00700 
00710     private function checkBrokenMessages($folderid) {
00711         // check for correctly synchronized messages of the folder
00712         foreach($this->loopdetection->GetSyncedButBeforeIgnoredMessages($folderid) as $okID) {
00713             $this->announceAcceptedMessage($folderid, $okID);
00714         }
00715         return true;
00716     }
00717 
00727     private function setLatestFolder($folderid) {
00728         // this is a multi folder operation
00729         // check on ignoredmessages before discaring the folderid
00730         if ($this->latestFolder !== false)
00731             $this->checkBrokenMessages($this->latestFolder);
00732 
00733         $this->latestFolder = $folderid;
00734 
00735         return true;
00736     }
00737 
00744     private function getLatestFolder() {
00745         return $this->latestFolder;
00746     }
00747 }
00748 
00749 ?>