1<?php 2/** 3 * EGroupware - eSync - ActiveSync protocol based on Z-Push: backend for EGroupware 4 * 5 * @link http://www.egroupware.org 6 * @package esync 7 * @author Ralf Becker <rb@egroupware.org> 8 * @author EGroupware GmbH <info@egroupware.org> 9 * @author Philip Herbert <philip@knauber.de> 10 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License 11 */ 12 13use EGroupware\Api; 14 15/** 16 * Z-Push backend for EGroupware 17 * 18 * Uses EGroupware application specific plugins, eg. mail_zpush class 19 * 20 * @todo store states in DB 21 * @todo change AlterPingChanges method to GetFolderState in plugins, interfaces and egw backend directly returning state 22 */ 23class activesync_backend extends BackendDiff implements ISearchProvider 24{ 25 var $egw_sessionID; 26 27 var $hierarchyimporter; 28 var $contentsimporter; 29 var $exporter; 30 31 var $authenticated = false; 32 33 /** 34 * Integer waitOnFailureDefault how long (in seconds) to wait on connection failure 35 * 36 * @var int 37 */ 38 static $waitOnFailureDefault = 30; 39 40 /** 41 * Integer waitOnFailureLimit how long (in seconds) to wait on connection failure until a 500 is raised 42 * 43 * @var int 44 */ 45 static $waitOnFailureLimit = 7200; 46 47 /** 48 * Constructor 49 * 50 * We create/verify EGroupware session here, as we need our plugins before Logon get called 51 */ 52 function __construct() 53 { 54 parent::__construct(); 55 56 // AS preferences needs to instanciate this class too, but has no running AS request 57 // regular AS still runs as "login", when it instanciates our backend 58 if ($GLOBALS['egw_info']['flags']['currentapp'] == 'login') 59 { 60 // need to call ProcessHeaders() here, to be able to use ::GetAuth(User|Password), unfortunately zpush calls it too so it runs twice 61 Request::ProcessHeaders(); 62 $username = Request::GetAuthUser(); 63 $password = Request::GetAuthPassword(); 64 65 // check credentials and create session 66 $GLOBALS['egw_info']['flags']['currentapp'] = 'activesync'; 67 $this->authenticated = (($this->egw_sessionID = Api\Session::get_sessionid(true)) && 68 $GLOBALS['egw']->session->verify($this->egw_sessionID) && 69 base64_decode(Api\Cache::getSession('phpgwapi', 'password')) === $password || // check if session contains password 70 ($this->egw_sessionID = $GLOBALS['egw']->session->create($username,$password,'text',true))); // true = no real session 71 72 // closing session right away to not block parallel requests, 73 // this is a must for Ping requests, others might give better performance 74 if ($this->authenticated) // && Request::GetCommand() == 'Ping') 75 { 76 $GLOBALS['egw']->session->commit_session(); 77 } 78 79 // enable logging for all devices of a user or a specific device 80 $dev_id = Request::GetDeviceID(); 81 //error_log(__METHOD__."() username=$username, dev_id=$dev_id, prefs[activesync]=".array2string($GLOBALS['egw_info']['user']['preferences']['activesync'])); 82 if ($GLOBALS['egw_info']['user']['preferences']['activesync']['logging'] == 'user' || 83 $GLOBALS['egw_info']['user']['preferences']['activesync']['logging'] == $dev_id.'.log') 84 { 85 ZLog::SpecialLogUser(); 86 } 87 88 // check if we support loose provisioning for that device 89 $response = $this->run_on_all_plugins('LooseProvisioning', array(), $dev_id); 90 $loose_provisioning = $response ? (boolean)$response['response'] : false; 91 define('LOOSE_PROVISIONING', $loose_provisioning); 92 93 ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."() username=$username, loose_provisioning=".array2string($loose_provisioning).", autheticated=".array2string($this->authenticated)); 94 } 95 } 96 97 /** 98 * Log into EGroupware 99 * 100 * @param string $username 101 * @param string $domain 102 * @param string $password 103 * @return boolean TRUE if the logon succeeded, FALSE if not 104 * @throws FatalException e.g. some required libraries are unavailable 105 */ 106 public function Logon($username, $domain, $password) 107 { 108 if (!$this->authenticated) 109 { 110 ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."() z-push authentication failed: ".$GLOBALS['egw']->session->cd_reason); 111 return false; 112 } 113 if (!isset($GLOBALS['egw_info']['user']['apps']['activesync'])) 114 { 115 ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."() z-push authentication failed: NO run rights for E-Push application!"); 116 return false; 117 } 118 ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$username','$domain',...) logon SUCCESS"); 119 120 // call plugins in case they are interested in being call on each command 121 $this->run_on_all_plugins(__FUNCTION__, array(), $username, $domain, $password); 122 123 return true; 124 } 125 126 /** 127 * Called before closing connection 128 */ 129 public function Logoff() 130 { 131 $this->_loggedin = FALSE; 132 133 ZLog::Write(LOGLEVEL_DEBUG, "LOGOFF"); 134 } 135 136 /** 137 * This function is analogous to GetMessageList. 138 */ 139 function GetFolderList() 140 { 141 //error_log(__METHOD__."()"); 142 $folderlist = $this->run_on_all_plugins(__FUNCTION__); 143 //error_log(__METHOD__."() run_On_all_plugins() returned ".array2string($folderlist)); 144 $applist = array('addressbook','calendar','mail'); 145 foreach($applist as $app) 146 { 147 if (!isset($GLOBALS['egw_info']['user']['apps'][$app])) 148 { 149 $folderlist[] = $folder = array( 150 'id' => $this->createID($app, 151 $app == 'mail' ? 0 : // fmail uses id=0 for INBOX, other apps account_id of user 152 $GLOBALS['egw_info']['user']['account_id']), 153 'mod' => 'not-enabled', 154 'parent'=> '0', 155 ); 156 ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."() adding for disabled $app ".array2string($folder)); 157 } 158 } 159 //ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."() returning ".array2string($folderlist)); 160 161 return $folderlist; 162 } 163 164 /** 165 * Get Information about a folder 166 * 167 * @param string $id 168 * @return SyncFolder|boolean false on error 169 */ 170 function GetFolder($id) 171 { 172 if (!($ret = $this->run_on_plugin_by_id(__FUNCTION__, $id))) 173 { 174 $type = $folder = $app = null; 175 $this->splitID($id, $type, $folder, $app); 176 177 if (!isset($GLOBALS['egw_info']['user']['apps'][$app])) 178 { 179 $ret = new SyncFolder(); 180 $ret->serverid = $id; 181 $ret->parentid = '0'; 182 $ret->displayname = 'not-enabled'; 183 $account_id = $GLOBALS['egw_info']['user']['account_id']; 184 switch($app) 185 { 186 case 'addressbook': 187 $ret->type = $folder == $account_id ? SYNC_FOLDER_TYPE_CONTACT : SYNC_FOLDER_TYPE_USER_CONTACT; 188 break; 189 case 'calendar': 190 $ret->type = $folder == $account_id ? SYNC_FOLDER_TYPE_APPOINTMENT : SYNC_FOLDER_TYPE_USER_APPOINTMENT; 191 break; 192 case 'infolog': 193 $ret->type = $folder == $account_id ? SYNC_FOLDER_TYPE_TASK : SYNC_FOLDER_TYPE_USER_TASK; 194 break; 195 default: 196 $ret->type = $folder == 0 ? SYNC_FOLDER_TYPE_INBOX : SYNC_FOLDER_TYPE_USER_MAIL; 197 break; 198 } 199 ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."($id) return ".array2string($ret)." for disabled app!"); 200 } 201 } 202 //ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$id') returning ".array2string($ret)); 203 return $ret; 204 } 205 206 /** 207 * Return folder stats. This means you must return an associative array with the 208 * following properties: 209 * 210 * "id" => The server ID that will be used to identify the folder. It must be unique, and not too long 211 * How long exactly is not known, but try keeping it under 20 chars or so. It must be a string. 212 * "parent" => The server ID of the parent of the folder. Same restrictions as 'id' apply. 213 * "mod" => This is the modification signature. It is any arbitrary string which is constant as long as 214 * the folder has not changed. In practice this means that 'mod' can be equal to the folder name 215 * as this is the only thing that ever changes in folders. (the type is normally constant) 216 * 217 * @return array with values for keys 'id', 'mod' and 'parent' 218 */ 219 function StatFolder($id) 220 { 221 if (!($ret = $this->run_on_plugin_by_id(__FUNCTION__, $id))) 222 { 223 $type = $folder = $app = null; 224 $this->splitID($id, $type, $folder, $app); 225 226 if (!isset($GLOBALS['egw_info']['user']['apps'][$app])) 227 { 228 $ret = array( 229 'id' => $id, 230 'mod' => 'not-enabled', 231 'parent'=> '0', 232 ); 233 ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."($id) return ".array2string($ret)." for disabled app!"); 234 } 235 } 236 //ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$id') returning ".array2string($ret)); 237 return $ret; 238 } 239 240 241 /** 242 * Creates or modifies a folder 243 * 244 * Attention: eGroupware currently does not support creating folders. The first device seen during testing 245 * is now iOS 5 (beta). At least returning false causes the sync not to break. 246 * As we currently do not support this function currently nothing is forwarded to the plugin. 247 * 248 * @param $id of the parent folder 249 * @param $oldid => if empty -> new folder created, else folder is to be renamed 250 * @param $displayname => new folder name (to be created, or to be renamed to) 251 * @param type => folder type, ignored in IMAP 252 * 253 * @return stat | boolean false on error 254 * 255 */ 256 function ChangeFolder($id, $oldid, $displayname, $type) 257 { 258 ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."(ParentId=$id, oldid=$oldid, displaname=$displayname, type=$type)"); 259 $ret = $this->run_on_plugin_by_id(__FUNCTION__, $id, $oldid, $displayname, $type); 260 if (!$ret) ZLog::Write(LOGLEVEL_ERROR, __METHOD__." WARNING : something failed changing folders, now informing the device that this has failed"); 261 return $ret; 262 } 263 264 265 /** 266 * Should return a list (array) of messages, each entry being an associative array 267 * with the same entries as StatMessage(). This function should return stable information; ie 268 * if nothing has changed, the items in the array must be exactly the same. The order of 269 * the items within the array is not important though. 270 * 271 * The cutoffdate is a date in the past, representing the date since which items should be shown. 272 * This cutoffdate is determined by the user's setting of getting 'Last 3 days' of e-mail, etc. If 273 * you ignore the cutoffdate, the user will not be able to select their own cutoffdate, but all 274 * will work OK apart from that. 275 * 276 * @param string $id folder id 277 * @param int $cutoffdate =null 278 * @return array 279 */ 280 function GetMessageList($id, $cutoffdate=NULL) 281 { 282 $type = $folder = $app = null; 283 $this->splitID($id, $type, $folder, $app); 284 //ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."($id, $cutoffdate) type=$type, folder=$folder, app=$app"); 285 if (!($ret = $this->run_on_plugin_by_id(__FUNCTION__, $id, $cutoffdate))) 286 { 287 if (!isset($GLOBALS['egw_info']['user']['apps'][$app])) 288 { 289 ZLog::Write(LOGLEVEL_ERROR, __METHOD__."($id, $cutoffdate) return array() for disabled app!"); 290 $ret = array(); 291 } 292 } 293 // allow other apps to insert meeting requests 294 /*if ($app == 'mail' && $folder == 0) 295 { 296 $before = count($ret); 297 $not_uids = array(); 298 foreach($ret as $message) 299 { 300 if (isset($message->meetingrequest) && is_a($message->meetingrequest, 'SyncMeetingRequest')) 301 { 302 $not_uids[] = self::globalObjId2uid($message->meetingrequest->globalobjid); 303 } 304 } 305 $ret2 = $this->run_on_all_plugins('GetMeetingRequests', $ret, $not_uids, $cutoffdate); 306 if (is_array($ret2) && !empty($ret2)) 307 { 308 ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."($id, $cutoffdate) call to GetMeetingRequests added ".(count($ret2)-$before)." messages"); 309 ZLog::Write(LOGLEVEL_DEBUG, array2string($ret2)); 310 $ret = $ret2; // should be already merged by run_on_all_plugins 311 } 312 }*/ 313 ZLog::Write(LOGLEVEL_DEBUG, __METHOD__.'->retrieved '.count($ret)." Messages for type=$type, folder=$folder, app=$app ($id, $cutoffdate)");//.array2string($ret)); 314 return $ret; 315 } 316 317 /** 318 * convert UID to GlobalObjID for meeting requests 319 * 320 * @param string $uid iCal UID 321 * @return binary GlobalObjId 322 */ 323 public static function uid2globalObjId($uid) 324 { 325 $objid = base64_encode( 326 /* Bytes 1-16: */ "\x04\x00\x00\x00\x82\x00\xE0\x00\x74\xC5\xB7\x10\x1A\x82\xE0\x08". 327 /* Bytes 17-20: */ "\x00\x00\x00\x00". 328 /* Bytes 21-36: */ "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00". 329 /* Bytes 37-40: */ pack('V',13+bytes($uid)). // binary length + 13 for next line and terminating \x00 330 /* Bytes 41-52: */ 'vCal-Uid'."\x01\x00\x00\x00". 331 $uid."\x00"); 332 ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$uid') returning '$objid'"); 333 return $objid; 334 } 335 336 /** 337 * Extract UID from GlobalObjId 338 * 339 * @param string $objid 340 * @return string 341 */ 342 public static function globalObjId2uid($objid) 343 { 344 $uid = cut_bytes(base64_decode($objid), 52, -1); // -1 to cut off terminating \0x00 345 ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$objid') returning '$uid'"); 346 return $uid; 347 } 348 349 /** 350 * Get specified item from specified folder. 351 * 352 * @param string $folderid 353 * @param string $id 354 * @param ContentParameters $contentparameters parameters of the requested message (truncation, mimesupport etc) 355 * @return $messageobject|boolean false on error 356 */ 357 function GetMessage($folderid, $id, $contentparameters) 358 { 359 ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."($folderid, $id)"); 360 /*if ($id < 0) 361 { 362 $type = $folder = $app = null; 363 $this->splitID($folderid, $type, $folder, $app); 364 if ($app == 'mail' && $folder == 0) 365 { 366 367 return $this->run_on_all_plugins('GetMeetingRequest', 'return-first', $id, $contentparameters); 368 } 369 }*/ 370 return $this->run_on_plugin_by_id(__FUNCTION__, $folderid, $id, $contentparameters); 371 } 372 373 /** 374 * GetAttachmentData - may be MailSpecific 375 * Should return attachment data for the specified attachment. The passed attachment identifier is 376 * the exact string that is returned in the 'AttName' property of an SyncAttachment. So, you should 377 * encode any information you need to find the attachment in that 'attname' property. 378 * 379 * @param string $attname - should contain (folder)id 380 * @return true, prints the content of the attachment 381 */ 382 function GetAttachmentData($attname) 383 { 384 list($id) = explode(":", $attname); // split name as it is constructed that way FOLDERID:UID:PARTID 385 return $this->run_on_plugin_by_id(__FUNCTION__, $id, $attname); 386 } 387 388 /** 389 * ItemOperationsGetAttachmentData - may be MailSpecific 390 * Should return attachment data for the specified attachment. The passed attachment identifier is 391 * the exact string that is returned in the 'AttName' property of an SyncAttachment. So, you should 392 * encode any information you need to find the attachment in that 'attname' property. 393 * 394 * @param string $attname - should contain (folder)id 395 * @return SyncAirSyncBaseFileAttachment-object 396 */ 397 function ItemOperationsGetAttachmentData($attname) 398 { 399 list($id) = explode(":", $attname); // split name as it is constructed that way FOLDERID:UID:PARTID 400 return $this->run_on_plugin_by_id(__FUNCTION__, $id, $attname); 401 } 402 403 function ItemOperationsFetchMailbox($entryid, $bodypreference, $mimesupport = 0) { 404 ZLog::Write(LOGLEVEL_DEBUG, __METHOD__.__LINE__.'Entry:'.$entryid.', BodyPref:'.array2string( $bodypreference).', MimeSupport:'.array2string($mimesupport)); 405 list($folderid, $uid) = explode(":", $entryid); // split name as it is constructed that way FOLDERID:UID:PARTID 406 $type = $folder = $app = null; 407 $this->splitID($folderid, $type, $folder, $app); 408 //ZLog::Write(LOGLEVEL_DEBUG, __METHOD__.__LINE__."$folderid, $type, $folder, $app"); 409 if ($app == 'mail') 410 { // GetMessage($folderid, $id, $truncsize, $bodypreference=false, $optionbodypreference=false, $mimesupport = 0) 411 return $this->run_on_plugin_by_id('GetMessage', $folderid, $uid, $truncsize=($bodypreference[1]['TruncationSize']?$bodypreference[1]['TruncationSize']:500), $bodypreference, false, $mimesupport); 412 } 413 return false; 414 } 415 416 /** 417 * StatMessage should return message stats, analogous to the folder stats (StatFolder). Entries are: 418 * 'id' => Server unique identifier for the message. Again, try to keep this short (under 20 chars) 419 * 'flags' => simply '0' for unread, '1' for read 420 * 'mod' => modification signature. As soon as this signature changes, the item is assumed to be completely 421 * changed, and will be sent to the PDA as a whole. Normally you can use something like the modification 422 * time for this field, which will change as soon as the contents have changed. 423 * 424 * @param string $folderid 425 * @param int integer id of message 426 * @return array 427 */ 428 function StatMessage($folderid, $id) 429 { 430 if ($id < 0) 431 { 432 $type = $folder = $app = null; 433 $this->splitID($folderid, $type, $folder, $app); 434 if (($app == 'mail') && $folder == 0) 435 { 436 return $this->run_on_all_plugins('StatMeetingRequest',array(),$id); 437 } 438 } 439 return $this->run_on_plugin_by_id(__FUNCTION__, $folderid, $id); 440 } 441 442 /** 443 * Indicates if the backend has a ChangesSink. 444 * A sink is an active notification mechanism which does not need polling. 445 * The EGroupware backend simulates a sink by polling status information of the folder 446 * 447 * @access public 448 * @return boolean 449 */ 450 public function HasChangesSink() 451 { 452 $this->sinkfolders = array(); 453 $this->sinkstates = array(); 454 return true; 455 } 456 457 /** 458 * The folder should be considered by the sink. 459 * Folders which were not initialized should not result in a notification 460 * of IBacken->ChangesSink(). 461 * 462 * @param string $folderid 463 * 464 * @access public 465 * @return boolean false if found can not be found 466 */ 467 public function ChangesSinkInitialize($folderid) 468 { 469 //ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."($folderid)"); 470 471 $this->sinkfolders[] = $folderid; 472 473 return true; 474 } 475 476 /** 477 * The actual ChangesSink. 478 * For max. the $timeout value this method should block and if no changes 479 * are available return an empty array. 480 * If changes are available a list of folderids is expected. 481 * 482 * @param int $timeout max. amount of seconds to block 483 * 484 * @access public 485 * @return array 486 */ 487 public function ChangesSink($timeout = 30) 488 { 489 ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."($timeout)"); 490 $notifications = array(); 491 $stopat = time() + $timeout - 1; 492 493 while($stopat > time() && empty($notifications)) 494 { 495 foreach ($this->sinkfolders as $folderid) 496 { 497 $newstate = null; 498 $this->AlterPingChanges($folderid, $newstate); 499 500 if (!isset($this->sinkstates[$folderid])) 501 $this->sinkstates[$folderid] = $newstate; 502 503 if ($this->sinkstates[$folderid] != $newstate) 504 { 505 ZLog::Write(LOGLEVEL_DEBUG, "ChangeSink() found change for folderid=$folderid from ".array2string($this->sinkstates[$folderid])." to ".array2string($newstate)); 506 $notifications[] = $folderid; 507 $this->sinkstates[$folderid] = $newstate; 508 } 509 } 510 511 if (empty($notifications)) 512 { 513 $sleep_time = min($timeout, 30); 514 ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."($timeout) no changes, going to sleep($sleep_time)"); 515 sleep($sleep_time); 516 } 517 } 518 ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."($timeout) returning ".array2string($notifications)); 519 520 return $notifications; 521 } 522 523 /** 524 * Return a changes array 525 * 526 * if changes occurr default diff engine computes the actual changes 527 * 528 * We can NOT use run_on_plugin_by_id, because $syncstate is passed by reference! 529 * 530 * @param string $folderid 531 * @param string &$syncstate on call old syncstate, on return new syncstate 532 * @return array|boolean false if $folderid not found, array() if no changes or array(array("type" => "fakeChange")) 533 */ 534 function AlterPingChanges($folderid, &$syncstate) 535 { 536 $this->setup_plugins(); 537 538 $type = $folder = null; 539 $this->splitID($folderid, $type, $folder); 540 541 if (is_numeric($type)) 542 { 543 $type = 'mail'; 544 } 545 546 $ret = array(); // so unsupported or not enabled/installed backends return "no change" 547 if (isset($this->plugins[$type]) && method_exists($this->plugins[$type], __FUNCTION__)) 548 { 549 $this->device_wait_on_failure(__FUNCTION__); 550 try { 551 $ret = call_user_func_array(array($this->plugins[$type], __FUNCTION__), array($folderid, &$syncstate)); 552 } 553 catch(Exception $e) { 554 // log error and block device 555 $this->device_wait_on_failure(__FUNCTION__, $type, $e); 556 } 557 } 558 //ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$folderid','".array2string($syncstate)."') type=$type, folder=$folder returning ".array2string($ret)); 559 return $ret; 560 } 561 562 /** 563 * Indicates if the Backend supports folder statistics. 564 * 565 * @access public 566 * @return boolean 567 */ 568 public function HasFolderStats() 569 { 570 return true; 571 } 572 573 /** 574 * Returns a status indication of the folder. 575 * If there are changes in the folder, the returned value must change. 576 * The returned values are compared with '===' to determine if a folder needs synchronization or not. 577 * 578 * @param string $store the store where the folder resides 579 * @param string $folderid the folder id 580 * 581 * @access public 582 * @return string 583 */ 584 public function GetFolderStat($store, $folderid) 585 { 586 $syncstate = null; 587 $this->AlterPingChanges($folderid, $syncstate); 588 ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."($store, '$folderid') returning ".array2string($syncstate)); 589 return $syncstate; 590 } 591 592 /** 593 * Called when a message has been changed on the mobile. The new message must be saved to disk. 594 * The return value must be whatever would be returned from StatMessage() after the message has been saved. 595 * This way, the 'flags' and the 'mod' properties of the StatMessage() item may change via ChangeMessage(). 596 * This method will never be called on E-mail items as it's not 'possible' to change e-mail items. It's only 597 * possible to set them as 'read' or 'unread'. 598 * 599 * @param string $folderid id of the folder 600 * @param string $id id of the message 601 * @param SyncXXX $message the SyncObject containing a message 602 * @param ContentParameters $contentParameters 603 * 604 * @access public 605 * @return array same return value as StatMessage() 606 * @throws StatusException could throw specific SYNC_STATUS_* exceptions 607 */ 608 public function ChangeMessage($folderid, $id, $message, $contentParameters) 609 { 610 return $this->run_on_plugin_by_id(__FUNCTION__, $folderid, $id, $message, $contentParameters); 611 } 612 613 /** 614 * Called when the user moves an item on the PDA from one folder to another. Whatever is needed 615 * to move the message on disk has to be done here. After this call, StatMessage() and GetMessageList() 616 * should show the items to have a new parent. This means that it will disappear from GetMessageList() 617 * of the sourcefolder and the destination folder will show the new message 618 * 619 * @param string $folderid id of the source folder 620 * @param string $id id of the message 621 * @param string $newfolderid id of the destination folder 622 * @param ContentParameters $contentParameters 623 * 624 * @access public 625 * @return boolean status of the operation 626 * @throws StatusException could throw specific SYNC_MOVEITEMSSTATUS_* exceptions 627 */ 628 public function MoveMessage($folderid, $id, $newfolderid, $contentParameters) 629 { 630 return $this->run_on_plugin_by_id(__FUNCTION__, $folderid, $id, $newfolderid, $contentParameters); 631 } 632 633 /** 634 * Called when the user has requested to delete (really delete) a message. Usually 635 * this means just unlinking the file its in or somesuch. After this call has succeeded, a call to 636 * GetMessageList() should no longer list the message. If it does, the message will be re-sent to the mobile 637 * as it will be seen as a 'new' item. This means that if this method is not implemented, it's possible to 638 * delete messages on the PDA, but as soon as a sync is done, the item will be resynched to the mobile 639 * 640 * @param string $folderid id of the folder 641 * @param string $id id of the message 642 * @param ContentParameters $contentParameters 643 * 644 * @access public 645 * @return boolean status of the operation 646 * @throws StatusException could throw specific SYNC_STATUS_* exceptions 647 */ 648 public function DeleteMessage($folderid, $id, $contentParameters) 649 { 650 ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$folderid','".array2string($id)."') with Params ".array2string($contentParameters)); 651 if ($id < 0) 652 { 653 $type = $folder = $app = null; 654 $this->splitID($folderid, $type, $folder, $app); 655 if ($app == 'mail' && $folder == 0) 656 { 657 return $this->run_on_all_plugins('DeleteMeetingRequest', array(), $id, $contentParameters); 658 } 659 } 660 return $this->run_on_plugin_by_id(__FUNCTION__, $folderid, $id, $contentParameters); 661 } 662 663 /** 664 * Changes the 'read' flag of a message on disk. The $flags 665 * parameter can only be '1' (read) or '0' (unread). After a call to 666 * SetReadFlag(), GetMessageList() should return the message with the 667 * new 'flags' but should not modify the 'mod' parameter. If you do 668 * change 'mod', simply setting the message to 'read' on the mobile will trigger 669 * a full resync of the item from the server. 670 * 671 * @param string $folderid id of the folder 672 * @param string $id id of the message 673 * @param int $flags read flag of the message 674 * @param ContentParameters $contentParameters 675 * 676 * @access public 677 * @return boolean status of the operation 678 * @throws StatusException could throw specific SYNC_STATUS_* exceptions 679 */ 680 public function SetReadFlag($folderid, $id, $flags, $contentParameters) 681 { 682 return $this->run_on_plugin_by_id(__FUNCTION__, $folderid, $id, $flags, $contentParameters); 683 } 684 685 function ChangeMessageFlag($folderid, $id, $flag) 686 { 687 return $this->run_on_plugin_by_id(__FUNCTION__, $folderid, $id, $flag); 688 } 689 690 /** 691 * Applies settings to and gets informations from the device 692 * 693 * @param SyncObject $settings (SyncOOF or SyncUserInformation possible) 694 * 695 * @access public 696 * @return SyncObject $settings 697 */ 698 public function Settings($settings) 699 { 700 parent::Settings($settings); 701 702 if ($settings instanceof SyncUserInformation) 703 { 704 $settings->emailaddresses[] = $GLOBALS['egw_info']['user']['account_email']; 705 $settings->Status = SYNC_SETTINGSSTATUS_SUCCESS; 706 } 707 if ($settings instanceof SyncOOF) 708 { 709 // OOF (out of office) not yet supported via AS, but required parameter 710 $settings->oofstate = 0; 711 712 // seems bodytype is not allowed in outgoing SyncOOF message 713 // iOS 8.3 shows Oof Response as "Loading ..." instead of "Off" 714 unset($settings->bodytype); 715 716 // iOS console shows: received results for an unknown oof settings request 717 // setting following parameters do not help 718 //$settings->starttime = $settings->endtime = time(); 719 //$settings->oofmessage = array(); 720 721 } 722 723 // call all plugins with settings 724 $this->run_on_all_plugins(__FUNCTION__, array(), $settings); 725 726 return $settings; 727 } 728 729 /** 730 * Get provisioning data for a given device and user 731 * 732 * @param string $devid 733 * @param string $user 734 * @return array 735 */ 736 function getProvision($devid, $user) 737 { 738 $ret = $this->run_on_all_plugins(__FUNCTION__, array(), $devid, $user); 739 //error_log(__METHOD__."('$devid', '$user') returning ".array2string($ret)); 740 return $ret; 741 } 742 743 /** 744 * Checks if the sent policykey matches the latest policykey on the server 745 * 746 * Plugins either return array() to NOT change standard response or array('response' => value) 747 * 748 * @param string $policykey 749 * @param string $devid 750 * 751 * @return int status flag SYNC_PROVISION_STATUS_SUCCESS, SYNC_PROVISION_STATUS_POLKEYMISM (triggers provisioning) 752 */ 753 function CheckPolicy($policykey, $devid) 754 { 755 $response_in = array('response' => parent::CheckPolicy($policykey, $devid)); 756 757 // allow plugins to overwrite standard responses 758 $response = $this->run_on_all_plugins(__FUNCTION__, $response_in, $policykey, $devid); 759 760 //error_log(__METHOD__."('$policykey', '$devid') returning ".array2string($response['response'])); 761 return $response['response']; 762 } 763 764 /** 765 * Return a device wipe status 766 * 767 * Z-Push will send remote wipe request to client, if returned status is SYNC_PROVISION_RWSTATUS_PENDING or _WIPED 768 * 769 * Plugins either return array() to NOT change standard response or array('response' => value) 770 * 771 * @param string $user 772 * @param string $pass 773 * @param string $devid 774 * @return int SYNC_PROVISION_RWSTATUS_NA, SYNC_PROVISION_RWSTATUS_OK, SYNC_PROVISION_RWSTATUS_PENDING, SYNC_PROVISION_RWSTATUS_WIPED 775 */ 776 function getDeviceRWStatus($user, $pass, $devid) 777 { 778 $response_in = array('response' => false); 779 // allow plugins to overwrite standard responses 780 $response = $this->run_on_all_plugins(__FUNCTION__, $response_in, $user, $pass, $devid); 781 782 //error_log(__METHOD__."('$user', '$pass', '$devid') returning ".array2string($response['response'])); 783 return $response['response']; 784 } 785 786 /** 787 * Set a new rw status for the device 788 * 789 * Z-Push call this with SYNC_PROVISION_RWSTATUS_WIPED, after sending remote wipe command 790 * 791 * Plugins either return array() to NOT change standard response or array('response' => value) 792 * 793 * @param string $user 794 * @param string $pass 795 * @param string $devid 796 * @param int $status SYNC_PROVISION_RWSTATUS_OK, SYNC_PROVISION_RWSTATUS_PENDING, SYNC_PROVISION_RWSTATUS_WIPED 797 * 798 * @return boolean seems not to be used in Z-Push 799 */ 800 function setDeviceRWStatus($user, $pass, $devid, $status) 801 { 802 $response_in = array('response' => false); 803 // allow plugins to overwrite standard responses 804 $response = $this->run_on_all_plugins(__FUNCTION__, $response_in, $user, $pass, $devid, $status); 805 806 //error_log(__METHOD__."('$user', '$pass', '$devid', '$status') returning ".array2string($response['response'])); 807 return $response['response']; 808 } 809 810 /** 811 * Sends a message which is passed as rfc822. You basically can do two things 812 * 1) Send the message to an SMTP server as-is 813 * 2) Parse the message yourself, and send it some other way 814 * It is up to you whether you want to put the message in the sent items folder. If you 815 * want it in 'sent items', then the next sync on the 'sent items' folder should return 816 * the new message as any other new message in a folder. 817 * 818 * @param string $rfc822 mail 819 * @param array $smartdata =array() values for keys: 820 * 'task': 'forward', 'new', 'reply' 821 * 'itemid': id of message if it's an reply or forward 822 * 'folderid': folder 823 * 'replacemime': false = send as is, false = decode and recode for whatever reason ??? 824 * 'saveinsentitems': 1 or absent? 825 * @param boolean|double $protocolversion =false 826 * @return boolean true on success, false on error 827 * 828 * @see eg. BackendIMAP::SendMail() 829 * @todo implement either here or in fmail backend 830 * (maybe sending here and storing to sent folder in plugin, as sending is supposed to always work in EGroupware) 831 */ 832 function SendMail($rfc822, $smartdata=array(), $protocolversion = false) 833 { 834 $ret = $this->run_on_all_plugins(__FUNCTION__, 'return-first', $rfc822, $smartdata, $protocolversion); 835 ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$rfc822', ".array2string($smartdata).", $protocolversion) returning ".array2string($ret)); 836 return $ret; 837 } 838 839 /** 840 * Searches the GAL. 841 * 842 * @param string $searchquery string to be searched for 843 * @param string $searchrange specified searchrange 844 * @param SyncResolveRecipientsPicture $searchpicture limitations for picture 845 * 846 * @access public 847 * @return array search results 848 * @throws StatusException 849 */ 850 public function GetGALSearchResults($searchquery, $searchrange, $searchpicture) 851 { 852 ZLog::Write(LOGLEVEL_DEBUG, __METHOD__.__LINE__.':'.array2string(array('query'=>$searchquery, 'range'=>$searchrange))); 853 return $this->getSearchResults(array('query'=>$searchquery, 'range'=>$searchrange),'GAL'); 854 } 855 856 /** 857 * Searches for the emails on the server 858 * 859 * @param ContentParameter $cpo 860 * 861 * @return array 862 */ 863 public function GetMailboxSearchResults($cpo) 864 { 865 ZLog::Write(LOGLEVEL_DEBUG, __METHOD__.__LINE__.':'.array2string($cpo)); 866 return $this->getSearchResults($cpo,'MAILBOX'); 867 } 868 869 /** 870 * Returns a ISearchProvider implementation used for searches 871 * 872 * @access public 873 * @return object Implementation of ISearchProvider 874 */ 875 public function GetSearchProvider() 876 { 877 return $this; 878 } 879 880 /** 881 * Indicates if a search type is supported by this SearchProvider 882 * Currently only the type SEARCH_GAL (Global Address List) is implemented 883 * 884 * @param string $searchtype 885 * 886 * @access public 887 * @return boolean 888 */ 889 public function SupportsType($searchtype) 890 { 891 ZLog::Write(LOGLEVEL_DEBUG, __METHOD__.__LINE__.'='.array2string($searchtype)); 892 return ($searchtype == ISearchProvider::SEARCH_MAILBOX) || ($searchtype == ISearchProvider::SEARCH_GAL); 893 } 894 895 /** 896 * Terminates a search for a given PID 897 * 898 * @param int $pid 899 * 900 * @return boolean 901 */ 902 public function TerminateSearch($pid) 903 { 904 ZLog::Write(LOGLEVEL_DEBUG, __METHOD__.__LINE__.' PID:'.array2string($pid)); 905 return true; 906 } 907 908 /** 909 * Disconnects from the current search provider 910 * 911 * @access public 912 * @return boolean 913 */ 914 public function Disconnect() 915 { 916 return true; 917 } 918 919 /** 920 * Returns array of items which contain searched for information 921 * 922 * @param string $searchquery 923 * @param string $searchname 924 * 925 * @return array 926 */ 927 function getSearchResults($searchquery,$searchname) 928 { 929 ZLog::Write(LOGLEVEL_DEBUG, "EGW:getSearchResults : query: ". print_r($searchquery,true) . " : searchname : ". $searchname); 930 switch (strtoupper($searchname)) { 931 case 'GAL': 932 $rows = $this->run_on_all_plugins('getSearchResultsGAL',array(),$searchquery); 933 break; 934 case 'MAILBOX': 935 $rows = $this->run_on_all_plugins('getSearchResultsMailbox',array(),$searchquery); 936 break; 937 case 'DOCUMENTLIBRARY': 938 $rows = $this->run_on_all_plugins('getSearchDocumentLibrary',array(),$searchquery); 939 break; 940 default: 941 ZLog::Write(LOGLEVEL_DEBUG, __METHOD__." unknown searchname ". $searchname); 942 return NULL; 943 } 944 if (is_array($rows)) 945 { 946 $result = &$rows; 947 } 948 //error_log(__METHOD__."('$searchquery', '$searchname') returning ".count($result['rows']).' rows = '.array2string($result)); 949 return $result; 950 } 951 952/* 95304/28/11 21:55:28 [8923] POST cmd: MeetingResponse 95404/28/11 21:55:28 [8923] I <MeetingResponse:MeetingResponse> 95504/28/11 21:55:28 [8923] I <MeetingResponse:Request> 95604/28/11 21:55:28 [8923] I <MeetingResponse:UserResponse> 95704/28/11 21:55:28 [8923] I 1 95804/28/11 21:55:28 [8923] I </MeetingResponse:UserResponse> 95904/28/11 21:55:28 [8923] I <MeetingResponse:FolderId> 96004/28/11 21:55:28 [8923] I 101000000000 96104/28/11 21:55:28 [8923] I </MeetingResponse:FolderId> 96204/28/11 21:55:28 [8923] I <MeetingResponse:RequestId> 96304/28/11 21:55:28 [8923] I 99723 96404/28/11 21:55:28 [8923] I </MeetingResponse:RequestId> 96504/28/11 21:55:28 [8923] I </MeetingResponse:Request> 96604/28/11 21:55:28 [8923] I </MeetingResponse:MeetingResponse> 96704/28/11 21:55:28 [8923] activesync_backend::MeetingResponse('99723', '101000000000', '1', ) returning FALSE 96804/28/11 21:55:28 [8923] O <MeetingResponse:MeetingResponse> 96904/28/11 21:55:28 [8923] O <MeetingResponse:Result> 97004/28/11 21:55:28 [8923] O <MeetingResponse:RequestId> 97104/28/11 21:55:28 [8923] O 99723 97204/28/11 21:55:28 [8923] O </MeetingResponse:RequestId> 97304/28/11 21:55:28 [8923] O <MeetingResponse:Status> 97404/28/11 21:55:28 [8923] O 2 97504/28/11 21:55:28 [8923] O </MeetingResponse:Status> 97604/28/11 21:55:28 [8923] O </MeetingResponse:Result> 97704/28/11 21:55:28 [8923] O </MeetingResponse:MeetingResponse> 978*/ 979 /** 980 * 981 * @see BackendDiff::MeetingResponse() 982 * @param int $requestid uid of mail with meeting request 983 * @param string $folderid folder of meeting request mail 984 * @param int $response 1=accepted, 2=tentative, 3=decline 985 * @return boolean calendar-id on success, false on error 986 */ 987 function MeetingResponse($requestid, $folderid, $response) 988 { 989 $calendarid = $this->run_on_plugin_by_id(__FUNCTION__, $folderid, $requestid, $response); 990 991 ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$requestid', '$folderid', '$response') returning ".array2string($calendarid)); 992 return $calendarid; 993 } 994 995 /** 996 * Type ID for addressbook 997 * 998 * To work around a bug in Android / HTC the ID must not have leading "0" (as they get removed) 999 * 1000 * @var int 1001 */ 1002 const TYPE_ADDRESSBOOK = 0x1000; 1003 const TYPE_CALENDAR = 0x1001; 1004 const TYPE_INFOLOG = 0x1002; 1005 const TYPE_MAIL = 0x1010; 1006 1007 /** 1008 * Create a max. 32 hex letter ID, current 20 chars are used 1009 * 1010 * Currently only $folder supports negative numbers correctly on 64bit PHP systems 1011 * 1012 * @param int|string $type appname or integer mail account id 1013 * @param int $folder integer folder ID 1014 * @return string 1015 * @throws Api\Exception\WrongParameter 1016 */ 1017 public function createID($type,$folder) 1018 { 1019 // get a nummeric $type 1020 switch((string)($t=$type)) // we have to cast to string, as (0 == 'addressbook')===TRUE! 1021 { 1022 case 'addressbook': 1023 $type = self::TYPE_ADDRESSBOOK; 1024 break; 1025 case 'calendar': 1026 $type = self::TYPE_CALENDAR; 1027 break; 1028 case 'infolog': 1029 $type = self::TYPE_INFOLOG; 1030 break; 1031 case 'mail': 1032 $type = self::TYPE_MAIL; 1033 break; 1034 default: 1035 if (!is_numeric($type)) 1036 { 1037 throw new Api\Exception\WrongParameter("type='$type' is NOT nummeric!"); 1038 } 1039 $type += self::TYPE_MAIL; 1040 break; 1041 } 1042 1043 if (!is_numeric($folder)) 1044 { 1045 throw new Api\Exception\WrongParameter("folder='$folder' is NOT nummeric!"); 1046 } 1047 1048 $folder_hex = sprintf('%08X',$folder); 1049 // truncate negative number on a 64bit system to 8 hex digits = 32bit 1050 if (strlen($folder_hex) > 8) $folder_hex = substr($folder_hex,-8); 1051 $str = sprintf('%04X',$type).$folder_hex; 1052 1053 //ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$t','$folder') type=$type --> '$str'"); 1054 return $str; 1055 } 1056 1057 /** 1058 * Split an ID string into $app and $folder 1059 * 1060 * Currently only $folder supports negative numbers correctly on 64bit PHP systems 1061 * 1062 * @param string $str 1063 * @param string|int &$type on return appname or integer mail account ID 1064 * @param int &$folder on return integer folder ID 1065 * @param string &$app=null application of ID 1066 * @throws Api\Exception\WrongParameter 1067 */ 1068 public function splitID($str,&$type,&$folder,&$app=null) 1069 { 1070 $type = hexdec(substr($str,0,4)); 1071 $folder = hexdec(substr($str,4,8)); 1072 // convert 32bit negative numbers on a 64bit system to a 64bit negative number 1073 if ($folder > 0x7fffffff) $folder -= 0x100000000; 1074 1075 switch($type) 1076 { 1077 case self::TYPE_ADDRESSBOOK: 1078 $app = $type = 'addressbook'; 1079 break; 1080 case self::TYPE_CALENDAR: 1081 $app = $type = 'calendar'; 1082 break; 1083 case self::TYPE_INFOLOG: 1084 $app = $type = 'infolog'; 1085 break; 1086 default: 1087 if ($type < self::TYPE_MAIL) 1088 { 1089 throw new Api\Exception\WrongParameter("Unknown type='$type'!"); 1090 } 1091 $app = 'mail'; 1092 $type -= self::TYPE_MAIL; 1093 break; 1094 } 1095 // ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$str','$type','$folder')"); 1096 } 1097 1098 /** 1099 * Convert note to requested bodypreference format and truncate if requested 1100 * 1101 * @param string $note containing the plaintext message 1102 * @param array $bodypreference 1103 * @param SyncBaseBody $airsyncbasebody the airsyncbasebody object to send to the client 1104 * 1105 * @return string plain textbody for message or false 1106 */ 1107 public function note2messagenote($note, $bodypreference, SyncBaseBody $airsyncbasebody) 1108 { 1109 //error_log (__METHOD__."('$note', ".array2string($bodypreference).", ...)"); 1110 if ($bodypreference == false) 1111 { 1112 return $note; 1113 } 1114 else 1115 { 1116 if (isset($bodypreference[2])) 1117 { 1118 ZLog::Write(LOGLEVEL_DEBUG, "HTML Body"); 1119 $airsyncbasebody->type = 2; 1120 $html = '<html>'. 1121 '<head>'. 1122 '<meta name="Generator" content="eGroupware/Z-Push">'. 1123 '<meta http-equiv="Content-Type" content="text/html; charset=utf-8">'. 1124 '</head>'. 1125 '<body>'. 1126 // using <p> instead of <br/>, as W10mobile seems to have problems with it 1127 str_replace(array("\r\n", "\n", "\r"), "<p>", $note). 1128 '</body>'. 1129 '</html>'; 1130 if (isset($bodypreference[2]["TruncationSize"]) && strlen($html) > $bodypreference[2]["TruncationSize"]) 1131 { 1132 $html = utf8_truncate($html,$bodypreference[2]["TruncationSize"]); 1133 $airsyncbasebody->truncated = 1; 1134 } 1135 $airsyncbasebody->estimateddatasize = strlen($html); 1136 $airsyncbasebody->data = StringStreamWrapper::Open($html); 1137 } 1138 else 1139 { 1140 ZLog::Write(LOGLEVEL_DEBUG, "Plaintext Body"); 1141 $airsyncbasebody->type = 1; 1142 $plainnote = str_replace("\n","\r\n",str_replace("\r","",$note)); 1143 if(isset($bodypreference[1]["TruncationSize"]) && strlen($plainnote) > $bodypreference[1]["TruncationSize"]) 1144 { 1145 $plainnote = utf8_truncate($plainnote, $bodypreference[1]["TruncationSize"]); 1146 $airsyncbasebody->truncated = 1; 1147 } 1148 $airsyncbasebody->estimateddatasize = strlen($plainnote); 1149 $airsyncbasebody->data = StringStreamWrapper::Open((string)$plainnote !== '' ? $plainnote : ' '); 1150 } 1151 if ($airsyncbasebody->type != 3 && !isset($airsyncbasebody->data)) 1152 { 1153 $airsyncbasebody->data = StringStreamWrapper::Open(" "); 1154 } 1155 } 1156 } 1157 1158 /** 1159 * Convert received messagenote to egroupware plaintext note 1160 * 1161 * @param string $body the plain body received 1162 * @param string $rtf the rtf body data 1163 * @param SyncBaseBody $airsyncbasebody =null object received from client, or null if none received 1164 * 1165 * @return string plaintext for eGroupware 1166 */ 1167 public function messagenote2note($body, $rtf, SyncBaseBody $airsyncbasebody=null) 1168 { 1169 if (isset($airsyncbasebody) && is_resource($airsyncbasebody->data)) 1170 { 1171 switch($airsyncbasebody->type) 1172 { 1173 case '3': 1174 $rtf = stream_get_contents($airsyncbasebody->data); 1175 //error_log("Airsyncbase RTF Body"); 1176 break; 1177 1178 case '2': 1179 $body = Api\Mail\Html::convertHTMLToText(stream_get_contents($airsyncbasebody->data)); 1180 break; 1181 1182 case '1': 1183 $body = stream_get_contents($airsyncbasebody->data); 1184 //error_log("Airsyncbase Plain Body"); 1185 break; 1186 } 1187 } 1188 // Nokia MfE 2.9.158 sends contact notes with RTF and Body element. 1189 // The RTF is empty, the body contains the note therefore we need to unpack the rtf 1190 // to see if it is realy empty and in case not, take the appointment body. 1191 /*if (isset($message->rtf)) 1192 { 1193 error_log("RTF Body"); 1194 $rtf_body = new rtf (); 1195 $rtf_body->loadrtf(base64_decode($rtf)); 1196 $rtf_body->output("ascii"); 1197 $rtf_body->parse(); 1198 if (isset($body) && isset($rtf_body->out) && $rtf_body->out == "" && $body != "") 1199 { 1200 unset($rtf); 1201 } 1202 if($rtf_body->out <> "") $body=$rtf_body->out; 1203 }*/ 1204 ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."(body=".array2string($body).", rtf=".array2string($rtf).", airsyncbasebody=".array2string($airsyncbasebody).") returning ".array2string($body)); 1205 return $body; 1206 } 1207 1208 /** 1209 * Run and return settings from all plugins 1210 * 1211 * @param array|string $hook_data 1212 * @return array with settings from all plugins 1213 */ 1214 public function egw_settings($hook_data) 1215 { 1216 return $this->run_on_all_plugins('egw_settings',array(),$hook_data); 1217 } 1218 1219 /** 1220 * Run and return verify_settings from all plugins 1221 * 1222 * @param array|string $hook_data 1223 * @return array with error-messages from all plugins 1224 */ 1225 public function verify_settings($hook_data) 1226 { 1227 return $this->run_on_all_plugins('verify_settings', array(), $hook_data); 1228 } 1229 1230 /** 1231 * Returns the waste basket 1232 * 1233 * The waste basked is used when deleting items; if this function returns a valid folder ID, 1234 * then all deletes are handled as moves and are sent to the backend as a move. 1235 * If it returns FALSE, then deletes are handled as real deletes 1236 * 1237 * @access public 1238 * @return string 1239 */ 1240 public function GetWasteBasket() 1241 { 1242 //return $this->run_on_all_plugins(__FUNCTION__, 'return-first'); 1243 return false; 1244 } 1245 1246 /** 1247 * Deletes a folder 1248 * 1249 * @param string $id 1250 * @param string $parentid is normally false 1251 * 1252 * @access public 1253 * @return boolean status - false if e.g. does not exist 1254 * @throws StatusException could throw specific SYNC_FSSTATUS_* exceptions 1255 */ 1256 public function DeleteFolder($id, $parentid=false) 1257 { 1258 $ret = $this->run_on_plugin_by_id(__FUNCTION__, $id, $parentid); 1259 if (!$ret) ZLog::Write(LOGLEVEL_ERROR, __METHOD__." WARNING : something failed deleting a folder ($id), now informing the device that this has failed"); 1260 return $ret; 1261 } 1262 1263 /** 1264 * Plugins to use, filled by setup_plugins 1265 * 1266 * @var array 1267 */ 1268 private $plugins; 1269 1270 /** 1271 * Run a certain method on the plugin for the given id 1272 * 1273 * @param string $method 1274 * @param string $id will be first parameter to method 1275 * @param optional further parameters 1276 * @return mixed 1277 */ 1278 public function run_on_plugin_by_id($method,$id) 1279 { 1280 $this->setup_plugins(); 1281 1282 // check if device is still blocked 1283 $this->device_wait_on_failure($method); 1284 1285 $type = $folder = null; 1286 $this->splitID($id, $type, $folder); 1287 1288 if (is_numeric($type)) 1289 { 1290 $type = 'mail'; 1291 } 1292 $params = func_get_args(); 1293 array_shift($params); // remove $method 1294 1295 $ret = false; 1296 if (isset($this->plugins[$type]) && method_exists($this->plugins[$type], $method)) 1297 { 1298 try { 1299 //error_log($method.' called with Params:'.array2string($params)); 1300 $ret = call_user_func_array(array($this->plugins[$type], $method),$params); 1301 } 1302 catch(Exception $e) { 1303 // log error and block device 1304 $this->device_wait_on_failure($method, $type, $e); 1305 } 1306 } 1307 //error_log(__METHOD__."('$method','$id') type=$type, folder=$folder returning ".array2string($ret)); 1308 return $ret; 1309 } 1310 1311 /** 1312 * Run a certain method on all plugins 1313 * 1314 * @param string $method 1315 * @param mixed $agregate=array() if array given array_merge is used, otherwise += 1316 * or 'return-first' to return result from first matching plugin returning not null, false or '' result 1317 * @param optional parameters 1318 * @return mixed agregated result 1319 */ 1320 public function run_on_all_plugins($method,$agregate=array()) 1321 { 1322 $this->setup_plugins(); 1323 1324 // check if device is still blocked 1325 $this->device_wait_on_failure($method); 1326 1327 $params = func_get_args(); 1328 array_shift($params); array_shift($params); // remove $method+$agregate 1329 1330 foreach($this->plugins as $app => $plugin) 1331 { 1332 if (method_exists($plugin, $method)) 1333 { 1334 try { 1335 //ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."() calling ".get_class($plugin).'::'.$method); 1336 $result = call_user_func_array(array($plugin, $method),$params); 1337 //ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."() calling ".get_class($plugin).'::'.$method.' returning '.array2string($result)); 1338 } 1339 catch(Exception $e) { 1340 // log error and block device 1341 $this->device_wait_on_failure($method, $app, $e); 1342 } 1343 1344 if (is_array($agregate)) 1345 { 1346 $agregate = array_merge($agregate,$result); 1347 //error_log(__METHOD__."('$method', , ".array2string($params).") result plugin::$method=".array2string($result).' --> agregate='.array2string($agregate)); 1348 } 1349 elseif ($agregate === 'return-first') 1350 { 1351 if ($result) 1352 { 1353 $agregate = $result; 1354 break; 1355 } 1356 } 1357 else 1358 { 1359 //error_log(__METHOD__."('$method') agg:".array2string($agregate).' res:'.array2string($result)); 1360 $agregate += (is_bool($agregate)? (bool) $result:$result); 1361 } 1362 } 1363 } 1364 if ($agregate === 'return-first') $agregate = false; 1365 //error_log(__METHOD__."('$method') returning ".array2string($agregate)); 1366 return $agregate; 1367 } 1368 1369 /** 1370 * Instanciate all plugins the user has application rights for 1371 */ 1372 private function setup_plugins() 1373 { 1374 if (isset($this->plugins)) return; 1375 1376 $this->plugins = array(); 1377 if (isset($GLOBALS['egw_info']['user']['apps'])) $apps = array_keys($GLOBALS['egw_info']['user']['apps']); 1378 if (!isset($apps)) // happens during setup 1379 { 1380 $apps = array('addressbook', 'calendar', 'mail', 'infolog'/*, 'filemanager'*/); 1381 } 1382 // allow apps without user run-rights to hook into eSync 1383 if (($hook_data = Api\Hooks::process('esync_extra_apps', array(), true))) // true = no perms. check 1384 { 1385 foreach($hook_data as $app => $extra_apps) 1386 { 1387 if ($extra_apps) $apps = array_unique(array_merge($apps, (array)$extra_apps)); 1388 } 1389 } 1390 foreach($apps as $app) 1391 { 1392 if (strpos($app,'_')!==false) continue; 1393 $class = $app.'_zpush'; 1394 if (class_exists($class)) 1395 { 1396 $this->plugins[$app] = new $class($this); 1397 } 1398 } 1399 //error_log(__METHOD__."() hook_data=".array2string($hook_data).' returning '.array2string(array_keys($this->plugins))); 1400 } 1401 1402 /** 1403 * Name of log for blocked devices within instances files dir or null to not log blocking 1404 */ 1405 const BLOCKING_LOG = 'esync-blocking.log'; 1406 1407 /** 1408 * Check or set device failure mode: in failure mode we only return 503 Service unavailble 1409 * 1410 * Behavior on exceptions eg. connection failures (flags stored by user and device-ID): 1411 * a) if connections fails initialy: 1412 * we log time under "lastattempt" and initial blocking-time of self::$waitOnFailureDefault=30 as "howlong" and 1413 * send a "Retry-After: 30" header and a HTTP-Status of "503 Service Unavailable" 1414 * b) if clients attempts connection before lastattempt+howlong: 1415 * send a "Retry-After: <remaining-time>" header and a HTTP-Status of "503 Service Unavailable" 1416 * c) if connection fails again within twice the blocking time: 1417 * we double the blocking time up to maximum of $this->waitOnFailureLimit=7200=2h and 1418 * send a "Retry-After: 2*<blocking-time>" header and a HTTP-Status of "503 Service Unavailable" 1419 * 1420 * @link https://social.msdn.microsoft.com/Forums/en-US/3658aca8-36fd-4058-9d43-10f48c3f7d3b/what-does-commandfrequency-mean-with-respect-to-eas-throttling?forum=os_exchangeprotocols 1421 * @param string $method z-push backend method called 1422 * @param string $app application of pluging causing the failure 1423 * @param Exception $set =null 1424 */ 1425 private function device_wait_on_failure($method, $app=null, $set=null) 1426 { 1427 if (($dev_id = Request::GetDeviceID()) === false) 1428 { 1429 return; // no real request, or request not yet initialised 1430 } 1431 $waitOnFailure = Api\Cache::getInstance(__CLASS__, 'waitOnFailure-'.$GLOBALS['egw_info']['user']['account_lid'], function() 1432 { 1433 return array(); 1434 }); 1435 $deviceWaitOnFailure =& $waitOnFailure[$dev_id]; 1436 1437 // check if device is blocked 1438 if (!isset($set)) 1439 { 1440 // case b: client attempts new connection, before blocking-time is over --> return 503 Service Unavailable immediatly 1441 if ($deviceWaitOnFailure && $deviceWaitOnFailure['lastattempt']+$deviceWaitOnFailure['howlong'] > time()) 1442 { 1443 $keepwaiting = $deviceWaitOnFailure['lastattempt']+$deviceWaitOnFailure['howlong'] - time(); 1444 ZLog::Write(LOGLEVEL_ERROR, "$method() still blocking for an other $keepwaiting seconds for Instance=".$GLOBALS['egw_info']['user']['domain'].', User='.$GLOBALS['egw_info']['user']['account_lid'].', Device:'.Request::GetDeviceID()); 1445 if (self::BLOCKING_LOG) error_log(date('Y-m-d H:i:s ')."$method() still blocking for an other $keepwaiting seconds for Instance=".$GLOBALS['egw_info']['user']['domain'].', User='.$GLOBALS['egw_info']['user']['account_lid'].', Device:'.Request::GetDeviceID()."\n", 3, $GLOBALS['egw_info']['server']['files_dir'].'/'.self::BLOCKING_LOG); 1446 // let z-push know we want to terminate 1447 header("Retry-After: ".$keepwaiting); 1448 throw new HTTPReturnCodeException('Service Unavailable', 503); 1449 } 1450 return; // everything ok, device is not blocked 1451 } 1452 // block device because Exception happend in $method plugin for $app 1453 1454 // case a) initial failure: set lastattempt and howlong=self::$waitOnFailureDefault=30 1455 if (!$deviceWaitOnFailure || time() > $deviceWaitOnFailure['lastattempt']+2*$deviceWaitOnFailure['howlong']) 1456 { 1457 $deviceWaitOnFailure = array( 1458 'lastattempt' => time(), 1459 'howlong' => self::$waitOnFailureDefault, 1460 'app' => $app, 1461 ); 1462 } 1463 // case c) connection failed again: double waiting time up to max. of self::$waitOnFailureLimit=2h 1464 else 1465 { 1466 $deviceWaitOnFailure = array( 1467 'lastattempt' => time(), 1468 'howlong' => 2*$deviceWaitOnFailure['howlong'], 1469 'app' => $app, 1470 ); 1471 if ($deviceWaitOnFailure['howlong'] > self::$waitOnFailureLimit) 1472 { 1473 $deviceWaitOnFailure['howlong'] = self::$waitOnFailureLimit; 1474 } 1475 } 1476 Api\Cache::setInstance(__CLASS__, 'waitOnFailure-'.$GLOBALS['egw_info']['user']['account_lid'], $waitOnFailure); 1477 1478 ZLog::Write(LOGLEVEL_ERROR, "$method() Error happend in $app ".$set->getMessage()." blocking for $deviceWaitOnFailure[howlong] seconds for Instance=".$GLOBALS['egw_info']['user']['domain'].', User='.$GLOBALS['egw_info']['user']['account_lid'].', Device:'.Request::GetDeviceID()); 1479 if (self::BLOCKING_LOG) error_log(date('Y-m-d H:i:s ')."$method() Error happend in $app: ".$set->getMessage()." blocking for $deviceWaitOnFailure[howlong] seconds for Instance=".$GLOBALS['egw_info']['user']['domain'].', User='.$GLOBALS['egw_info']['user']['account_lid'].', Device:'.Request::GetDeviceID()."\n", 3, $GLOBALS['egw_info']['server']['files_dir'].'/'.self::BLOCKING_LOG); 1480 // let z-push know we want to terminate 1481 header("Retry-After: ".$deviceWaitOnFailure['howlong']); 1482 throw new HTTPReturnCodeException('Service Unavailable', 503, $set); 1483 } 1484 1485 /** 1486 * Indicates which AS version is supported by the backend. 1487 * By default AS version 2.5 (ASV_25) is returned (Z-Push 1 standard). 1488 * Subclasses can overwrite this method to set another AS version 1489 * 1490 * @access public 1491 * @return string AS version constant 1492 */ 1493 public function GetSupportedASVersion() 1494 { 1495 return ZPush::ASV_14; 1496 } 1497 1498 /** 1499 * Returns a IStateMachine implementation used to save states 1500 * The default StateMachine should be used here, so, false is fine 1501 * 1502 * @access public 1503 * @return boolean/object 1504 */ 1505 public function GetStateMachine() 1506 { 1507 return new activesync_statemachine($this); 1508 } 1509} 1510