/** * Handles a webservice command * * @param int $commandCode * * @access public * @return boolean * @throws SoapFault */ public function Handle($commandCode) { if (Request::GetDeviceType() !== "webservice" || Request::GetDeviceID() !== "webservice") { throw new FatalException("Invalid device id and type for webservice execution"); } if (Request::GetGETUser() != Request::GetAuthUser()) { ZLog::Write(LOGLEVEL_INFO, sprintf("Webservice::HandleWebservice('%s'): user '%s' executing action for user '%s'", $commandCode, Request::GetAuthUser(), Request::GetGETUser())); } // initialize non-wsdl soap server $this->server = new SoapServer(null, array('uri' => "http://z-push.sf.net/webservice")); // the webservice command is handled by its class if ($commandCode == ZPush::COMMAND_WEBSERVICE_DEVICE) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("Webservice::HandleWebservice('%s'): executing WebserviceDevice service", $commandCode)); include_once 'webservicedevice.php'; $this->server->setClass("WebserviceDevice"); } // the webservice command is handled by its class if ($commandCode == ZPush::COMMAND_WEBSERVICE_USERS) { if (!defined("ALLOW_WEBSERVICE_USERS_ACCESS") || ALLOW_WEBSERVICE_USERS_ACCESS !== true) { throw new HTTPReturnCodeException(sprintf("Access to the WebserviceUsers service is disabled in configuration. Enable setting ALLOW_WEBSERVICE_USERS_ACCESS.", Request::GetAuthUser()), 403); } ZLog::Write(LOGLEVEL_DEBUG, sprintf("Webservice::HandleWebservice('%s'): executing WebserviceUsers service", $commandCode)); if (ZPush::GetBackend()->Setup("SYSTEM", true) == false) { throw new AuthenticationRequiredException(sprintf("User '%s' has no admin privileges", Request::GetAuthUser())); } include_once 'webserviceusers.php'; $this->server->setClass("WebserviceUsers"); } $this->server->handle(); ZLog::Write(LOGLEVEL_DEBUG, sprintf("Webservice::HandleWebservice('%s'): sucessfully sent %d bytes", $commandCode, ob_get_length())); return true; }
/** * Returns a list of folders of the Request::GetGETUser(). * If the user has not enough permissions an empty result is returned. * * @access public * @return array */ public function ListUserFolders() { $user = Request::GetGETUser(); $output = array(); $hasRights = ZPush::GetBackend()->Setup($user); ZLog::Write(LOGLEVEL_INFO, sprintf("WebserviceInfo::ListUserFolders(): permissions to open store '%s': %s", $user, Utils::PrintAsString($hasRights))); if ($hasRights) { $folders = ZPush::GetBackend()->GetHierarchy(); ZPush::GetTopCollector()->AnnounceInformation(sprintf("Retrieved details of %d folders", count($folders)), true); foreach ($folders as $folder) { $folder->StripData(); unset($folder->Store, $folder->flags, $folder->content, $folder->NoBackendFolder); $output[] = $folder; } } return $output; }
/** * Only used to load additional folder sync information for hierarchy changes * * @param array $state current state of additional hierarchy folders * * @access public * @return boolean */ public function Config($state, $flags = 0) { // we should never forward this changes to a backend if (!isset($this->destinationImporter)) { foreach ($state as $addKey => $addFolder) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("ChangesMemoryWrapper->Config(AdditionalFolders) : process folder '%s'", $addFolder->displayname)); if (isset($addFolder->NoBackendFolder) && $addFolder->NoBackendFolder == true) { // check rights for readonly access only $hasRights = ZPush::GetBackend()->Setup($addFolder->Store, true, $addFolder->BackendId, true); // delete the folder on the device if (!$hasRights) { // delete the folder only if it was an additional folder before, else ignore it $synchedfolder = $this->GetFolder($addFolder->serverid); if (isset($synchedfolder->NoBackendFolder) && $synchedfolder->NoBackendFolder == true) { $this->ImportFolderDeletion($addFolder); } continue; } } // add folder to the device - if folder is already on the device, nothing will happen $this->ImportFolderChange($addFolder); } // look for folders which are currently on the device if there are now not to be synched anymore $alreadyDeleted = $this->GetDeletedFolders(); foreach ($this->ExportFolders(true) as $sid => $folder) { // we are only looking at additional folders if (isset($folder->NoBackendFolder)) { // look if this folder is still in the list of additional folders and was not already deleted (e.g. missing permissions) if (!array_key_exists($sid, $state) && !array_key_exists($sid, $alreadyDeleted)) { ZLog::Write(LOGLEVEL_INFO, sprintf("ChangesMemoryWrapper->Config(AdditionalFolders) : previously synchronized folder '%s' is not to be synched anymore. Sending delete to mobile.", $folder->displayname)); $this->ImportFolderDeletion($folder); } } } } return true; }
/** * Checks the hierarchy for changes. * * @param boolean export changes, default: false * * @access private * @return boolean indicating if changes were found or not */ private function countHierarchyChange($exportChanges = false) { $folderid = false; $spa = $this->GetCollection($folderid); // Check with device manager if the hierarchy should be reloaded. // New additional folders are loaded here. if (ZPush::GetDeviceManager()->IsHierarchySyncRequired()) { ZLog::Write(LOGLEVEL_DEBUG, "SyncCollections->countHierarchyChange(): DeviceManager says HierarchySync is required."); return true; } $changecount = false; if ($exportChanges || $this->hierarchyExporterChecked === false) { try { // if this is a validation (not first run), make sure to load the hierarchy data again if ($this->hierarchyExporterChecked === true && !$this->LoadCollection(false, true, false)) { throw new StatusException("Invalid states found while re-loading hierarchy data."); } $changesMem = ZPush::GetDeviceManager()->GetHierarchyChangesWrapper(); // the hierarchyCache should now fully be initialized - check for changes in the additional folders $changesMem->Config(ZPush::GetAdditionalSyncFolders(false)); // reset backend to the main store ZPush::GetBackend()->Setup(false); $exporter = ZPush::GetBackend()->GetExporter(); if ($exporter !== false && isset($this->addparms[$folderid]["state"])) { $exporter->Config($this->addparms[$folderid]["state"]); $ret = $exporter->InitializeExporter($changesMem); while (is_array($exporter->Synchronize())) { } if ($ret !== false) { $changecount = $changesMem->GetChangeCount(); } $this->hierarchyExporterChecked = true; } } catch (StatusException $ste) { throw new StatusException("SyncCollections->countHierarchyChange(): exporter can not be re-configured.", self::ERROR_WRONG_HIERARCHY, null, LOGLEVEL_WARN); } // start over if exporter can not be configured atm if ($changecount === false) { ZLog::Write(LOGLEVEL_WARN, "SyncCollections->countHierarchyChange(): no changes received from Exporter."); } } return $changecount > 0; }
/** * Reads an email object from MAPI * * @param mixed $mapimessage * @param ContentParameters $contentparameters * * @access private * @return SyncEmail */ private function getEmail($mapimessage, $contentparameters) { $message = new SyncMail(); $this->getPropsFromMAPI($message, $mapimessage, MAPIMapping::GetEmailMapping()); $emailproperties = MAPIMapping::GetEmailProperties(); $messageprops = $this->getProps($mapimessage, $emailproperties); if (isset($messageprops[PR_SOURCE_KEY])) { $sourcekey = $messageprops[PR_SOURCE_KEY]; } else { return false; } //set the body according to contentparameters and supported AS version $this->setMessageBody($mapimessage, $contentparameters, $message); $fromname = $fromaddr = ""; if (isset($messageprops[$emailproperties["representingname"]])) { // remove encapsulating double quotes from the representingname $fromname = preg_replace('/^\\"(.*)\\"$/', "\${1}", $messageprops[$emailproperties["representingname"]]); } if (isset($messageprops[$emailproperties["representingentryid"]])) { $fromaddr = $this->getSMTPAddressFromEntryID($messageprops[$emailproperties["representingentryid"]]); } if ($fromname == $fromaddr) { $fromname = ""; } if ($fromname) { $from = "\"" . w2u($fromname) . "\" <" . w2u($fromaddr) . ">"; } else { //START CHANGED dw2412 HTC shows "error" if sender name is unknown $from = "\"" . w2u($fromaddr) . "\" <" . w2u($fromaddr) . ">"; } //END CHANGED dw2412 HTC shows "error" if sender name is unknown $message->from = $from; // process Meeting Requests if (isset($message->messageclass) && strpos($message->messageclass, "IPM.Schedule.Meeting") === 0) { $message->meetingrequest = new SyncMeetingRequest(); $this->getPropsFromMAPI($message->meetingrequest, $mapimessage, MAPIMapping::GetMeetingRequestMapping()); $meetingrequestproperties = MAPIMapping::GetMeetingRequestProperties(); $props = $this->getProps($mapimessage, $meetingrequestproperties); // Get the GOID if (isset($props[$meetingrequestproperties["goidtag"]])) { $message->meetingrequest->globalobjid = base64_encode($props[$meetingrequestproperties["goidtag"]]); } // Set Timezone if (isset($props[$meetingrequestproperties["timezonetag"]])) { $tz = $this->getTZFromMAPIBlob($props[$meetingrequestproperties["timezonetag"]]); } else { $tz = $this->getGMTTZ(); } $message->meetingrequest->timezone = base64_encode(TimezoneUtil::GetSyncBlobFromTZ($tz)); // send basedate if exception if (isset($props[$meetingrequestproperties["recReplTime"]]) || isset($props[$meetingrequestproperties["lidIsException"]]) && $props[$meetingrequestproperties["lidIsException"]] == true) { if (isset($props[$meetingrequestproperties["recReplTime"]])) { $basedate = $props[$meetingrequestproperties["recReplTime"]]; $message->meetingrequest->recurrenceid = $this->getGMTTimeByTZ($basedate, $this->getGMTTZ()); } else { if (!isset($props[$meetingrequestproperties["goidtag"]]) || !isset($props[$meetingrequestproperties["recurStartTime"]]) || !isset($props[$meetingrequestproperties["timezonetag"]])) { ZLog::Write(LOGLEVEL_WARN, "Missing property to set correct basedate for exception"); } else { $basedate = Utils::ExtractBaseDate($props[$meetingrequestproperties["goidtag"]], $props[$meetingrequestproperties["recurStartTime"]]); $message->meetingrequest->recurrenceid = $this->getGMTTimeByTZ($basedate, $tz); } } } // Organizer is the sender if (strpos($message->messageclass, "IPM.Schedule.Meeting.Resp") === 0) { $message->meetingrequest->organizer = $message->to; } else { $message->meetingrequest->organizer = $message->from; } // Process recurrence if (isset($props[$meetingrequestproperties["isrecurringtag"]]) && $props[$meetingrequestproperties["isrecurringtag"]]) { $myrec = new SyncMeetingRequestRecurrence(); // get recurrence -> put $message->meetingrequest as message so the 'alldayevent' is set correctly $this->getRecurrence($mapimessage, $props, $message->meetingrequest, $myrec, $tz); $message->meetingrequest->recurrences = array($myrec); } // Force the 'alldayevent' in the object at all times. (non-existent == 0) if (!isset($message->meetingrequest->alldayevent) || $message->meetingrequest->alldayevent == "") { $message->meetingrequest->alldayevent = 0; } // Instancetype // 0 = single appointment // 1 = master recurring appointment // 2 = single instance of recurring appointment // 3 = exception of recurring appointment $message->meetingrequest->instancetype = 0; if (isset($props[$meetingrequestproperties["isrecurringtag"]]) && $props[$meetingrequestproperties["isrecurringtag"]] == 1) { $message->meetingrequest->instancetype = 1; } else { if ((!isset($props[$meetingrequestproperties["isrecurringtag"]]) || $props[$meetingrequestproperties["isrecurringtag"]] == 0) && isset($message->meetingrequest->recurrenceid)) { if (isset($props[$meetingrequestproperties["appSeqNr"]]) && $props[$meetingrequestproperties["appSeqNr"]] == 0) { $message->meetingrequest->instancetype = 2; } else { $message->meetingrequest->instancetype = 3; } } } // Disable reminder if it is off if (!isset($props[$meetingrequestproperties["reminderset"]]) || $props[$meetingrequestproperties["reminderset"]] == false) { $message->meetingrequest->reminder = ""; } else { ///set the default reminder time to seconds if ($props[$meetingrequestproperties["remindertime"]] == 0x5ae980e1) { $message->meetingrequest->reminder = 900; } else { $message->meetingrequest->reminder = $props[$meetingrequestproperties["remindertime"]] * 60; } } // Set sensitivity to 0 if missing if (!isset($message->meetingrequest->sensitivity)) { $message->meetingrequest->sensitivity = 0; } // If the user is working from a location other than the office the busystatus should be interpreted as free. if (isset($message->meetingrequest->busystatus) && $message->meetingrequest->busystatus == fbWorkingElsewhere) { $message->meetingrequest->busystatus = fbFree; } // If the busystatus has the value of -1, we should be interpreted as tentative (1) / ZP-581 if (isset($message->meetingrequest->busystatus) && $message->meetingrequest->busystatus == -1) { $message->meetingrequest->busystatus = fbTentative; } // if a meeting request response hasn't been processed yet, // do it so that the attendee status is updated on the mobile if (!isset($messageprops[$emailproperties["processed"]])) { // check if we are not sending the MR so we can process it - ZP-581 $cuser = ZPush::GetBackend()->GetUserDetails(ZPush::GetBackend()->GetCurrentUsername()); if (isset($cuser["emailaddress"]) && $cuser["emailaddress"] != $fromaddr) { $req = new Meetingrequest($this->store, $mapimessage, $this->session); if ($req->isMeetingRequestResponse()) { $req->processMeetingRequestResponse(); } if ($req->isMeetingCancellation()) { $req->processMeetingCancellation(); } } } $message->contentclass = DEFAULT_CALENDAR_CONTENTCLASS; } // Add attachments $attachtable = mapi_message_getattachmenttable($mapimessage); $rows = mapi_table_queryallrows($attachtable, array(PR_ATTACH_NUM)); $entryid = bin2hex($messageprops[$emailproperties["entryid"]]); foreach ($rows as $row) { if (isset($row[PR_ATTACH_NUM])) { if (Request::GetProtocolVersion() >= 12.0) { $attach = new SyncBaseAttachment(); } else { $attach = new SyncAttachment(); } $mapiattach = mapi_message_openattach($mapimessage, $row[PR_ATTACH_NUM]); $attachprops = mapi_getprops($mapiattach, array(PR_ATTACH_LONG_FILENAME, PR_ATTACH_FILENAME, PR_ATTACHMENT_HIDDEN, PR_ATTACH_CONTENT_ID, PR_ATTACH_CONTENT_ID_W, PR_ATTACH_MIME_TAG, PR_ATTACH_MIME_TAG_W, PR_ATTACH_METHOD, PR_DISPLAY_NAME, PR_DISPLAY_NAME_W, PR_ATTACH_SIZE)); if (isset($attachprops[PR_ATTACH_MIME_TAG]) && strpos(strtolower($attachprops[PR_ATTACH_MIME_TAG]), 'signed') !== false || isset($attachprops[PR_ATTACH_MIME_TAG_W]) && strpos(strtolower($attachprops[PR_ATTACH_MIME_TAG_W]), 'signed') !== false) { continue; } // the displayname is handled equaly for all AS versions $attach->displayname = w2u(isset($attachprops[PR_ATTACH_LONG_FILENAME]) ? $attachprops[PR_ATTACH_LONG_FILENAME] : (isset($attachprops[PR_ATTACH_FILENAME]) ? $attachprops[PR_ATTACH_FILENAME] : (isset($attachprops[PR_DISPLAY_NAME]) ? $attachprops[PR_DISPLAY_NAME] : "attachment.bin"))); // fix attachment name in case of inline images if ($attach->displayname == "inline.txt" && (isset($attachprops[PR_ATTACH_MIME_TAG]) || $attachprops[PR_ATTACH_MIME_TAG_W])) { $mimetype = isset($attachprops[PR_ATTACH_MIME_TAG]) ? $attachprops[PR_ATTACH_MIME_TAG] : $attachprops[PR_ATTACH_MIME_TAG_W]; $mime = explode("/", $mimetype); if (count($mime) == 2 && $mime[0] == "image") { $attach->displayname = "inline." . $mime[1]; } } // set AS version specific parameters if (Request::GetProtocolVersion() >= 12.0) { $attach->filereference = $entryid . ":" . $row[PR_ATTACH_NUM]; $attach->method = isset($attachprops[PR_ATTACH_METHOD]) ? $attachprops[PR_ATTACH_METHOD] : ATTACH_BY_VALUE; // if displayname does not have the eml extension for embedde messages, android and WP devices won't open it if ($attach->method == ATTACH_EMBEDDED_MSG) { if (strtolower(substr($attach->displayname, -4)) != '.eml') { $attach->displayname .= '.eml'; } } $attach->estimatedDataSize = $attachprops[PR_ATTACH_SIZE]; if (isset($attachprops[PR_ATTACH_CONTENT_ID]) && $attachprops[PR_ATTACH_CONTENT_ID]) { $attach->contentid = $attachprops[PR_ATTACH_CONTENT_ID]; } if (!isset($attach->contentid) && isset($attachprops[PR_ATTACH_CONTENT_ID_W]) && $attachprops[PR_ATTACH_CONTENT_ID_W]) { $attach->contentid = $attachprops[PR_ATTACH_CONTENT_ID_W]; } if (isset($attachprops[PR_ATTACHMENT_HIDDEN]) && $attachprops[PR_ATTACHMENT_HIDDEN]) { $attach->isinline = 1; } if (!isset($message->asattachments)) { $message->asattachments = array(); } array_push($message->asattachments, $attach); } else { $attach->attsize = $attachprops[PR_ATTACH_SIZE]; $attach->attname = $entryid . ":" . $row[PR_ATTACH_NUM]; if (!isset($message->attachments)) { $message->attachments = array(); } array_push($message->attachments, $attach); } } } // Get To/Cc as SMTP addresses (this is different from displayto and displaycc because we are putting // in the SMTP addresses as well, while displayto and displaycc could just contain the display names $message->to = array(); $message->cc = array(); $reciptable = mapi_message_getrecipienttable($mapimessage); $rows = mapi_table_queryallrows($reciptable, array(PR_RECIPIENT_TYPE, PR_DISPLAY_NAME, PR_ADDRTYPE, PR_EMAIL_ADDRESS, PR_SMTP_ADDRESS, PR_ENTRYID)); foreach ($rows as $row) { $address = ""; $fulladdr = ""; $addrtype = isset($row[PR_ADDRTYPE]) ? $row[PR_ADDRTYPE] : ""; if (isset($row[PR_SMTP_ADDRESS])) { $address = $row[PR_SMTP_ADDRESS]; } elseif ($addrtype == "SMTP" && isset($row[PR_EMAIL_ADDRESS])) { $address = $row[PR_EMAIL_ADDRESS]; } elseif ($addrtype == "ZARAFA" && isset($row[PR_ENTRYID])) { $address = $this->getSMTPAddressFromEntryID($row[PR_ENTRYID]); } $name = isset($row[PR_DISPLAY_NAME]) ? $row[PR_DISPLAY_NAME] : ""; if ($name == "" || $name == $address) { $fulladdr = w2u($address); } else { if (substr($name, 0, 1) != '"' && substr($name, -1) != '"') { $fulladdr = "\"" . w2u($name) . "\" <" . w2u($address) . ">"; } else { $fulladdr = w2u($name) . "<" . w2u($address) . ">"; } } if ($row[PR_RECIPIENT_TYPE] == MAPI_TO) { array_push($message->to, $fulladdr); } else { if ($row[PR_RECIPIENT_TYPE] == MAPI_CC) { array_push($message->cc, $fulladdr); } } } if (is_array($message->to) && !empty($message->to)) { $message->to = implode(", ", $message->to); } if (is_array($message->cc) && !empty($message->cc)) { $message->cc = implode(", ", $message->cc); } // without importance some mobiles assume "0" (low) - Mantis #439 if (!isset($message->importance)) { $message->importance = IMPORTANCE_NORMAL; } //TODO contentclass and nativebodytype and internetcpid if (!isset($message->internetcpid)) { $message->internetcpid = defined('STORE_INTERNET_CPID') ? constant('STORE_INTERNET_CPID') : INTERNET_CPID_WINDOWS1252; } $this->setFlag($mapimessage, $message); if (!isset($message->contentclass)) { $message->contentclass = DEFAULT_EMAIL_CONTENTCLASS; } if (!isset($message->nativebodytype)) { $message->nativebodytype = $this->getNativeBodyType($messageprops); } // reply, reply to all, forward flags if (isset($message->lastverbexecuted) && $message->lastverbexecuted) { $message->lastverbexecuted = Utils::GetLastVerbExecuted($message->lastverbexecuted); } return $message; }
/** * Checks a folder for changes performing Exporter->GetChangeCount() * * @param string $folderid counts changes for a folder * * @access private * @return boolean indicating if changes were found or not */ private function CountChange($folderid) { $spa = $this->GetCollection($folderid); // switch user store if this is a additional folder (additional true -> do not debug) ZPush::GetBackend()->Setup(ZPush::GetAdditionalSyncFolderStore($folderid, true)); $changecount = false; try { $exporter = ZPush::GetBackend()->GetExporter($folderid); if ($exporter !== false && isset($this->addparms[$folderid]["state"])) { $importer = false; $exporter->Config($this->addparms[$folderid]["state"], BACKEND_DISCARD_DATA); $exporter->ConfigContentParameters($spa->GetCPO()); $ret = $exporter->InitializeExporter($importer); if ($ret !== false) { $changecount = $exporter->GetChangeCount(); } } } catch (StatusException $ste) { throw new StatusException("SyncCollections->CountChange(): exporter can not be re-configured.", self::ERROR_WRONG_HIERARCHY, null, LOGLEVEL_WARN); } // start over if exporter can not be configured atm if ($changecount === false) { ZLog::Write(LOGLEVEL_WARN, "SyncCollections->CountChange(): no changes received from Exporter."); } $this->changes[$folderid] = $changecount; if (isset($this->addparms[$folderid]['savestate'])) { try { // Discard any data while (is_array($exporter->Synchronize())) { } $this->addparms[$folderid]['savestate'] = $exporter->GetState(); } catch (StatusException $ste) { throw new StatusException("SyncCollections->CountChange(): could not get new state from exporter", self::ERROR_WRONG_HIERARCHY, null, LOGLEVEL_WARN); } } return $changecount > 0; }
/** * Imports a move of a message. This occurs when a user moves an item to another folder * * Normally, we would implement this via the 'offical' importmessagemove() function on the ICS importer, * but the Zarafa importer does not support this. Therefore we currently implement it via a standard mapi * call. This causes a mirror 'add/delete' to be sent to the PDA at the next sync. * Manfred, 2010-10-21. For some mobiles import was causing duplicate messages in the destination folder * (Mantis #202). Therefore we will create a new message in the destination folder, copy properties * of the source message to the new one and then delete the source message. * * @param string $id * @param string $newfolder destination folder * * @access public * @return boolean * @throws StatusException */ public function ImportMessageMove($id, $newfolder) { if (strtolower($newfolder) == strtolower(bin2hex($this->folderid))) { throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, source and destination are equal", $id, $newfolder), SYNC_MOVEITEMSSTATUS_SAMESOURCEANDDEST); } // Get the entryid of the message we're moving $entryid = mapi_msgstore_entryidfromsourcekey($this->store, $this->folderid, hex2bin($id)); if (!$entryid) { throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to resolve source message id", $id, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID); } //open the source message $srcmessage = mapi_msgstore_openentry($this->store, $entryid); if (!$srcmessage) { throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to open source message: 0x%X", $id, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID); } // get correct mapi store for the destination folder $dststore = ZPush::GetBackend()->GetMAPIStoreForFolderId(ZPush::GetAdditionalSyncFolderStore($newfolder), $newfolder); if ($dststore === false) { throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to open store of destination folder", $id, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDDESTID); } $dstentryid = mapi_msgstore_entryidfromsourcekey($dststore, hex2bin($newfolder)); if (!$dstentryid) { throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to resolve destination folder", $id, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDDESTID); } $dstfolder = mapi_msgstore_openentry($dststore, $dstentryid); if (!$dstfolder) { throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to open destination folder", $id, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDDESTID); } $newmessage = mapi_folder_createmessage($dstfolder); if (!$newmessage) { throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to create message in destination folder: 0x%X", $id, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_INVALIDDESTID); } // Copy message mapi_copyto($srcmessage, array(), array(), $newmessage); if (mapi_last_hresult()) { throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, copy to destination message failed: 0x%X", $id, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_CANNOTMOVE); } $srcfolderentryid = mapi_msgstore_entryidfromsourcekey($this->store, $this->folderid); if (!$srcfolderentryid) { throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to resolve source folder", $id, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID); } $srcfolder = mapi_msgstore_openentry($this->store, $srcfolderentryid); if (!$srcfolder) { throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to open source folder: 0x%X", $id, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID); } // Save changes mapi_savechanges($newmessage); if (mapi_last_hresult()) { throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, mapi_savechanges() failed: 0x%X", $id, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_CANNOTMOVE); } // Delete the old message if (!mapi_folder_deletemessages($srcfolder, array($entryid))) { throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, delete of source message failed: 0x%X. Possible duplicates.", $id, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_SOURCEORDESTLOCKED); } $sourcekeyprops = mapi_getprops($newmessage, array(PR_SOURCE_KEY)); if (isset($sourcekeyprops[PR_SOURCE_KEY]) && $sourcekeyprops[PR_SOURCE_KEY]) { return bin2hex($sourcekeyprops[PR_SOURCE_KEY]); } return false; }
/** * Does the complete autodiscover. * @access public * @throws AuthenticationRequiredException if login to the backend failed. * @throws ZPushException if the incoming XML is invalid.. * * @return void */ public function DoAutodiscover() { if (!defined('REAL_BASE_PATH')) { define('REAL_BASE_PATH', str_replace('autodiscover/', '', BASE_PATH)); } set_include_path(get_include_path() . PATH_SEPARATOR . REAL_BASE_PATH); $response = ""; try { $incomingXml = $this->getIncomingXml(); $backend = ZPush::GetBackend(); $username = $this->login($backend, $incomingXml); $userDetails = $backend->GetUserDetails($username); $email = $this->getAttribFromUserDetails($userDetails, 'emailaddress') ? $this->getAttribFromUserDetails($userDetails, 'emailaddress') : $incomingXml->Request->EMailAddress; $userFullname = $this->getAttribFromUserDetails($userDetails, 'fullname') ? $this->getAttribFromUserDetails($userDetails, 'fullname') : $email; ZLog::Write(LOGLEVEL_WBXML, sprintf("Resolved user's '%s' fullname to '%s'", $username, $userFullname)); $response = $this->createResponse($email, $userFullname); setcookie("membername", $username); } catch (AuthenticationRequiredException $ex) { if (isset($incomingXml)) { ZLog::Write(LOGLEVEL_ERROR, sprintf("Unable to complete autodiscover because login failed for user with email '%s'", $incomingXml->Request->EMailAddress)); } else { ZLog::Write(LOGLEVEL_ERROR, sprintf("Unable to complete autodiscover incorrect request: '%s'", $ex->getMessage())); } header('HTTP/1.1 401 Unauthorized'); header('WWW-Authenticate: Basic realm="ZPush"'); http_response_code(401); } catch (ZPushException $ex) { ZLog::Write(LOGLEVEL_ERROR, sprintf("Unable to complete autodiscover because of ZPushException. Error: %s", $ex->getMessage())); if (!headers_sent()) { header('HTTP/1.1 ' . $ex->getHTTPCodeString()); foreach ($ex->getHTTPHeaders() as $h) { header($h); } } } $this->sendResponse($response); }
/** * Gets the policy name set in the backend or in device data. * * @access private * @return string */ private function getPolicyName() { $policyName = ZPush::GetBackend()->GetUserPolicyName(); $policyName = !empty($policyName) && $policyName !== false ? $policyName : ASDevice::DEFAULTPOLICYNAME; ZLog::Write(LOGLEVEL_DEBUG, sprintf("DeviceManager->getPolicyName(): determined policy name: '%s'", $policyName)); return $policyName; }
/** * Called at the end of the request * Statistics about received/sent data is saved here * * @access public * @return boolean */ public function Save() { // TODO save other stuff // check if previousily ignored messages were synchronized for the current folder // on multifolder operations of AS14 this is done by setLatestFolder() if ($this->latestFolder !== false) { $this->checkBrokenMessages($this->latestFolder); } // update the user agent and AS version on the device $this->device->SetUserAgent(Request::GetUserAgent()); $this->device->SetASVersion(Request::GetProtocolVersion()); // data to be saved $data = $this->device->GetData(); if ($data && Request::IsValidDeviceID()) { ZLog::Write(LOGLEVEL_DEBUG, "DeviceManager->Save(): Device data changed"); try { // 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 if ($this->device->IsNewDevice() && RequestProcessor::isUserAuthenticated()) { ZLog::Write(LOGLEVEL_INFO, sprintf("Linking device ID '%s' to user '%s'", $this->devid, $this->device->GetDeviceUser())); $this->statemachine->LinkUserDevice($this->device->GetDeviceUser(), $this->devid); } if (RequestProcessor::isUserAuthenticated() || $this->device->GetForceSave()) { $this->statemachine->SetState($data, $this->devid, IStateMachine::DEVICEDATA); ZLog::Write(LOGLEVEL_DEBUG, "DeviceManager->Save(): Device data saved"); } } catch (StateNotFoundException $snfex) { ZLog::Write(LOGLEVEL_ERROR, "DeviceManager->Save(): Exception: " . $snfex->getMessage()); } } // remove old search data $oldpid = $this->loopdetection->ProcessLoopDetectionGetOutdatedSearchPID(); if ($oldpid) { ZPush::GetBackend()->GetSearchProvider()->TerminateSearch($oldpid); } // we terminated this process if ($this->loopdetection) { $this->loopdetection->ProcessLoopDetectionTerminate(); } return true; }
$versions = ZPush::GetSupportedProtocolVersions(true); ZLog::Write(LOGLEVEL_INFO, sprintf("Announcing latest AS version to device: %s", $versions)); header("X-MS-RP: " . $versions); } RequestProcessor::Initialize(); RequestProcessor::HandleRequest(); // eventually the RequestProcessor wants to send other headers to the mobile foreach (RequestProcessor::GetSpecialHeaders() as $header) { header($header); } // log amount of data transferred // TODO check $len when streaming more data (e.g. Attachments), as the data will be send chunked ZPush::GetDeviceManager()->SentData(ob_get_length()); ZPush::FinishResponse(); // destruct backend after all data is on the stream ZPush::GetBackend()->Logoff(); } catch (NoPostRequestException $nopostex) { $len = ob_get_length(); if ($len) { ZLog::Write(LOGLEVEL_WARN, sprintf("Cleaning %d octets of data", $len)); ob_clean(); } if ($nopostex->getCode() == NoPostRequestException::OPTIONS_REQUEST) { header(ZPush::GetServerHeader()); header(ZPush::GetSupportedProtocolVersions()); header(ZPush::GetSupportedCommands()); ZLog::Write(LOGLEVEL_INFO, $nopostex->getMessage()); } else { if ($nopostex->getCode() == NoPostRequestException::GET_REQUEST) { if (Request::GetUserAgent()) { ZLog::Write(LOGLEVEL_INFO, sprintf("User-agent: '%s'", Request::GetUserAgent()));
/** * Imports a move of a message. This occurs when a user moves an item to another folder * * Normally, we would implement this via the 'offical' importmessagemove() function on the ICS importer, * but the Zarafa/Kopano importer does not support this. Therefore we currently implement it via a standard mapi * call. This causes a mirror 'add/delete' to be sent to the PDA at the next sync. * Manfred, 2010-10-21. For some mobiles import was causing duplicate messages in the destination folder * (Mantis #202). Therefore we will create a new message in the destination folder, copy properties * of the source message to the new one and then delete the source message. * * @param string $id * @param string $newfolder destination folder * * @access public * @return boolean * @throws StatusException */ public function ImportMessageMove($id, $newfolder) { list(, $sk) = Utils::SplitMessageId($id); if (strtolower($newfolder) == strtolower(bin2hex($this->folderid))) { throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, source and destination are equal", $id, $newfolder), SYNC_MOVEITEMSSTATUS_SAMESOURCEANDDEST); } // Get the entryid of the message we're moving $entryid = mapi_msgstore_entryidfromsourcekey($this->store, $this->folderid, hex2bin($sk)); $srcmessage = false; if ($entryid) { //open the source message $srcmessage = mapi_msgstore_openentry($this->store, $entryid); } if (!$entryid || !$srcmessage) { $code = SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID; // if we move to the trash and the source message is not found, we can also just tell the mobile that we successfully moved to avoid errors (ZP-624) if ($newfolder == ZPush::GetBackend()->GetWasteBasket()) { $code = SYNC_MOVEITEMSSTATUS_SUCCESS; } $errorCase = !$entryid ? "resolve source message id" : "open source message"; throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to %s: 0x%X", $sk, $newfolder, $errorCase, mapi_last_hresult()), $code); } // check if the source message is in the current syncinterval if (!$this->isMessageInSyncInterval($sk)) { throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Source message is outside the sync interval. Move not performed.", $sk, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID); } // get correct mapi store for the destination folder $dststore = ZPush::GetBackend()->GetMAPIStoreForFolderId(ZPush::GetAdditionalSyncFolderStore($newfolder), $newfolder); if ($dststore === false) { throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to open store of destination folder", $sk, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDDESTID); } $dstentryid = mapi_msgstore_entryidfromsourcekey($dststore, hex2bin($newfolder)); if (!$dstentryid) { throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to resolve destination folder", $sk, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDDESTID); } $dstfolder = mapi_msgstore_openentry($dststore, $dstentryid); if (!$dstfolder) { throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to open destination folder", $sk, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDDESTID); } $newmessage = mapi_folder_createmessage($dstfolder); if (!$newmessage) { throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to create message in destination folder: 0x%X", $sk, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_INVALIDDESTID); } // Copy message mapi_copyto($srcmessage, array(), array(), $newmessage); if (mapi_last_hresult()) { throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, copy to destination message failed: 0x%X", $sk, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_CANNOTMOVE); } $srcfolderentryid = mapi_msgstore_entryidfromsourcekey($this->store, $this->folderid); if (!$srcfolderentryid) { throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to resolve source folder", $sk, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID); } $srcfolder = mapi_msgstore_openentry($this->store, $srcfolderentryid); if (!$srcfolder) { throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to open source folder: 0x%X", $sk, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID); } // Save changes mapi_savechanges($newmessage); if (mapi_last_hresult()) { throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, mapi_savechanges() failed: 0x%X", $sk, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_CANNOTMOVE); } // Delete the old message if (!mapi_folder_deletemessages($srcfolder, array($entryid))) { throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, delete of source message failed: 0x%X. Possible duplicates.", $sk, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_SOURCEORDESTLOCKED); } $sourcekeyprops = mapi_getprops($newmessage, array(PR_SOURCE_KEY)); if (isset($sourcekeyprops[PR_SOURCE_KEY]) && $sourcekeyprops[PR_SOURCE_KEY]) { $prefix = ""; // prepend the destination short folderid, if it exists $destShortId = ZPush::GetDeviceManager()->GetFolderIdForBackendId($newfolder); if ($destShortId !== $newfolder) { $prefix = $destShortId . ":"; } return $prefix . bin2hex($sourcekeyprops[PR_SOURCE_KEY]); } return false; }
/** * Sends an email notification to the user containing the data the user tried to save. * * @param SyncObject $message * @param SyncObject $oldmessage * @return void */ private function sendNotificationEmail($message, $oldmessage) { // get email address and full name of the user $userinfo = ZPush::GetBackend()->GetUserDetails(Request::GetAuthUser()); // get the name of the folder $foldername = "unknown"; $folderid = bin2hex($this->folderid); $folders = ZPush::GetAdditionalSyncFolders(); if (isset($folders[$folderid]) && isset($folders[$folderid]->displayname)) { $foldername = $folders[$folderid]->displayname; } // get the differences between the two objects $data = substr(get_class($oldmessage), 4) . "\r\n"; // get the suppported fields as we need them to determine the ghosted properties $supportedFields = ZPush::GetDeviceManager()->GetSupportedFields(ZPush::GetDeviceManager()->GetFolderIdForBackendId($folderid)); $dataarray = $oldmessage->EvaluateAndCompare($message, @constant('READ_ONLY_NOTIFY_YOURDATA'), $supportedFields); foreach ($dataarray as $key => $value) { $value = str_replace("\r", "", $value); $value = str_replace("\n", str_pad("\r\n", 25), $value); $data .= str_pad(ucfirst($key) . ":", 25) . $value . "\r\n"; } // build a simple mime message $toEmail = $userinfo['emailaddress']; $mail = "From: Z-Push <no-reply>\r\n"; $mail .= "To: {$toEmail}\r\n"; $mail .= "Content-Type: text/plain; charset=utf-8\r\n"; $mail .= "Subject: " . @constant('READ_ONLY_NOTIFY_SUBJECT') . "\r\n\r\n"; $mail .= @constant('READ_ONLY_NOTIFY_BODY') . "\r\n"; // replace values of template $mail = str_replace("**USERFULLNAME**", $userinfo['fullname'], $mail); $mail = str_replace("**DATE**", strftime(@constant('READ_ONLY_NOTIFY_DATE_FORMAT')), $mail); $mail = str_replace("**TIME**", strftime(@constant('READ_ONLY_NOTIFY_TIME_FORMAT')), $mail); $mail = str_replace("**FOLDERNAME**", $foldername, $mail); $mail = str_replace("**MOBILETYPE**", Request::GetDeviceType(), $mail); $mail = str_replace("**MOBILEDEVICEID**", Request::GetDeviceID(), $mail); $mail = str_replace("**DIFFERENCES**", $data, $mail); // user send email to himself $m = new SyncSendMail(); $m->saveinsent = false; $m->replacemime = true; $m->mime = $mail; ZPush::GetBackend()->SendMail($m); }
/** * Returns a list of all additional folders of the given device and the Request::GetGETUser(). * * @param string $deviceId device id that should be listed. * * @access public * @return array */ public function AdditionalFolderList($deviceId) { $user = Request::GetGETUser(); $deviceId = preg_replace("/[^A-Za-z0-9]/", "", $deviceId); $folders = ZPushAdmin::AdditionalFolderList($user, $deviceId); ZLog::Write(LOGLEVEL_INFO, sprintf("WebserviceDevice::AdditionalFolderList(): found %d folders for device '%s' of user '%s'", count($folders), $deviceId, $user)); // retrieve the permission flags from the backend $backend = ZPush::GetBackend(); foreach ($folders as &$folder) { $folder['readable'] = $backend->Setup($folder['store'], true, $folder['folderid'], true); $folder['writeable'] = $backend->Setup($folder['store'], true, $folder['folderid']); } // make sure folder is not pointing to our last folder anymore unset($folder); ZPush::GetTopCollector()->AnnounceInformation(sprintf("Retrieved details of %d folders", count($folders)), true); return $folders; }
/** * Does the complete autodiscover. * @access public * @throws AuthenticationRequiredException if login to the backend failed. * @throws ZPushException if the incoming XML is invalid.. * * @return void */ public function DoAutodiscover() { $response = ""; try { $incomingXml = $this->getIncomingXml(); $backend = ZPush::GetBackend(); $username = $this->login($backend, $incomingXml); $userDetails = $backend->GetUserDetails($username); $email = $this->getAttribFromUserDetails($userDetails, 'emailaddress') ? $this->getAttribFromUserDetails($userDetails, 'emailaddress') : $incomingXml->Request->EMailAddress; $userFullname = $this->getAttribFromUserDetails($userDetails, 'fullname') ? $this->getAttribFromUserDetails($userDetails, 'fullname') : $email; ZLog::Write(LOGLEVEL_WBXML, sprintf("Resolved user's '%s' fullname to '%s'", $username, $userFullname)); // At the moment Z-Push only supports mobile response schema for autodiscover. Send empty response if the client request outlook response schema. if ($incomingXml->Request->AcceptableResponseSchema == ZPushAutodiscover::ACCEPTABLERESPONSESCHEMAMOBILESYNC) { $response = $this->createResponse($email, $userFullname); setcookie("membername", $username); } } catch (AuthenticationRequiredException $ex) { if (isset($incomingXml)) { ZLog::Write(LOGLEVEL_ERROR, sprintf("Unable to complete autodiscover because login failed for user with email '%s'", $incomingXml->Request->EMailAddress)); } else { ZLog::Write(LOGLEVEL_ERROR, sprintf("Unable to complete autodiscover incorrect request: '%s'", $ex->getMessage())); } http_response_code(401); header('WWW-Authenticate: Basic realm="ZPush"'); } catch (ZPushException $ex) { ZLog::Write(LOGLEVEL_ERROR, sprintf("Unable to complete autodiscover because of ZPushException. Error: %s", $ex->getMessage())); if (!headers_sent()) { header('HTTP/1.1 ' . $ex->getHTTPCodeString()); foreach ($ex->getHTTPHeaders() as $h) { header($h); } } } $this->sendResponse($response); }
/** * Initialize the RequestProcessor * * @access public * @return */ public static function Initialize() { self::$backend = ZPush::GetBackend(); self::$deviceManager = ZPush::GetDeviceManager(); self::$topCollector = ZPush::GetTopCollector(); if (!ZPush::CommandNeedsPlainInput(Request::GetCommandCode())) { self::$decoder = new WBXMLDecoder(Request::GetInputStream()); } self::$encoder = new WBXMLEncoder(Request::GetOutputStream(), Request::GetGETAcceptMultipart()); }
ZLog::Initialize(); ZLog::Write(LOGLEVEL_DEBUG, "-------- Start"); ZLog::Write(LOGLEVEL_INFO, sprintf("Version='%s' method='%s' from='%s' cmd='%s' getUser='******' devId='%s' devType='%s'", @constant('ZPUSH_VERSION'), Request::GetMethod(), Request::GetRemoteAddr(), Request::GetCommand(), Request::GetGETUser(), Request::GetDeviceID(), Request::GetDeviceType())); // Stop here if this is an OPTIONS request if (Request::IsMethodOPTIONS()) { throw new NoPostRequestException("Options request", NoPostRequestException::OPTIONS_REQUEST); } ZPush::CheckAdvancedConfig(); // Process request headers and look for AS headers Request::ProcessHeaders(); // Check required GET parameters if (Request::IsMethodPOST() && (Request::GetCommandCode() === false || !Request::GetGETUser() || !Request::GetDeviceID() || !Request::GetDeviceType())) { throw new FatalException("Requested the Z-Push URL without the required GET parameters"); } // Load the backend $backend = ZPush::GetBackend(); // always request the authorization header if (!Request::AuthenticationInfo()) { throw new AuthenticationRequiredException("Access denied. Please send authorisation information"); } // check the provisioning information if (PROVISIONING === true && Request::IsMethodPOST() && ZPush::CommandNeedsProvisioning(Request::GetCommandCode()) && (Request::WasPolicyKeySent() && Request::GetPolicyKey() == 0 || ZPush::GetDeviceManager()->ProvisioningRequired(Request::GetPolicyKey())) && (LOOSE_PROVISIONING === false || LOOSE_PROVISIONING === true && Request::WasPolicyKeySent())) { //TODO for AS 14 send a wbxml response throw new ProvisioningRequiredException(); } // most commands require an authenticated user if (ZPush::CommandNeedsAuthentication(Request::GetCommandCode())) { RequestProcessor::Authenticate(); } // Do the actual processing of the request if (Request::IsMethodGET()) {
/** * Applies settings to and gets informations from the device * * @param SyncObject $settings (SyncOOF or SyncUserInformation possible) * * @access public * @return SyncObject $settings */ public function Settings($settings) { if ($settings instanceof SyncOOF) { $isget = !empty($settings->bodytype); $settings = new SyncOOF(); if ($isget) { //oof get $settings->oofstate = 0; $settings->Status = SYNC_SETTINGSSTATUS_SUCCESS; } else { //oof set $settings->Status = SYNC_SETTINGSSTATUS_PROTOCOLLERROR; } } if ($settings instanceof SyncUserInformation) { $settings->emailaddresses = array(ZPush::GetBackend()->GetUserDetails(Request::GetAuthUser())['emailaddress']); $settings->Status = SYNC_SETTINGSSTATUS_SUCCESS; } return $settings; }