. * * Consult LICENSE file for details ************************************************/ //include the CombinedBackend's own config file require_once("backend/combined/config.php"); require_once("backend/combined/importer.php"); require_once("backend/combined/exporter.php"); class BackendCombined extends Backend implements ISearchProvider { public $config; /** * @var IBackend[] */ public $backends; /** * @var IBackend */ private $activeBackend; private $activeBackendID; private $numberChangesSink; private $logon_done = false; /** * Constructor of the combined backend * * @access public */ public function BackendCombined() { parent::Backend(); $this->config = BackendCombinedConfig::GetBackendCombinedConfig(); $backend_values = array_unique(array_values($this->config['folderbackend'])); foreach ($backend_values as $i) { ZPush::IncludeBackend($this->config['backends'][$i]['name']); $this->backends[$i] = new $this->config['backends'][$i]['name'](); } ZLog::Write(LOGLEVEL_DEBUG, sprintf("Combined %d backends loaded.", count($this->backends))); } /** * Authenticates the user on each backend * * @param string $username * @param string $domain * @param string $password * * @access public * @return boolean */ public function Logon($username, $domain, $password) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("Combined->Logon('%s', '%s',***))", $username, $domain)); if(!is_array($this->backends)){ return false; } foreach ($this->backends as $i => $b){ $u = $username; $d = $domain; $p = $password; // Apply mapping from configuration if(isset($this->config['backends'][$i]['users'])){ if(!isset($this->config['backends'][$i]['users'][$username])){ unset($this->backends[$i]); continue; } if(isset($this->config['backends'][$i]['users'][$username]['username'])) $u = $this->config['backends'][$i]['users'][$username]['username']; if(isset($this->config['backends'][$i]['users'][$username]['password'])) $p = $this->config['backends'][$i]['users'][$username]['password']; if(isset($this->config['backends'][$i]['users'][$username]['domain'])) $d = $this->config['backends'][$i]['users'][$username]['domain']; } // Apply username mapping from state backend if (isset($this->config['usemapping']) && $this->config['usemapping']) { $mappedUsername = ZPush::GetStateMachine()->GetMappedUsername($u, strtolower($this->config['backends'][$i]['name'])); if ($mappedUsername !== null) { $u = $mappedUsername; } } if ($this->backends[$i]->Logon($u, $d, $p) == false) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("Combined->Logon() failed on %s ", $this->config['backends'][$i]['name'])); return false; } $this->backends[$i]->SetOriginalUsername($username); } $this->logon_done = true; ZLog::Write(LOGLEVEL_DEBUG, "Combined->Logon() success"); return true; } /** * Setup the backend to work on a specific store or checks ACLs there. * If only the $store is submitted, all Import/Export/Fetch/Etc operations should be * performed on this store (switch operations store). * If the ACL check is enabled, this operation should just indicate the ACL status on * the submitted store, without changing the store for operations. * For the ACL status, the currently logged on user MUST have access rights on * - the entire store - admin access if no folderid is sent, or * - on a specific folderid in the store (secretary/full access rights) * * The ACLcheck MUST fail if a folder of the authenticated user is checked! * * @param string $store target store, could contain a "domain\user" value * @param boolean $checkACLonly if set to true, Setup() should just check ACLs * @param string $folderid if set, only ACLs on this folderid are relevant * * @access public * @return boolean */ public function Setup($store, $checkACLonly = false, $folderid = false) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("Combined->Setup('%s', '%s', '%s')", $store, Utils::PrintAsString($checkACLonly), $folderid)); if(!is_array($this->backends)){ return false; } foreach ($this->backends as $i => $b){ $u = $store; if(isset($this->config['backends'][$i]['users']) && isset($this->config['backends'][$i]['users'][$store]['username'])){ $u = $this->config['backends'][$i]['users'][$store]['username']; } if($this->backends[$i]->Setup($u, $checkACLonly, $folderid) == false){ ZLog::Write(LOGLEVEL_WARN, "Combined->Setup() failed"); return false; } } ZLog::Write(LOGLEVEL_DEBUG, "Combined->Setup() success"); return true; } /** * Logs off each backend * * @access public * @return boolean */ public function Logoff() { // If no Logon in done, omit Logoff if (!$this->logon_done) return true; ZLog::Write(LOGLEVEL_DEBUG, "Combined->Logoff()"); foreach ($this->backends as $i => $b){ $this->backends[$i]->Logoff(); } ZLog::Write(LOGLEVEL_DEBUG, "Combined->Logoff() success"); return true; } /** * Returns an array of SyncFolder types with the entire folder hierarchy * from all backends combined * * provides AS 1.0 compatibility * * @access public * @return array SYNC_FOLDER */ public function GetHierarchy(){ ZLog::Write(LOGLEVEL_DEBUG, "Combined->GetHierarchy()"); $ha = array(); foreach ($this->backends as $i => $b){ if(!empty($this->config['backends'][$i]['subfolder'])){ $f = new SyncFolder(); $f->serverid = $i.$this->config['delimiter'].'0'; $f->parentid = '0'; $f->displayname = $this->config['backends'][$i]['subfolder']; $f->type = SYNC_FOLDER_TYPE_OTHER; $ha[] = $f; } $h = $this->backends[$i]->GetHierarchy(); if(is_array($h)){ foreach($h as $j => $f){ $h[$j]->serverid = $i.$this->config['delimiter'].$h[$j]->serverid; if($h[$j]->parentid != '0' || !empty($this->config['backends'][$i]['subfolder'])){ $h[$j]->parentid = $i.$this->config['delimiter'].$h[$j]->parentid; } if(isset($this->config['folderbackend'][$h[$j]->type]) && $this->config['folderbackend'][$h[$j]->type] != $i){ $h[$j]->type = SYNC_FOLDER_TYPE_OTHER; } } $ha = array_merge($ha, $h); } } ZLog::Write(LOGLEVEL_DEBUG, "Combined->GetHierarchy() success"); return $ha; } /** * Returns the importer to process changes from the mobile * * @param string $folderid (opt) * * @access public * @return object(ImportChanges) */ public function GetImporter($folderid = false) { if($folderid !== false) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("Combined->GetImporter() Content: ImportChangesCombined:('%s')", $folderid)); // get the contents importer from the folder in a backend // the importer is wrapped to check foldernames in the ImportMessageMove function $backend = $this->GetBackend($folderid); if($backend === false) return false; $importer = $backend->GetImporter($this->GetBackendFolder($folderid)); if($importer){ return new ImportChangesCombined($this, $folderid, $importer); } return false; } else { ZLog::Write(LOGLEVEL_DEBUG, "Combined->GetImporter() -> Hierarchy: ImportChangesCombined()"); //return our own hierarchy importer which send each change to the right backend return new ImportChangesCombined($this); } } /** * Returns the exporter to send changes to the mobile * the exporter from right backend for contents exporter and our own exporter for hierarchy exporter * * @param string $folderid (opt) * * @access public * @return object(ExportChanges) */ public function GetExporter($folderid = false){ ZLog::Write(LOGLEVEL_DEBUG, sprintf("Combined->GetExporter('%s')", $folderid)); if($folderid){ $backend = $this->GetBackend($folderid); if($backend == false) return false; return $backend->GetExporter($this->GetBackendFolder($folderid)); } return new ExportChangesCombined($this); } /** * Sends an e-mail * This messages needs to be saved into the 'sent items' folder * * @param SyncSendMail $sm SyncSendMail object * * @access public * @return boolean * @throws StatusException */ public function SendMail($sm) { ZLog::Write(LOGLEVEL_DEBUG, "Combined->SendMail()"); // Convert source folderid if (isset($sm->source->folderid)) { $sm->source->folderid = $this->GetBackendFolder($sm->source->folderid); } foreach ($this->backends as $i => $b){ if($this->backends[$i]->SendMail($sm) == true){ return true; } } return false; } /** * Returns all available data of a single message * * @param string $folderid * @param string $id * @param ContentParameters $contentparameters flag * * @access public * @return object(SyncObject) * @throws StatusException */ public function Fetch($folderid, $id, $contentparameters) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("Combined->Fetch('%s', '%s', CPO)", $folderid, $id)); $backend = $this->GetBackend($folderid); if($backend == false) return false; return $backend->Fetch($this->GetBackendFolder($folderid), $id, $contentparameters); } /** * Returns the waste basket * If the wastebasket is set to one backend, return the wastebasket of that backend * else return the first waste basket we can find * * @access public * @return string */ function GetWasteBasket(){ ZLog::Write(LOGLEVEL_DEBUG, "Combined->GetWasteBasket()"); if (isset($this->activeBackend)) { if (!$this->activeBackend->GetWasteBasket()) return false; else return $this->activeBackendID . $this->config['delimiter'] . $this->activeBackend->GetWasteBasket(); } return false; } /** * Returns the content of the named attachment as stream. * There is no way to tell which backend the attachment is from, so we try them all * * @param string $attname * * @access public * @return SyncItemOperationsAttachment * @throws StatusException */ public function GetAttachmentData($attname) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("Combined->GetAttachmentData('%s')", $attname)); foreach ($this->backends as $i => $b) { try { $attachment = $this->backends[$i]->GetAttachmentData($attname); if ($attachment instanceof SyncItemOperationsAttachment) return $attachment; } catch (StatusException $s) { // backends might throw StatusExceptions if it's not their attachment } } throw new StatusException("Combined->GetAttachmentData(): no backend found", SYNC_ITEMOPERATIONSSTATUS_INVALIDATT); } /** * Processes a response to a meeting request. * * @param string $requestid id of the object containing the request * @param string $folderid id of the parent folder of $requestid * @param string $response * * @access public * @return string id of the created/updated calendar obj * @throws StatusException */ public function MeetingResponse($requestid, $folderid, $response) { $backend = $this->GetBackend($folderid); if($backend === false) return false; return $backend->MeetingResponse($requestid, $this->GetBackendFolder($folderid), $response); } /** * Deletes all contents of the specified folder. * This is generally used to empty the trash (wastebasked), but could also be used on any * other folder. * * @param string $folderid * @param boolean $includeSubfolders (opt) also delete sub folders, default true * * @access public * @return boolean * @throws StatusException */ public function EmptyFolder($folderid, $includeSubfolders = true) { $backend = $this->GetBackend($folderid); if($backend === false) return false; return $backend->EmptyFolder($this->GetBackendFolder($folderid), $includeSubfolders); } /** * Indicates if the backend has a ChangesSink. * A sink is an active notification mechanism which does not need polling. * * @access public * @return boolean */ public function HasChangesSink() { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCombined->HasChangesSink()")); $this->numberChangesSink = 0; foreach ($this->backends as $i => $b) { if ($this->backends[$i]->HasChangesSink()) { $this->numberChangesSink++; } } ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCombined->HasChangesSink - Number ChangesSink found: %d", $this->numberChangesSink)); return true; } /** * The folder should be considered by the sink. * Folders which were not initialized should not result in a notification * of IBacken->ChangesSink(). * * @param string $folderid * * @access public * @return boolean false if there is any problem with that folder */ public function ChangesSinkInitialize($folderid) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCombined->ChangesSinkInitialize('%s')", $folderid)); $backend = $this->GetBackend($folderid); if($backend === false) { // if not backend is found we return true, we don't want this to never cause an error return true; } if ($backend->HasChangesSink()) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCombined->ChangesSinkInitialize('%s') is supported, initializing", $folderid)); return $backend->ChangesSinkInitialize($this->GetBackendFolder($folderid)); } else { // if the backend doesn't support ChangesSink, we also return true so we don't get an error return true; } } /** * The actual ChangesSink. * For max. the $timeout value this method should block and if no changes * are available return an empty array. * If changes are available a list of folderids is expected. * * @param int $timeout max. amount of seconds to block * * @access public * @return array */ public function ChangesSink($timeout = 30) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCombined->ChangesSink(%d)", $timeout)); $notifications = array(); if ($this->numberChangesSink == 0) { ZLog::Write(LOGLEVEL_DEBUG, "BackendCombined doesn't include any Sinkable backends"); } else { $time_each = $timeout / $this->numberChangesSink; foreach ($this->backends as $i => $b) { if ($this->backends[$i]->HasChangesSink()) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendCombined->ChangesSink - Calling in '%s' with %d", get_class($b), $time_each)); $notifications_backend = $this->backends[$i]->ChangesSink($time_each); //preppend backend delimiter for ($c = 0; $c < count($notifications_backend); $c++) { $notifications_backend[$c] = $i . $this->config['delimiter'] . $notifications_backend[$c]; } $notifications = array_merge($notifications, $notifications_backend); } } } return $notifications; } /** * Finds the correct backend for a folder * * @param string $folderid combinedid of the folder * * @access public * @return object */ public function GetBackend($folderid){ $pos = strpos($folderid, $this->config['delimiter']); if($pos === false) return false; $id = substr($folderid, 0, $pos); if(!isset($this->backends[$id])) return false; $this->activeBackend = $this->backends[$id]; $this->activeBackendID = $id; return $this->backends[$id]; } /** * Returns an understandable folderid for the backend * * @param string $folderid combinedid of the folder * * @access public * @return string */ public function GetBackendFolder($folderid){ $pos = strpos($folderid, $this->config['delimiter']); if($pos === false) return false; return substr($folderid,$pos + strlen($this->config['delimiter'])); } /** * Returns backend id for a folder * * @param string $folderid combinedid of the folder * * @access public * @return object */ public function GetBackendId($folderid){ $pos = strpos($folderid, $this->config['delimiter']); if($pos === false) return false; return substr($folderid, 0, $pos); } /** * Indicates which AS version is supported by the backend. * Return the lowest version supported by the backends used. * * @access public * @return string AS version constant */ public function GetSupportedASVersion() { $version = ZPush::ASV_14; foreach ($this->backends as $i => $b) { $subversion = $this->backends[$i]->GetSupportedASVersion(); if ($subversion < $version) { $version = $subversion; } } return $version; } /** * Returns the BackendCombined as it implements the ISearchProvider interface * This could be overwritten by the global configuration * * @access public * @return object Implementation of ISearchProvider */ public function GetSearchProvider() { return $this; } /*----------------------------------------------------------------------------------------- -- ISearchProvider ------------------------------------------------------------------------------------------*/ /** * Indicates if a search type is supported by this SearchProvider * It supports all the search types, searches are delegated. * * @param string $searchtype * * @access public * @return boolean */ public function SupportsType($searchtype) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("Combined->SupportsType('%s')", $searchtype)); $i = $this->getSearchBackend($searchtype); return $i !== false; } /** * Queries the LDAP backend * * @param string $searchquery string to be searched for * @param string $searchrange specified searchrange * * @access public * @return array search results */ public function GetGALSearchResults($searchquery, $searchrange) { ZLog::Write(LOGLEVEL_DEBUG, "Combined->GetGALSearchResults()"); $i = $this->getSearchBackend(ISearchProvider::SEARCH_GAL); $result = false; if ($i !== false) { $result = $this->backends[$i]->GetGALSearchResults($searchquery, $searchrange); } return $result; } /** * Searches for the emails on the server * * @param ContentParameter $cpo * * @return array */ public function GetMailboxSearchResults($cpo) { ZLog::Write(LOGLEVEL_DEBUG, "Combined->GetMailboxSearchResults()"); $i = $this->getSearchBackend(ISearchProvider::SEARCH_MAILBOX); $result = false; if ($i !== false) { //Convert $cpo GetSearchFolderid $cpo->SetSearchFolderid($this->GetBackendFolder($cpo->GetSearchFolderid())); $result = $this->backends[$i]->GetMailboxSearchResults($cpo, $i . $this->config['delimiter']); } return $result; } /** * Terminates a search for a given PID * * @param int $pid * * @return boolean */ public function TerminateSearch($pid) { ZLog::Write(LOGLEVEL_DEBUG, "Combined->TerminateSearch()"); foreach ($this->backends as $i => $b) { if ($this->backends[$i] instanceof ISearchProvider) { $this->backends[$i]->TerminateSearch($pid); } } return true; } /** * Disconnects backends * * @access public * @return boolean */ public function Disconnect() { ZLog::Write(LOGLEVEL_DEBUG, "Combined->Disconnect()"); foreach ($this->backends as $i => $b) { if ($this->backends[$i] instanceof ISearchProvider) { $this->backends[$i]->Disconnect(); } } return true; } /** * Returns the first backend that support a search type * * @param string $searchtype * * @access private * @return string */ private function getSearchBackend($searchtype) { foreach ($this->backends as $i => $b) { if ($this->backends[$i] instanceof ISearchProvider) { if ($this->backends[$i]->SupportsType($searchtype)) { return $i; } } } ZLog::Write(LOGLEVEL_DEBUG, sprintf("Combined->getSearchBackend('%s') No support found!", $searchtype)); return false; } }