/** * Import a message change from the wbxml stream * * @param string|boolean $id A server message id or * false if a new message. * @param Horde_ActiveSync_Message_Base $message A message object * @param Horde_ActiveSync_Device $device A device descriptor * @param integer $clientid Client id sent from PIM * on message addition. * @param string $class The collection class (only needed for SMS). * @since 2.6.0 * * @todo Revisit passing $class for SMS. Probably pass class in the * const'r. * * @return string|array|boolean The server message id, an array containing * the serverid and failure code, or false */ public function importMessageChange($id, Horde_ActiveSync_Message_Base $message, Horde_ActiveSync_Device $device, $clientid, $class = null) { // Don't support SMS, but can't tell client that. Send back a phoney // UID for any imported SMS objects. if ($class == Horde_ActiveSync::CLASS_SMS || strpos($id, 'IGNORESMS_') === 0) { return 'IGNORESMS_' . $clientid; } // Changing an existing object if ($id && $this->_flags == Horde_ActiveSync::CONFLICT_OVERWRITE_PIM) { $conflict = $this->_isConflict(Horde_ActiveSync::CHANGE_TYPE_CHANGE, $this->_folderId, $id); if ($conflict) { $this->_logger->notice(sprintf('[%s] Conflict when updating %s, will overwrite client version on next sync.', $this->_procid, $id)); return array($id, Horde_ActiveSync_Request_Sync::STATUS_CONFLICT); } } elseif (!$id && ($uid = $this->_state->isDuplicatePIMAddition($clientid))) { // Already saw this addition, but PIM never received UID $this->_logger->notice(sprintf('[%s] Duplicate addition for %s', $this->_procid, $uid)); return $uid; } // Tell the backend about the change if (!($stat = $this->_as->driver->changeMessage($this->_folderId, $id, $message, $device))) { $this->_logger->err(sprintf('[%s] Change message failed when updating %s', $this->_procid, $id)); return $id ? array($id, Horde_ActiveSync_Request_Sync::STATUS_NOTFOUND) : array(false, Horde_ActiveSync_Request_Sync::STATUS_SERVERERROR); } $stat['serverid'] = $this->_folderId; // Record the state of the message, but only if we aren't updating // categories. @todo This should be fixed, but for now we can't // differentiate between different flag changes. Note that categories // only exists for email changes so for non email this will still // work as before. if (!array_key_exists('categories', $stat) || $stat['categories'] === false) { $this->_state->updateState($message instanceof Horde_ActiveSync_Message_Mail ? Horde_ActiveSync::CHANGE_TYPE_FLAGS : Horde_ActiveSync::CHANGE_TYPE_CHANGE, $stat, Horde_ActiveSync::CHANGE_ORIGIN_PIM, $this->_as->driver->getUser(), $clientid); } return $stat['id']; }
/** * Import a message change from the wbxml stream * * @param string|boolean $id A server message id or * false if a new message. * @param Horde_ActiveSync_Message_Base $message A message object * @param Horde_ActiveSync_Device $device A device descriptor * @param integer $clientid Client id sent from client. * on message addition. * @param string $class The collection class - needed for SMS since the * actual serverid will be for an email folder. * @since 2.6.0 * @param string $synckey The synckey currently being processed when * processing a SYNC_MODIFY command. * @since 2.31.0 * * @todo Revisit passing $class for SMS. Probably pass class in the * const'r. * * @return array|boolean A stat array, or an array containing the 'error' * key on error, or false on duplicate addition. */ public function importMessageChange($id, Horde_ActiveSync_Message_Base $message, Horde_ActiveSync_Device $device, $clientid, $class = false, $synckey = false) { // Don't support SMS, but can't tell client that. Send back a phoney // UID for any imported SMS objects. if ($class == Horde_ActiveSync::CLASS_SMS || strpos($id, 'IGNORESMS_') === 0) { return 'IGNORESMS_' . $clientid; } // Changing an existing object if ($id && $this->_flags == Horde_ActiveSync::CONFLICT_OVERWRITE_PIM) { // This is complicated by the fact that in EAS 16.0, clients // will send a CHANGE for adding/editing an exception along with // a seperate change with the entire appointment - even if nothing // else has changed. This leads to conficts (since the appointment // is marked as changed after the first edit). Sniff that out here // and prevent the conflict check for those messages. if (!$message instanceof Horde_ActiveSync_Message_Appointment && $this->_state->isDuplicatePIMChange($id, $synckey)) { $conflict = $this->_isConflict(Horde_ActiveSync::CHANGE_TYPE_CHANGE, $this->_folderId, $id); if ($conflict) { $this->_logger->notice(sprintf('[%s] Conflict when updating %s, will overwrite client version on next sync.', $this->_procid, $id)); return array($id, 'error' => array(Horde_ActiveSync_Request_Sync::STATUS_CONFLICT)); } } } elseif (!$id && ($uid = $this->_state->isDuplicatePIMAddition($clientid))) { // Already saw this addition, but client never received UID $this->_logger->notice(sprintf('[%s] Duplicate addition for %s', $this->_procid, $uid)); return $uid; } // Set the supported/ghosted data if this is a SYNC_MODIFY. if ($id && !empty($device->supported[$class])) { $message->setSupported($device->supported[$class]); } // Tell the backend about the change if (!($stat = $this->_as->driver->changeMessage($this->_folderId, $id, $message, $device))) { $this->_logger->err(sprintf('[%s] Change message failed when updating %s', $this->_procid, $id)); return $id ? array(0 => $id, 'error' => Horde_ActiveSync_Request_Sync::STATUS_NOTFOUND) : array(0 => false, 'error' => Horde_ActiveSync_Request_Sync::STATUS_SERVERERROR); } $stat['serverid'] = $this->_folderId; // Record the state of the message. // Email messages are only changed if they are Drafts or if we are // updating flags. // When CHANGING a draft message, we are actually deleting the old one // and replacing it with a message (since we can't edit an existing // IMAP message while keeping the UID the same). So, do not call // updateState() for these messages since we don't want to ignore // this as a PIM sourced change - the change will be caught during // normal ping/sync cycle. if ($message instanceof Horde_ActiveSync_Message_Mail) { if (!empty($message->airsyncbasebody) && !empty($id)) { // Changing an existing Draft mail. return $stat; } // Either a flag change, or adding a new Draft mail. $changeType = !empty($message->airsyncbasebody) ? Horde_ActiveSync::CHANGE_TYPE_DRAFT : Horde_ActiveSync::CHANGE_TYPE_FLAGS; } else { $changeType = Horde_ActiveSync::CHANGE_TYPE_CHANGE; } $this->_state->updateState($changeType, $stat, Horde_ActiveSync::CHANGE_ORIGIN_PIM, $this->_as->driver->getUser(), $clientid); return $stat; }
/** * Load and initialize the sync state * * @param array $collection The collection array for the collection, if * a FOLDERSYNC, pass an empty array. * @param string $syncKey The synckey of the state to load. If empty will * force a reset of the state for the class * specified in $id * @param string $type The type of state a * Horde_ActiveSync::REQUEST_TYPE constant. * @param string $id The folder id this state represents. If empty * assumed to be a foldersync state. * * @throws Horde_ActiveSync_Exception, Horde_ActiveSync_Exception_StateGone */ public function loadState(array $collection, $syncKey, $type = null, $id = null) { // Initialize the local members. $this->_collection = $collection; $this->_changes = null; $this->_type = $type; // If this is a FOLDERSYNC, mock the device id. if ($type == Horde_ActiveSync::REQUEST_TYPE_FOLDERSYNC && empty($id)) { $id = Horde_ActiveSync::REQUEST_TYPE_FOLDERSYNC; } // synckey == 0 is an initial sync or reset. if (empty($syncKey)) { $this->_logger->notice(sprintf('[%s] %s::loadState: clearing folder state.', $this->_procid, __CLASS__)); if ($type == Horde_ActiveSync::REQUEST_TYPE_FOLDERSYNC) { $this->_folder = array(); } else { // Create a new folder object. $this->_folder = $this->_collection['class'] == Horde_ActiveSync::CLASS_EMAIL ? new Horde_ActiveSync_Folder_Imap($this->_collection['serverid'], Horde_ActiveSync::CLASS_EMAIL) : ($this->_collection['serverid'] == 'RI' ? new Horde_ActiveSync_Folder_RI('RI', 'RI') : new Horde_ActiveSync_Folder_Collection($this->_collection['serverid'], $this->_collection['class'])); } $this->_syncKey = '0'; $this->_resetDeviceState($id); return; } $this->_logger->info(sprintf('[%s] Loading state for synckey %s', $this->_procid, $syncKey)); // Check if synckey is allowed if (!preg_match('/^\\{([0-9A-Za-z-]+)\\}([0-9]+)$/', $syncKey, $matches)) { throw new Horde_ActiveSync_Exception('Invalid sync key'); } $this->_syncKey = $syncKey; // Cleanup older syncstates $this->_gc($syncKey); // Load the state $this->_loadState($type); }
/** * Authenticate to the backend. * * @param Horde_ActiveSync_Credentials $credentials The credentials object. * * @return boolean True on successful authentication to the backend. * @throws Horde_ActiveSync_Exception */ public function authenticate(Horde_ActiveSync_Credentials $credentials) { if (!$credentials->username) { // No provided username or Authorization header. self::$_logger->notice(sprintf('[%s] Client did not provide authentication data.', $this->_procid)); return false; } $user = $this->_driver->getUsernameFromEmail($credentials->username); $pos = strrpos($user, '\\'); if ($pos !== false) { $domain = substr($user, 0, $pos); $user = substr($user, $pos + 1); } else { $domain = null; } // Authenticate if ($result = $this->_driver->authenticate($user, $credentials->password, $domain)) { if ($result === self::AUTH_REASON_USER_DENIED) { $this->_globalError = Horde_ActiveSync_Status::SYNC_NOT_ALLOWED; } elseif ($result === self::AUTH_REASON_DEVICE_DENIED) { $this->_globalError = Horde_ActiveSync_Status::DEVICE_BLOCKED_FOR_USER; } elseif ($result !== true) { $this->_globalError = Horde_ActiveSync_Status::DENIED; } } else { return false; } if (!$this->_driver->setup($user)) { return false; } return true; }
/** * Poll the backend for changes. * * @param integer $heartbeat The heartbeat lifetime to wait for changes. * @param integer $interval The wait interval between poll iterations. * @param array $options An options array containing any of: * - pingable: (boolean) Only poll collections with the pingable flag set. * DEFAULT: false * * @return boolean|integer True if changes were detected in any of the * collections, false if no changes detected * or a status code if failed. */ public function pollForChanges($heartbeat, $interval, array $options = array()) { $dataavailable = false; $started = time(); $until = $started + $heartbeat; $this->_logger->info(sprintf('Waiting for changes for %s seconds', $heartbeat)); // If pinging, make sure we have pingable collections. Note we can't // filter on them here because the collections might change during the // loop below. if (!empty($options['pingable']) && !$this->havePingableCollections()) { $this->_logger->err('No pingable collections.'); return self::COLLECTION_ERR_SERVER; } // Need to update AND SAVE the timestamp for race conditions to be // detected. $this->lasthbsyncstarted = $started; $this->save(); // We only check for remote wipe request once every 5 iterations to // save on DB load since we must reload the device's state each time. $rw_check_countdown = 5; while (($now = time()) < $until) { // Try not to go over the heartbeat interval. if ($until - $now < $interval) { $interval = $until - $now; } // See if another process has altered the sync_cache. if ($this->checkStaleRequest()) { return self::COLLECTION_ERR_STALE; } // Make sure the collections are still there (there might have been // an error in refreshing them from the cache). Ideally this should // NEVER happen. if (!count($this->_collections)) { $this->_logger->err('NO COLLECTIONS! This should not happen!'); return self::COLLECTION_ERR_SERVER; } // Check for WIPE request once every 5 iterations to balance between // performance and speed of catching a remote wipe request. if ($rw_check_countdown-- == 0) { $rw_check_countdown = 5; if ($this->_as->provisioning != Horde_ActiveSync::PROVISIONING_NONE) { $rwstatus = $this->_as->state->getDeviceRWStatus($this->_as->device->id, true); if ($rwstatus == Horde_ActiveSync::RWSTATUS_PENDING || $rwstatus == Horde_ActiveSync::RWSTATUS_WIPED) { return self::COLLECTION_ERR_FOLDERSYNC_REQUIRED; } } } // Check each collection we are interested in. foreach ($this->_collections as $id => $collection) { // Initialize the collection's state data in the state handler. try { $this->initCollectionState($collection, true); } catch (Horde_ActiveSync_Exception_StateGone $e) { $this->_logger->notice(sprintf('[%s] State not found for %s. Continuing.', $this->_procid, $id)); if (!empty($options['pingable'])) { return self::COLLECTION_ERR_PING_NEED_FULL; } $dataavailable = true; $this->setGetChangesFlag($id); continue; } catch (Horde_ActiveSync_Exception_InvalidRequest $e) { // Thrown when state is unable to be initialized because the // collection has not yet been synched, but was requested to // be pinged. $this->_logger->err(sprintf('[%s] Unable to initialize state for %s. Ignoring during pollForChanges: %s.', $this->_procid, $id, $e->getMessage())); continue; } catch (Horde_ActiveSync_Exception_FolderGone $e) { $this->_logger->warn('Folder gone for collection ' . $collection['id']); return self::COLLECTION_ERR_FOLDERSYNC_REQUIRED; } catch (Horde_ActiveSync_Exception $e) { $this->_logger->err('Error loading state: ' . $e->getMessage()); $this->_as->state->loadState(array(), null, Horde_ActiveSync::REQUEST_TYPE_SYNC, $id); $this->setGetChangesFlag($id); $dataavailable = true; continue; } if (!empty($options['pingable']) && !$this->_cache->collectionIsPingable($id)) { $this->_logger->notice(sprintf('[%s] Skipping %s because it is not PINGable.', $this->_procid, $id)); continue; } try { if ($cnt = $this->getCollectionChangeCount(true)) { $dataavailable = true; $this->setGetChangesFlag($id); if (!empty($options['pingable'])) { $this->_cache->setPingChangeFlag($id); } } else { try { $this->_as->state->updateSyncStamp(); } catch (Horde_ActiveSync_Exception $e) { $this->_logger->err($e->getMessage()); } } } catch (Horde_ActiveSync_Exception_StaleState $e) { $this->_logger->notice(sprintf('[%s] SYNC terminating and force-clearing device state: %s', $this->_procid, $e->getMessage())); $this->_as->state->loadState(array(), null, Horde_ActiveSync::REQUEST_TYPE_SYNC, $id); $this->setGetChangesFlag($id); $dataavailable = true; } catch (Horde_ActiveSync_Exception_FolderGone $e) { $this->_logger->notice(sprintf('[%s] SYNC terminating: %s', $this->_procid, $e->getMessage())); // If we are missing a folder, we should clear the PING // cache also, to be sure it picks up any hierarchy changes // since most clients don't seem smart enough to figure this // out on their own. $this->resetPingCache(); return self::COLLECTION_ERR_FOLDERSYNC_REQUIRED; } catch (Horde_Exception_AuthenticationFailure $e) { // We lost authentication for some reason. $this->_logger->err(sprintf('[%s] Authentication lost during PING!!', $this->_procid)); return self::COLLECTION_ERR_AUTHENTICATION; } catch (Horde_ActiveSync_Exception $e) { $this->_logger->err(sprintf('[%s] Sync object cannot be configured, throttling: %s', $this->_procid, $e->getMessage())); $this->_sleep(30); continue; } } if (!empty($dataavailable)) { $this->_logger->info(sprintf('[%s] Found changes!', $this->_procid)); break; } // Wait a bit... $this->_sleep($interval); // Refresh the collections. $this->updateCollectionsFromCache(); } // Check that no other Sync process already started // If so, we exit here and let the other process do the export. if ($this->checkStaleRequest()) { $this->_logger->info('Changes in cache determined during Sync Wait/Heartbeat, exiting here.'); return self::COLLECTION_ERR_STALE; } $this->_logger->info(sprintf('[%s] Looping Sync complete: DataAvailable: %s, DataImported: %s', $this->_procid, $dataavailable, $this->importedChanges)); return $dataavailable; }
/** * Recursively decodes the WBXML from input stream. This means that if this * message contains complex types (like Appointment.Recuurence for example) * the sub-objects are auto-instantiated and decoded as well. Places the * decoded objects in the local properties array. * * @param Horde_ActiveSync_Wbxml_Decoder The stream decoder * * @throws Horde_ActiveSync_Exception */ public function decodeStream(Horde_ActiveSync_Wbxml_Decoder &$decoder) { while (1) { $entity = $decoder->getElement(); if ($entity[Horde_ActiveSync_Wbxml::EN_TYPE] == Horde_ActiveSync_Wbxml::EN_TYPE_STARTTAG) { if (!($entity[Horde_ActiveSync_Wbxml::EN_FLAGS] & Horde_ActiveSync_Wbxml::EN_FLAGS_CONTENT)) { $map = $this->_mapping[$entity[Horde_ActiveSync_Wbxml::EN_TAG]]; if (!isset($map[self::KEY_TYPE])) { $this->{$map[self::KEY_ATTRIBUTE]} = ''; } elseif ($map[self::KEY_TYPE] == self::TYPE_DATE || $map[self::KEY_TYPE] == self::TYPE_DATE_DASHES) { $this->{$map[self::KEY_ATTRIBUTE]} = ''; } continue; } // Found start tag if (!isset($this->_mapping[$entity[Horde_ActiveSync_Wbxml::EN_TAG]])) { $this->_logger->err(sprintf('Tag %s unexpected in type XML type %s.', $entity[Horde_ActiveSync_Wbxml::EN_TAG], get_class($this))); throw new Horde_ActiveSync_Exception('Unexpected tag'); } else { $map = $this->_mapping[$entity[Horde_ActiveSync_Wbxml::EN_TAG]]; if (isset($map[self::KEY_VALUES])) { // Handle arrays of attribute values while (1) { // If we can have multiple types of objects in this // container, or we are parsing a NO_CONTAINER, // check that we are not at the end tag of the // or we have a valid start tag for the NO_CONTAINER // object. If not, break out of loop. if (is_array($map[self::KEY_VALUES])) { $token = $decoder->peek(); if ($token[Horde_ActiveSync_Wbxml_Decoder::EN_TYPE] == Horde_ActiveSync_Wbxml_Decoder::EN_TYPE_ENDTAG) { break; } } elseif (!(isset($map[self::KEY_PROPERTY]) && $map[self::KEY_PROPERTY] == self::PROPERTY_NO_CONTAINER) && !$decoder->getElementStartTag($map[self::KEY_VALUES])) { break; } // We know we have some valid value, parse out what // it is. Either an array of (possibly varied) // objects, a single object, or simple value. if (is_array($map[self::KEY_VALUES])) { $token = $decoder->getToken(); if (($idx = array_search($token[Horde_ActiveSync_Wbxml_Decoder::EN_TAG], $map[self::KEY_VALUES])) !== false) { $class = $map[self::KEY_TYPE][$idx]; $decoded = new $class(array('protocolversion' => $this->_version, 'logger' => $this->_logger)); $decoded->commandType = $this->commandType; $decoded->decodeStream($decoder); } else { throw new Horde_ActiveSync_Exception('Error in message map configuration'); } } elseif (isset($map[self::KEY_TYPE])) { $class = $map[self::KEY_TYPE]; $decoded = new $class(array('protocolversion' => $this->_version, 'logger' => $this->_logger)); $decoded->commandType = $this->commandType; $decoded->decodeStream($decoder); } else { $decoded = $decoder->getElementContent(); } // Assign the parsed value to the mapped attribute. if (!isset($this->{$map[self::KEY_ATTRIBUTE]})) { $this->{$map[self::KEY_ATTRIBUTE]} = array($decoded); } else { $this->{$map[self::KEY_ATTRIBUTE]}[] = $decoded; } // Get the end tag of this attribute node. if (!$decoder->getElementEndTag()) { throw new Horde_ActiveSync_Exception('Missing expected wbxml end tag'); } // For NO_CONTAINER attributes, need some magic to // make sure we break out properly. if (isset($map[self::KEY_PROPERTY]) && $map[self::KEY_PROPERTY] == self::PROPERTY_NO_CONTAINER) { $e = $decoder->peek(); // Go back to the initial while if another block // of a non-container element is found. if ($e[Horde_ActiveSync_Wbxml::EN_TYPE] == Horde_ActiveSync_Wbxml::EN_TYPE_STARTTAG) { continue 2; } // Break on end tag because no other container // elements block end is reached. if ($e[Horde_ActiveSync_Wbxml::EN_TYPE] == Horde_ActiveSync_Wbxml::EN_TYPE_ENDTAG || empty($e)) { break; } } } // Do not get container end tag for an array without a container if (!(isset($map[self::KEY_PROPERTY]) && $map[self::KEY_PROPERTY] == self::PROPERTY_NO_CONTAINER) && !$decoder->getElementEndTag()) { return false; } } else { // Handle a simple attribute value if (isset($map[self::KEY_TYPE])) { if (in_array($map[self::KEY_TYPE], array(self::TYPE_DATE, self::TYPE_DATE_DASHES, self::TYPE_DATE_LOCAL))) { $decoded = $this->_parseDate($decoder->getElementContent()); } elseif ($map[self::KEY_TYPE] == self::TYPE_HEX) { $decoded = self::_hex2bin($decoder->getElementContent()); } else { // Complex type, decode recursively $class = $map[self::KEY_TYPE]; $subdecoder = new $class(array('protocolversion' => $this->_version, 'logger' => $this->_logger)); $subdecoder->commandType = $this->commandType; $subdecoder->decodeStream($decoder); $decoded = $subdecoder; } } else { // Simple type, just get content $decoded = $decoder->getElementContent(); if ($decoded === false) { $decoded = ''; $this->_logger->notice(sprintf('Unable to get expected content for %s: Setting to an empty string.', $entity[Horde_ActiveSync_Wbxml::EN_TAG])); } } if (!$decoder->getElementEndTag()) { $this->_logger->err(sprintf('Unable to get end tag for %s.', $entity[Horde_ActiveSync_Wbxml::EN_TAG])); throw new Horde_ActiveSync_Exception('Missing expected wbxml end tag'); } $this->{$map[self::KEY_ATTRIBUTE]} = $decoded; } } } elseif ($entity[Horde_ActiveSync_Wbxml::EN_TYPE] == Horde_ActiveSync_Wbxml::EN_TYPE_ENDTAG) { $decoder->_ungetElement($entity); break; } else { $this->_logger->err('Unexpected content in type'); break; } } if (!$this->_validateDecodedValues()) { throw new Horde_ActiveSync_Exception(sprintf('Invalid values detected in %s.', get_class($this))); } }
/** * Recursively decodes the WBXML from input stream. This means that if this * message contains complex types (like Appointment.Recuurence for example) * the sub-objects are auto-instantiated and decoded as well. Places the * decoded objects in the local properties array. * * @param Horde_ActiveSync_Wbxml_Decoder The stream decoder * * @throws Horde_ActiveSync_Exception */ public function decodeStream(Horde_ActiveSync_Wbxml_Decoder &$decoder) { while (1) { $entity = $decoder->getElement(); if ($entity[Horde_ActiveSync_Wbxml::EN_TYPE] == Horde_ActiveSync_Wbxml::EN_TYPE_STARTTAG) { if (!($entity[Horde_ActiveSync_Wbxml::EN_FLAGS] & Horde_ActiveSync_Wbxml::EN_FLAGS_CONTENT)) { $map = $this->_mapping[$entity[Horde_ActiveSync_Wbxml::EN_TAG]]; if (!isset($map[self::KEY_TYPE])) { $this->{$map[self::KEY_ATTRIBUTE]} = ''; } elseif ($map[self::KEY_TYPE] == self::TYPE_DATE || $map[self::KEY_TYPE] == self::TYPE_DATE_DASHES) { $this->{$map[self::KEY_ATTRIBUTE]} = ''; } continue; } // Found start tag if (!isset($this->_mapping[$entity[Horde_ActiveSync_Wbxml::EN_TAG]])) { $this->_logger->err('Tag ' . $entity[Horde_ActiveSync_Wbxml::EN_TAG] . ' unexpected in type XML type ' . get_class($this)); throw new Horde_ActiveSync_Exception('Unexpected tag'); } else { $map = $this->_mapping[$entity[Horde_ActiveSync_Wbxml::EN_TAG]]; if (isset($map[self::KEY_VALUES])) { // Handle arrays of attribute values while (1) { //do not get start tag for an array without a container if (!(isset($map[self::KEY_PROPERTY]) && $map[self::KEY_PROPERTY] == self::PROPERTY_NO_CONTAINER)) { if (!$decoder->getElementStartTag($map[self::KEY_VALUES])) { break; } } if (isset($map[self::KEY_TYPE])) { $class = $map[self::KEY_TYPE]; $decoded = new $class(array('protocolversion' => $this->_version, 'logger' => $this->_logger)); $decoded->decodeStream($decoder); } else { $decoded = $decoder->getElementContent(); } if (!isset($this->{$map[self::KEY_ATTRIBUTE]})) { $this->{$map[self::KEY_ATTRIBUTE]} = array($decoded); } else { $this->{$map[self::KEY_ATTRIBUTE]}[] = $decoded; } if (!$decoder->getElementEndTag()) { throw new Horde_ActiveSync_Exception('Missing expected wbxml end tag'); } if (isset($map[self::KEY_PROPERTY]) && $map[self::KEY_PROPERTY] == self::PROPERTY_NO_CONTAINER) { $e = $decoder->peek(); //go back to the initial while if another block of no container elements is found if ($e[Horde_ActiveSync_Wbxml::EN_TYPE] == Horde_ActiveSync_Wbxml::EN_TYPE_STARTTAG) { continue 2; } //break on end tag because no container elements block end is reached if ($e[Horde_ActiveSync_Wbxml::EN_TYPE] == Horde_ActiveSync_Wbxml::EN_TYPE_ENDTAG) { break; } if (empty($e)) { break; } } } //do not get container end tag for an array without a container if (!(isset($map[self::KEY_PROPERTY]) && $map[self::KEY_PROPERTY] == self::PROPERTY_NO_CONTAINER)) { if (!$decoder->getElementEndTag()) { return false; } } } else { // Handle a simple attribute value if (isset($map[self::KEY_TYPE])) { // Complex type, decode recursively if ($map[self::KEY_TYPE] == self::TYPE_DATE || $map[self::KEY_TYPE] == self::TYPE_DATE_DASHES) { $decoded = $this->_parseDate($decoder->getElementContent()); } elseif ($map[self::KEY_TYPE] == self::TYPE_HEX) { $decoded = self::hex2bin($decoder->getElementContent()); } else { $class = $map[self::KEY_TYPE]; $subdecoder = new $class(array('protocolversion' => $this->_version, 'logger' => $this->_logger)); $subdecoder->decodeStream($decoder); $decoded = $subdecoder; } } else { // Simple type, just get content $decoded = $decoder->getElementContent(); if ($decoded === false) { $decoded = ''; $this->_logger->notice('Unable to get expected content for ' . $entity[Horde_ActiveSync_Wbxml::EN_TAG] . ': Setting to an empty string.'); } } if (!$decoder->getElementEndTag()) { $this->_logger->err('Unable to get end tag for ' . $entity[Horde_ActiveSync_Wbxml::EN_TAG]); throw new Horde_ActiveSync_Exception('Missing expected wbxml end tag'); } $this->{$map[self::KEY_ATTRIBUTE]} = $decoded; } } } elseif ($entity[Horde_ActiveSync_Wbxml::EN_TYPE] == Horde_ActiveSync_Wbxml::EN_TYPE_ENDTAG) { $decoder->_ungetElement($entity); break; } else { $this->_logger->err('Unexpected content in type'); break; } } }