Overview

Packages

  • ActiveSync
  • None

Classes

  • Horde_ActiveSync
  • Horde_ActiveSync_Connector_Exporter
  • Horde_ActiveSync_Connector_Importer
  • Horde_ActiveSync_Driver_Base
  • Horde_ActiveSync_Exception
  • Horde_ActiveSync_Exception_InvalidRequest
  • Horde_ActiveSync_Exception_StateGone
  • Horde_ActiveSync_Message_Base
  • Horde_ActiveSync_Request_Base
  • Horde_ActiveSync_Request_FolderCreate
  • Horde_ActiveSync_Request_FolderSync
  • Horde_ActiveSync_Request_GetHierarchy
  • Horde_ActiveSync_Request_GetItemEstimate
  • Horde_ActiveSync_Request_MeetingResponse
  • Horde_ActiveSync_Request_MoveItems
  • Horde_ActiveSync_Request_Notify
  • Horde_ActiveSync_Request_Ping
  • Horde_ActiveSync_Request_Provision
  • Horde_ActiveSync_Request_Search
  • Horde_ActiveSync_Request_SendMail
  • Horde_ActiveSync_Request_SmartForward
  • Horde_ActiveSync_Request_SmartReply
  • Horde_ActiveSync_Request_Sync
  • Horde_ActiveSync_State_File
  • Horde_ActiveSync_Sync
  • Horde_ActiveSync_Wbxml
  • Horde_ActiveSync_Wbxml_Decoder
  • Horde_ActiveSync_Wbxml_Encoder
  • Overview
  • Package
  • Class
  • Tree
  1: <?php
  2: /**
  3:  * ActiveSync Handler for PING requests
  4:  *
  5:  * Copyright 2009-2012 Horde LLC (http://www.horde.org/)
  6:  *
  7:  * @author Michael J. Rubinsky <mrubinsk@horde.org>
  8:  * @package ActiveSync
  9:  */
 10: /**
 11:  * Zarafa Deutschland GmbH, www.zarafaserver.de
 12:  * This file is distributed under GPL-2.0.
 13:  * Consult COPYING file for details
 14:  */
 15: class Horde_ActiveSync_Request_Ping extends Horde_ActiveSync_Request_Base
 16: {
 17:     const STATUS_NOCHANGES = 1;
 18:     const STATUS_NEEDSYNC = 2;
 19:     const STATUS_MISSING = 3;
 20:     const STATUS_PROTERROR = 4;
 21:     const STATUS_HBOUTOFBOUNDS = 5;
 22: 
 23:     // Requested more then the max folders (TODO)
 24:     const STATUS_MAXFOLDERS = 6;
 25: 
 26:     // Folder sync is required, hierarchy out of date.
 27:     const STATUS_FOLDERSYNCREQD = 7;
 28:     const STATUS_SERVERERROR = 8;
 29: 
 30:     // Ping
 31:     const PING = 'Ping:Ping';
 32:     const STATUS = 'Ping:Status';
 33:     const HEARTBEATINTERVAL =  'Ping:HeartbeatInterval';
 34:     const FOLDERS =  'Ping:Folders';
 35:     const FOLDER =  'Ping:Folder';
 36:     const SERVERENTRYID =  'Ping:ServerEntryId';
 37:     const FOLDERTYPE =  'Ping:FolderType';
 38: 
 39:     protected $_ping_settings;
 40: 
 41:     protected function _checkHeartbeat($lifetime)
 42:     {
 43:         if (!empty($this->_ping_settings['forcedheartbeat'])) {
 44:             return $this->_ping_settings['forcedheartbeat'];
 45:         }
 46:         if ($lifetime !== 0 && $lifetime < $this->_ping_settings['heartbeatmin']) {
 47:             $this->_statusCode = self::STATUS_HBOUTOFBOUNDS;
 48:             $lifetime = $this->_ping_settings['heartbeatmin'];
 49:             $this->_state->setHeartbeatInterval($lifetime);
 50:         } elseif ($lifetime > $this->_ping_settings['heartbeatmax']) {
 51:             $this->_statusCode = self::STATUS_HBOUTOFBOUNDS;
 52:             $lifetime = $this->_ping_settings['heartbeatmax'];
 53:             $this->_state->setHeartbeatInterval($lifetime);
 54:         }
 55: 
 56:         return $lifetime;
 57:     }
 58: 
 59:     /**
 60:      * Handle a PING command from the PIM. Ping is sent periodically by the PIM
 61:      * to tell the server what folders we are interested in monitoring for
 62:      * changes. If no changes are detected by the server during the 'heartbeat'
 63:      * interval, the server sends back a status of 1 to indicate heartbeat
 64:      * expired and the client should re-issue the PING command. If a change
 65:      * has been found, the client is sent a 2 status and should then issue a
 66:      * SYNC command.
 67:      *
 68:      * @return boolean
 69:      */
 70:     public function handle()
 71:     {
 72:         $now = time();
 73:         parent::handle();
 74: 
 75:         $this->_logger->info('[' . $this->_device->id . '] PING received at timestamp: ' . $now . '.');
 76: 
 77:         // Get the settings for the server
 78:         $this->_ping_settings = $this->_driver->getHeartbeatConfig();
 79:         $timeout = $this->_ping_settings['waitinterval'];
 80: 
 81:         // Glass half full kinda guy... */
 82:         $this->_statusCode = self::STATUS_NOCHANGES;
 83: 
 84:         // Initialize the state machine
 85:         $this->_state = &$this->_driver->getStateObject();
 86:         $this->_state->loadDeviceInfo($this->_device->id, $this->_driver->getUser());
 87: 
 88:         // See if we have an existing PING state. Need to do this here, before
 89:         // we read in the PING request since the PING request is allowed to omit
 90:         // sections if they have been sent previously
 91:         $collections = array_values($this->_state->initPingState($this->_device));
 92:         $lifetime = $this->_checkHeartbeat($this->_state->getHeartbeatInterval());
 93: 
 94:         // Build the $collections array if we receive request from PIM
 95:         if ($this->_decoder->getElementStartTag(self::PING)) {
 96:             if ($this->_decoder->getElementStartTag(self::HEARTBEATINTERVAL)) {
 97:                 $lifetime = $this->_checkHeartbeat($this->_decoder->getElementContent());
 98:                 $this->_decoder->getElementEndTag();
 99:             }
100:             if ($lifetime == 0) {
101:                 $lifetime = $this->_ping_settings['heartbeatdefault'];
102:             }
103:             $this->_state->setHeartbeatInterval($lifetime);
104: 
105:             if ($this->_decoder->getElementStartTag(self::FOLDERS)) {
106:                 $collections = array();
107:                 while ($this->_decoder->getElementStartTag(self::FOLDER)) {
108:                     $collection = array();
109:                     if ($this->_decoder->getElementStartTag(self::SERVERENTRYID)) {
110:                         $collection['id'] = $this->_decoder->getElementContent();
111:                         $this->_decoder->getElementEndTag();
112:                     }
113:                     if ($this->_decoder->getElementStartTag(self::FOLDERTYPE)) {
114:                         $collection['class'] = $this->_decoder->getElementContent();
115:                         $this->_decoder->getElementEndTag();
116:                     }
117: 
118:                     $this->_decoder->getElementEndTag();
119:                     // Ensure we only PING each collection once
120:                     $collections = array_merge($collections, array($collection['id'] => $collection));
121:                 }
122:                 $collections = array_values($collections);
123: 
124:                 if (!$this->_decoder->getElementEndTag()) {
125:                     $this->_statusCode = self::STATUS_PROTERROR;
126:                     return false;
127:                 }
128:             }
129: 
130:             if (!$this->_decoder->getElementEndTag()) {
131:                 $this->_statusCode = self::STATUS_PROTERROR;
132:                 return false;
133:             }
134: 
135:             $this->_state->addPingCollections($collections);
136:         } else {
137:             $this->_logger->debug(sprintf('Reusing PING state: %s', print_r($collections, true)));
138:         }
139: 
140:         $changes = array();
141:         $dataavailable = false;
142: 
143:         // Start waiting for changes, but only if we don't have any errors
144:         if ($this->_statusCode == self::STATUS_NOCHANGES) {
145:             $this->_logger->info(
146:                 sprintf('[%s] Waiting for changes (heartbeat interval: %d)',
147:                         $this->_device->id,
148:                         $lifetime));
149:             $expire = $now + $lifetime;
150:             while (time() <= $expire) {
151:                 // Check the remote wipe status and request a foldersync if
152:                 // we want the device wiped.
153:                 if ($this->_provisioning === true) {
154:                     $rwstatus = $this->_state->getDeviceRWStatus($this->_device->id);
155:                     if ($rwstatus == Horde_ActiveSync::RWSTATUS_PENDING || $rwstatus == Horde_ActiveSync::RWSTATUS_WIPED) {
156:                         $this->_statusCode = self::STATUS_FOLDERSYNCREQD;
157:                         break;
158:                     }
159:                 }
160: 
161:                 if (count($collections) == 0) {
162:                     $this->_logger->err('0 collections');
163:                     $this->_statusCode = self::STATUS_MISSING;
164:                     break;
165:                 }
166: 
167:                 for ($i = 0; $i < count($collections); $i++) {
168:                     $collection = $collections[$i];
169:                     $collection['synckey'] = $this->_device->id;
170:                     $sync = $this->_driver->getSyncObject();
171:                     try {
172:                         $this->_state->loadPingCollectionState($collection);
173:                     } catch (Horde_ActiveSync_Exception_InvalidRequest $e) {
174:                         // @TODO: I *love* standards that nobody follows. This
175:                         // really should throw an exception and return a HTTP 400
176:                         // response since this is explicitly forbidden by the
177:                         // specification. Some clients, e.g., TouchDown, send
178:                         // a PING in place of the initial SYNC. But sending the
179:                         // 400 causes TD to disable push entirely. Instead,
180:                         // cause the PING to terminate early and hope we have
181:                         // a SYNC next time it's pinged. We also use continue
182:                         // here instead of break to make sure we give all
183:                         // collections a change to report changes before we fail
184:                         $this->_logger->err('PING terminating: ' . $e->getMessage());
185:                         $expire = time();
186:                         continue;
187:                     } catch (Horde_ActiveSync_Exception_StateGone $e) {
188:                         $this->_logger->err('PING terminating, forcing a SYNC: ' . $e->getMessage());
189:                         $this->_statusCode = self::STATUS_NEEDSYNC;
190:                         $dataavailable = true;
191:                         $changes[$collection['id']] = 1;
192:                         continue;
193:                     } catch (Horde_ActiveSync_Exception $e) {
194:                         $this->_logger->err('PING terminating: ' . $e->getMessage());
195:                         $this->_statusCode = self::STATUS_SERVERERROR;
196:                         break;
197:                     }
198:                     try {
199:                         $sync->init($this->_state, null, $collection);
200:                     } catch (Horde_ActiveSync_Exception $e) {
201:                         /* Stop ping if exporter cannot be configured */
202:                         $this->_logger->err('Ping error: Exporter can not be configured. ' . $e->getMessage() . ' Waiting 30 seconds before ping is retried.');
203:                         sleep(30);
204:                         break;
205:                     }
206: 
207:                     $changecount = $sync->GetChangeCount();
208:                     if ($changecount > 0) {
209:                         $dataavailable = true;
210:                         $changes[$collection['id']] = $changecount;
211:                         $this->_statusCode = self::STATUS_NEEDSYNC;
212:                     }
213: 
214:                     // Update the state, but don't bother with the backend since
215:                     // we are not updating any data.
216:                     while (is_array($sync->syncronize(Horde_ActiveSync::BACKEND_DISCARD_DATA)));
217:                 }
218: 
219:                 if ($dataavailable) {
220:                     $this->_logger->info('[' . $this->_device->id . '] Changes available');
221:                     break;
222:                 }
223: 
224:                 sleep($timeout);
225:             }
226:         }
227: 
228:         // Prepare for response
229:         $this->_logger->info('[' . $this->_device->id . '] Sending response for PING.');
230:         $this->_encoder->StartWBXML();
231: 
232:         $this->_encoder->startTag(self::PING);
233: 
234:         $this->_encoder->startTag(self::STATUS);
235:         $this->_encoder->content($this->_statusCode);
236:         $this->_encoder->endTag();
237: 
238:         if ($this->_statusCode == self::STATUS_HBOUTOFBOUNDS) {
239:             $this->_encoder->startTag(self::HEARTBEATINTERVAL);
240:             $this->_encoder->content($lifetime);
241:             $this->_encoder->endTag();
242:         } else {
243:             $this->_encoder->startTag(self::FOLDERS);
244:             foreach ($collections as $collection) {
245:                 if (isset($changes[$collection['id']])) {
246:                     $this->_encoder->startTag(self::FOLDER);
247:                     $this->_encoder->content($collection['id']);
248:                     $this->_encoder->endTag();
249:                 }
250:             }
251:             $this->_encoder->endTag();
252:         }
253:         $this->_encoder->endTag();
254: 
255:         $this->_state->savePingState();
256: 
257:         return true;
258:     }
259: 
260: }
261: 
API documentation generated by ApiGen