1421 lines
46 KiB
PHP
1421 lines
46 KiB
PHP
<?php
|
|
/**
|
|
* Horde_ActiveSync_Collections::
|
|
*
|
|
* @license http://www.horde.org/licenses/gpl GPLv2
|
|
*
|
|
* @copyright 2010-2020 Horde LLC (http://www.horde.org)
|
|
* @author Michael J Rubinsky <mrubinsk@horde.org>
|
|
* @package ActiveSync
|
|
*/
|
|
/**
|
|
* Horde_ActiveSync_Collections:: Responsible for all functionality related to
|
|
* collections and managing the sync cache.
|
|
*
|
|
* @license http://www.horde.org/licenses/gpl GPLv2
|
|
*
|
|
* @copyright 2010-2020 Horde LLC (http://www.horde.org)
|
|
* @author Michael J Rubinsky <mrubinsk@horde.org>
|
|
* @package ActiveSync
|
|
* @internal Not intended for use outside of the ActiveSync library.
|
|
*/
|
|
class Horde_ActiveSync_Collections implements IteratorAggregate
|
|
{
|
|
const COLLECTION_ERR_FOLDERSYNC_REQUIRED = -1;
|
|
const COLLECTION_ERR_SERVER = -2;
|
|
const COLLECTION_ERR_STALE = -3;
|
|
const COLLECTION_ERR_SYNC_REQUIRED = -4;
|
|
const COLLECTION_ERR_AUTHENTICATION = -6;
|
|
|
|
/**
|
|
* The collection data
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $_collections = array();
|
|
|
|
/**
|
|
* Cache a temporary syncCache.
|
|
*
|
|
* @var Horde_ActiveSync_SyncCache
|
|
*/
|
|
protected $_tempSyncCache;
|
|
|
|
/**
|
|
* The syncCache
|
|
*
|
|
* @var Horde_ActiveSync_SyncCache
|
|
*/
|
|
protected $_cache;
|
|
|
|
/**
|
|
* The logger
|
|
*
|
|
* @var Horde_Log_Logger
|
|
*/
|
|
protected $_logger;
|
|
|
|
/**
|
|
* Count of unchanged collections calculated for PARTIAL sync.
|
|
*
|
|
* @var integer
|
|
*/
|
|
protected $_unchangedCount = 0;
|
|
|
|
/**
|
|
* Count of available synckeys
|
|
*
|
|
* @var integer
|
|
*/
|
|
protected $_synckeyCount = 0;
|
|
|
|
/**
|
|
* Global WINDOWSIZE
|
|
* Defaults to 100 (MS-ASCMD 2.2.3.188)
|
|
*
|
|
* @var integer
|
|
*/
|
|
protected $_globalWindowSize = 100;
|
|
|
|
/**
|
|
* Flag to indicate we have overridden the globalWindowSize
|
|
*
|
|
* @var boolean
|
|
*/
|
|
protected $_windowsizeOverride = false;
|
|
|
|
/**
|
|
* Imported changes flag.
|
|
*
|
|
* @var boolean
|
|
*/
|
|
protected $_importedChanges = false;
|
|
|
|
/**
|
|
* Short sync request flag.
|
|
*
|
|
* @var boolean
|
|
*/
|
|
protected $_shortSyncRequest = false;
|
|
|
|
/**
|
|
* Cache of collections that have had changes detected.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $_changedCollections = array();
|
|
|
|
/**
|
|
* The ActiveSync server object.
|
|
*
|
|
* @var Horde_ActiveSync
|
|
*/
|
|
protected $_as;
|
|
|
|
/**
|
|
* Cache the process id for logging.
|
|
*
|
|
* @var integer
|
|
*/
|
|
protected $_procid;
|
|
|
|
/**
|
|
* Cache of changes.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $_changes;
|
|
|
|
/**
|
|
* Flag to indicate the client is requesting a hanging SYNC.
|
|
*
|
|
* @var boolean
|
|
*/
|
|
protected $_hangingSync = false;
|
|
|
|
/**
|
|
* Const'r
|
|
*
|
|
* @param Horde_ActiveSync_SyncCache $cache The SyncCache.
|
|
* @param Horde_ActiveSync $as The ActiveSync server object.
|
|
*/
|
|
public function __construct(
|
|
Horde_ActiveSync_SyncCache $cache,
|
|
Horde_ActiveSync $as)
|
|
{
|
|
|
|
$this->_cache = $cache;
|
|
$this->_as = $as;
|
|
$this->_logger = $as->logger;
|
|
$this->_procid = getmypid();
|
|
}
|
|
|
|
/**
|
|
* Load all the collections we know about from the cache.
|
|
*/
|
|
public function loadCollectionsFromCache()
|
|
{
|
|
foreach ($this->_cache->getCollections(false) as $collection) {
|
|
if (empty($collection['synckey']) && !empty($collection['lastsynckey'])) {
|
|
$collection['synckey'] = $collection['lastsynckey'];
|
|
}
|
|
// Load the class if needed for EAS >= 12.1
|
|
if (empty($collection['class'])) {
|
|
$collection['class'] = $this->getCollectionClass($collection['id']);
|
|
}
|
|
if (empty($collection['serverid'])) {
|
|
try {
|
|
$collection['serverid'] = $this->getBackendIdForFolderUid($collection['id']);
|
|
} catch (Horde_ActiveSync_Exception $e) {
|
|
$this->_logger->err($e->getMessage());
|
|
continue;
|
|
}
|
|
}
|
|
$this->_collections[$collection['id']] = $collection;
|
|
$this->_logger->meta(sprintf(
|
|
'COLLECTIONS: Loaded %s from the cache.',
|
|
$collection['serverid'])
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Magic...
|
|
*/
|
|
public function __call($method, $parameters)
|
|
{
|
|
switch ($method) {
|
|
case 'hasPingChangeFlag':
|
|
case 'addConfirmedKey':
|
|
case 'updateCollection':
|
|
case 'collectionExists':
|
|
case 'updateWindowSize':
|
|
return call_user_func_array(array($this->_cache, $method), $parameters);
|
|
}
|
|
|
|
throw new BadMethodCallException('Unknown method: ' . $method);
|
|
}
|
|
|
|
/**
|
|
* Property getter
|
|
*/
|
|
public function __get($property)
|
|
{
|
|
switch ($property) {
|
|
case 'hbinterval':
|
|
case 'wait':
|
|
case 'confirmed_synckeys':
|
|
case 'lasthbsyncstarted':
|
|
case 'lastsyncendnormal':
|
|
return $this->_cache->$property;
|
|
case 'importedChanges':
|
|
case 'shortSyncRequest':
|
|
case 'hangingSync':
|
|
$p = '_' . $property;
|
|
return $this->$p;
|
|
}
|
|
|
|
throw new InvalidArgumentException('Unknown property: ' . $property);
|
|
}
|
|
|
|
/**
|
|
* Property setter.
|
|
*/
|
|
public function __set($property, $value)
|
|
{
|
|
switch ($property) {
|
|
case 'importedChanges':
|
|
case 'shortSyncRequest':
|
|
case 'hangingSync':
|
|
$p = '_' . $property;
|
|
$this->$p = $value;
|
|
return;
|
|
case 'lasthbsyncstarted':
|
|
case 'lastsyncendnormal':
|
|
case 'hbinterval':
|
|
case 'wait':
|
|
$this->_cache->$property = $value;
|
|
return;
|
|
|
|
case 'confirmed_synckeys':
|
|
throw new InvalidArgumentException($property . ' is READONLY.');
|
|
}
|
|
|
|
throw new InvalidArgumentException('Unknown property: ' . $property);
|
|
}
|
|
|
|
/**
|
|
* Get a new collection array, populated with default values.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getNewCollection()
|
|
{
|
|
return array(
|
|
'clientids' => array(),
|
|
'fetchids' => array(),
|
|
'windowsize' => 100,
|
|
'soft' => false,
|
|
'conflict' => Horde_ActiveSync::CONFLICT_OVERWRITE_PIM
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Ensure default OPTIONS values are populated, while not overwriting any
|
|
* existing values.
|
|
*
|
|
* @since 2.20.0
|
|
*/
|
|
public function ensureOptions()
|
|
{
|
|
foreach ($this->_collections as &$collection) {
|
|
$this->_logger->meta(sprintf(
|
|
'COLLECTIONS: Loading default OPTIONS for %s collection.',
|
|
$collection['id'])
|
|
);
|
|
|
|
if (!isset($collection['mimesupport'])) {
|
|
$collection['mimesupport'] = Horde_ActiveSync::MIME_SUPPORT_NONE;
|
|
}
|
|
if (!isset($collection['bodyprefs'])) {
|
|
$collection['bodyprefs'] = array();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a new populated collection array to the sync cache.
|
|
*
|
|
* @param array $collection The collection array.
|
|
* @param boolean $requireSyncKey Attempt to read missing synckey from
|
|
* cache if true. If not found, set to 0.
|
|
*
|
|
* @throws Horde_ActiveSync_Exception_StateGone Thrown when no synckey
|
|
* is provided when one is specified as required, indicating
|
|
* the state on the client is possibly corrupt or when the
|
|
* serverid can not be found by the backend.
|
|
*/
|
|
public function addCollection(array $collection, $requireSyncKey = false)
|
|
{
|
|
if ($requireSyncKey && empty($collection['synckey'])) {
|
|
$cached_collections = $this->_cache->getCollections(false);
|
|
$collection['synckey'] = !empty($cached_collections[$collection['id']])
|
|
? $cached_collections[$collection['id']]['lastsynckey']
|
|
: 0;
|
|
|
|
if ($collection['synckey'] === 0) {
|
|
$this->_logger->err('COLLECTIONS: Attempting to add a collection
|
|
to the sync cache while requiring a synckey, but no
|
|
synckey could be found. Most likely a client error in
|
|
requesting a collection during PING before it has issued a
|
|
SYNC.'
|
|
);
|
|
throw new Horde_ActiveSync_Exception_StateGone(
|
|
'Synckey required in Horde_ActiveSync_Collections::addCollection, but none was found.'
|
|
);
|
|
}
|
|
|
|
$this->_logger->meta(sprintf(
|
|
'COLLECTIONS: Obtained synckey for collection %s from cache: %s',
|
|
$collection['id'],
|
|
$collection['synckey'])
|
|
);
|
|
}
|
|
|
|
// Load the class if needed for EAS >= 12.1 and ensure we have the
|
|
// backend folder id.
|
|
if (empty($collection['class'])) {
|
|
$collection['class'] = $this->getCollectionClass($collection['id']);
|
|
}
|
|
|
|
try {
|
|
$collection['serverid'] = $this->getBackendIdForFolderUid($collection['id']);
|
|
} catch (Horde_ActiveSync_Exception $e) {
|
|
throw new Horde_ActiveSync_Exception_StateGone($e->getMessage());
|
|
}
|
|
|
|
$this->_collections[$collection['id']] = $collection;
|
|
$this->_logger->meta(sprintf(
|
|
'COLLECTIONS: Collection added to collection handler: collection: %s, synckey: %s.',
|
|
$collection['serverid'],
|
|
!empty($collection['synckey']) ? $collection['synckey'] : 'NONE')
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Translate an EAS folder uid into a backend serverid.
|
|
*
|
|
* @param $id The uid.
|
|
*
|
|
* @return string The backend server id.
|
|
* @throws Horde_ActiveSync_Exception_FolderGone
|
|
*/
|
|
public function getBackendIdForFolderUid($folderid)
|
|
{
|
|
// Always use RI for recipient cache.
|
|
if ($folderid == 'RI') {
|
|
return $folderid;
|
|
}
|
|
$folder = $this->_cache->getFolder($folderid);
|
|
if ($folder) {
|
|
return $folder['serverid'];
|
|
} else {
|
|
$this->_logger->err('COLLECTIONS: Horde_ActiveSync_Collections::getBackendIdForFolderUid failed because folder was not found in cache.');
|
|
throw new Horde_ActiveSync_Exception_FolderGone('Folder not found in cache.');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Translate a backend id E.g., INBOX into an EAS folder uid.
|
|
*
|
|
* @param string $folderid The backend id.
|
|
*
|
|
* @return string The EAS uid.
|
|
*/
|
|
public function getFolderUidForBackendId($folderid)
|
|
{
|
|
// Always use 'RI' for Recipient cache.
|
|
if ($folderid == 'RI') {
|
|
return $folderid;
|
|
}
|
|
$map = $this->_as->state->getFolderUidToBackendIdMap();
|
|
if (empty($map[$folderid])) {
|
|
return false;
|
|
}
|
|
|
|
return $map[$folderid];
|
|
}
|
|
|
|
/**
|
|
* Return the count of available collections.
|
|
*
|
|
* @return integer
|
|
*/
|
|
public function collectionCount()
|
|
{
|
|
return count($this->_collections);
|
|
}
|
|
|
|
/**
|
|
* Return the count of collections in the cache only.
|
|
*
|
|
* @return integer
|
|
*/
|
|
public function cachedCollectionCount()
|
|
{
|
|
return $this->_cache->countCollections();
|
|
}
|
|
|
|
/**
|
|
* Set the getchanges flag on the specified collection.
|
|
*
|
|
* @param string $collection_id The collection id.
|
|
*
|
|
* @throws Horde_ActiveSync_Exception
|
|
*/
|
|
public function setGetChangesFlag($collection_id)
|
|
{
|
|
if (empty($this->_collections[$collection_id])) {
|
|
throw new Horde_ActiveSync_Exception('Missing collection data');
|
|
}
|
|
$this->_collections[$collection_id]['getchanges'] = true;
|
|
}
|
|
|
|
/**
|
|
* Get the getchanges flag on the specified collection.
|
|
*
|
|
* @param string $collection_id The collection id.
|
|
*
|
|
* @return boolean
|
|
* @throws Horde_ActiveSync_Exception
|
|
*/
|
|
public function getChangesFlag($collection_id)
|
|
{
|
|
if (empty($this->_collections[$collection_id])) {
|
|
throw new Horde_ActiveSync_Exception('Missing collection data');
|
|
}
|
|
return !empty($this->_collections[$collection_id]['getchanges']);
|
|
}
|
|
|
|
/**
|
|
* Sets the default WINDOWSIZE.
|
|
*
|
|
* Note that this is really a ceiling on the number of TOTAL responses
|
|
* that can be sent (including all collections). This method should be
|
|
* renamed for 3.0
|
|
*
|
|
* @param integer $window The windowsize
|
|
* @param boolean $override If true, this value will override any client
|
|
* supplied value.
|
|
*/
|
|
public function setDefaultWindowSize($window, $override = false)
|
|
{
|
|
if ($override) {
|
|
$this->_windowsizeOverride = true;
|
|
}
|
|
|
|
if ($override || empty($this->_windowsizeOverride)) {
|
|
$this->_globalWindowSize = $window;
|
|
}
|
|
}
|
|
|
|
public function getDefaultWindowSize()
|
|
{
|
|
return $this->_globalWindowSize;
|
|
}
|
|
|
|
/**
|
|
* Validates the collection data from the syncCache, filling in missing
|
|
* values from the folder cache.
|
|
*/
|
|
public function validateFromCache()
|
|
{
|
|
$this->_cache->validateCollectionsFromCache($this->_collections);
|
|
}
|
|
|
|
/**
|
|
* Updates data from the cache for collectons that are already loaded. Used
|
|
* to ensure looping SYNC and PING requests are operating on the most
|
|
* recent syncKey.
|
|
*/
|
|
public function updateCollectionsFromCache()
|
|
{
|
|
$this->_cache->refreshCollections();
|
|
$collections = $this->_cache->getCollections();
|
|
foreach (array_keys($this->_collections) as $id) {
|
|
if (!empty($collections[$id])) {
|
|
$this->_logger->meta(sprintf(
|
|
'COLLECTIONS: Refreshing %s from the cache.',
|
|
$id)
|
|
);
|
|
$this->_collections[$id] = $collections[$id];
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return a collection class given the collection id.
|
|
*
|
|
* @param string $id The collection id.
|
|
*
|
|
* @return string|boolean The collection class or false if not found.
|
|
*/
|
|
public function getCollectionClass($id)
|
|
{
|
|
if ($id == 'RI') {
|
|
return $id;
|
|
}
|
|
|
|
// First try existing, loaded collections.
|
|
if (!empty($this->_collections[$id])) {
|
|
return $this->_collections[$id]['class'];
|
|
}
|
|
|
|
// Next look in the SyncCache.
|
|
if (isset($this->_cache->folders[$id]['class'])) {
|
|
$class = $this->_cache->folders[$id]['class'];
|
|
$this->_logger->meta(sprintf(
|
|
'COLLECTIONS: Obtaining collection class of %s for collection id %s',
|
|
$class, $id)
|
|
);
|
|
return $class;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public function getCollectionType($id)
|
|
{
|
|
if ($id == 'RI') {
|
|
return $id;
|
|
}
|
|
|
|
// First try existing, loaded collections.
|
|
if (!empty($this->_collections[$id]['type'])) {
|
|
return $this->_collections[$id]['type'];
|
|
}
|
|
|
|
// Next look in the SyncCache.
|
|
if (isset($this->_cache->folders[$id]['type'])) {
|
|
$type = $this->_cache->folders[$id]['type'];
|
|
$this->_logger->meta(sprintf(
|
|
'COLLECTIONS: Obtaining collection type of %s for collection id %s',
|
|
$type, $id)
|
|
);
|
|
return $type;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Determine if we have any syncable collections either locally or in the
|
|
* sync cache.
|
|
*
|
|
* @param long $version The EAS version
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public function haveSyncableCollections($version)
|
|
{
|
|
// Ensure we have syncable collections, using the cache if needed.
|
|
if ($version >= Horde_ActiveSync::VERSION_TWELVEONE && empty($this->_collections)) {
|
|
$this->_logger->meta('COLLECTIONS: No collections loaded, looking in sync_cache.');
|
|
$found = false;
|
|
foreach ($this->_cache->getCollections() as $value) {
|
|
if (isset($value['synckey'])) {
|
|
$this->_logger->meta(sprintf(
|
|
'COLLECTIONS: Found syncable collection: %s : %s.',
|
|
$value['serverid'], $value['synckey'])
|
|
);
|
|
$this->_collections[$value['id']] = $value;
|
|
$found = true;
|
|
}
|
|
}
|
|
return $found;
|
|
} elseif (empty($this->_collections)) {
|
|
return false;
|
|
}
|
|
|
|
$this->_logger->meta('COLLECTIONS: Have syncable collections!');
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Set the looping sync heartbeat values.
|
|
*
|
|
* @param array $hb An array containing one or both of: hbinterval, wait.
|
|
*/
|
|
public function setHeartbeat(array $hb)
|
|
{
|
|
if (isset($hb['wait'])) {
|
|
$this->_cache->wait = $hb['wait'];
|
|
}
|
|
if (isset($hb['hbinterval'])) {
|
|
$this->_cache->hbinterval = $hb['hbinterval'];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return the heartbeat interval. Always returned as the heartbeat (seconds)
|
|
* not wait interval (minutes).
|
|
*
|
|
* @return integer|boolean The number of seconds in a heartbeat, or false
|
|
* if no heartbeat set.
|
|
*/
|
|
public function getHeartbeat()
|
|
{
|
|
return !empty($this->_cache->hbinterval)
|
|
? $this->_cache->hbinterval
|
|
: (!empty($this->_cache->wait)
|
|
? $this->_cache->wait * 60
|
|
: false);
|
|
}
|
|
|
|
/**
|
|
* Return whether or not we want a looping sync. We can do a looping sync
|
|
* if we have no imported changes AND we have either a hbinterval, wait,
|
|
* or a shortSync.
|
|
*
|
|
* @return boolean True if we want a looping sync, false otherwise.
|
|
*/
|
|
public function canDoLoopingSync()
|
|
{
|
|
return $this->_hangingSync && !$this->_importedChanges && ($this->_shortSyncRequest || $this->_cache->hbinterval !== false || $this->_cache->wait !== false);
|
|
}
|
|
|
|
/**
|
|
* Return if the current looping sync is stale. A stale looping sync is one
|
|
* which has begun earlier than the most recently running sync reported by
|
|
* the syncCache.
|
|
*
|
|
* @return boolean True if the current looping sync is stale. False
|
|
* otherwise.
|
|
*/
|
|
public function checkStaleRequest()
|
|
{
|
|
return !$this->_cache->validateCache(true);
|
|
}
|
|
|
|
/**
|
|
* Return if we have a current folder hierarchy.
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public function haveHierarchy()
|
|
{
|
|
return isset($this->_cache->hierarchy);
|
|
}
|
|
|
|
/**
|
|
* Prepare for a hierarchy sync.
|
|
*
|
|
* @param string $synckey The current synckey from the client.
|
|
*
|
|
* @return array An array of known folders.
|
|
*/
|
|
public function initHierarchySync($synckey)
|
|
{
|
|
$this->_as->state->loadState(
|
|
array(),
|
|
$synckey,
|
|
Horde_ActiveSync::REQUEST_TYPE_FOLDERSYNC);
|
|
|
|
// Refresh the cache since it might have changed like e.g., if synckey
|
|
// was empty.
|
|
$this->_cache->loadCacheFromStorage();
|
|
|
|
return $this->_as->state->getKnownFolders();
|
|
}
|
|
|
|
/**
|
|
* Update/Add a folder in the hierarchy cache.
|
|
*
|
|
* @param Horde_ActiveSync_Message_Folder $folder The folder object.
|
|
* @param boolean $update Update the state objects? @since 2.4.0
|
|
*/
|
|
public function updateFolderinHierarchy(
|
|
Horde_ActiveSync_Message_Folder $folder, $update = false)
|
|
{
|
|
$this->_cache->updateFolder($folder);
|
|
$cols = $this->_cache->getCollections(false);
|
|
$cols[$folder->serverid]['serverid'] = $folder->_serverid;
|
|
$this->_cache->updateCollection($cols[$folder->serverid]);
|
|
if ($update) {
|
|
$this->_as->state->updateServerIdInState($folder->serverid, $folder->_serverid);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete a folder from the hierarchy cache.
|
|
*
|
|
* @param string $id The folder's uid.
|
|
*/
|
|
public function deleteFolderFromHierarchy($uid)
|
|
{
|
|
$this->_cache->deleteFolder($uid);
|
|
$this->_as->state->removeState(array(
|
|
'id' => $uid,
|
|
'devId' => $this->_as->device->id,
|
|
'user' => $this->_as->device->user));
|
|
}
|
|
|
|
/**
|
|
* Return all know hierarchy changes.
|
|
*
|
|
* @return array An array of changes.
|
|
*/
|
|
public function getHierarchyChanges()
|
|
{
|
|
return $this->_as->state->getChanges();
|
|
}
|
|
|
|
/**
|
|
* Validate and perform some sanity checks on the hierarchy changes before
|
|
* being sent to the client.
|
|
*
|
|
* @param Horde_ActiveSync_Connector_Exporter_FolderSync $exporter The exporter.
|
|
* @param array $seenFolders An array of folders.
|
|
*/
|
|
public function validateHierarchyChanges(Horde_ActiveSync_Connector_Exporter_FolderSync $exporter, array $seenFolders)
|
|
{
|
|
if ($this->_as->device->version < Horde_ActiveSync::VERSION_TWELVEONE ||
|
|
count($exporter->changed)) {
|
|
return;
|
|
}
|
|
|
|
// Remove unnecessary changes.
|
|
foreach ($exporter->changed as $key => $folder) {
|
|
if (isset($folder->serverid) &&
|
|
$syncFolder = $this->_cache->getFolder($folder->serverid) &&
|
|
in_array($folder->serverid, $seenfolders) &&
|
|
$syncFolder['parentid'] == $folder->parentid &&
|
|
$syncFolder['displayname'] == $folder->displayname &&
|
|
$syncFolder['type'] == $folder->type) {
|
|
|
|
$this->_logger->meta(sprintf(
|
|
'COLLECTIONS: Ignoring %s from changes because it contains no changes from device.',
|
|
$folder->serverid)
|
|
);
|
|
unset($exporter->changed[$key]);
|
|
$exporter->count--;
|
|
}
|
|
}
|
|
|
|
// Remove unnecessary deletions.
|
|
foreach ($exporter->deleted as $key => $folder) {
|
|
if (($sid = array_search($folder, $seenfolders)) === false) {
|
|
$this->_logger->meta(sprintf(
|
|
'COLLECTIONS: Ignoring %s from deleted list because the device does not know it',
|
|
$folder)
|
|
);
|
|
unset($exporter->deleted[$key]);
|
|
$exporter->count--;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update the hierarchy synckey in the cache.
|
|
*
|
|
* @param string $key The new/existing synckey.
|
|
*/
|
|
public function updateHierarchyKey($key)
|
|
{
|
|
$this->_cache->hierarchy = $key;
|
|
}
|
|
|
|
/**
|
|
* Prepares the syncCache for a full sync request.
|
|
*/
|
|
public function initFullSync()
|
|
{
|
|
$this->_cache->confirmed_synckeys = array();
|
|
$this->_cache->clearCollectionKeys();
|
|
}
|
|
|
|
/**
|
|
* Prepare the syncCache for an EMPTY sync request.
|
|
*
|
|
* @return boolean False if EMPTY request cannot be performed, otherwise
|
|
* true.
|
|
* @since 2.25.0
|
|
*/
|
|
public function initEmptySync()
|
|
{
|
|
$this->loadCollectionsFromCache();
|
|
foreach ($this->_collections as $value) {
|
|
// Remove keys from confirmed synckeys array and count them
|
|
if (isset($value['synckey'])) {
|
|
if (isset($this->_cache->confirmed_synckeys[$value['synckey']])) {
|
|
$this->_logger->meta(sprintf(
|
|
'COLLECTIONS: Removed %s from confirmed_synckeys',
|
|
$value['synckey'])
|
|
);
|
|
$this->_cache->removeConfirmedKey($value['synckey']);
|
|
}
|
|
$this->_synckeyCount++;
|
|
}
|
|
}
|
|
if (!$this->_checkConfirmedKeys()) {
|
|
$this->_logger->err('COLLECTIONS: Some synckeys were not confirmed, but handling an empty request. Requesting full SYNC');
|
|
$this->save();
|
|
return false;
|
|
}
|
|
$this->shortSyncRequest = true;
|
|
$this->hangingSync = true;
|
|
$this->save(true);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Prepares the syncCache for a partial sync request and checks that
|
|
* it is allowed.
|
|
*
|
|
* MS-ASCMD 2.2.3.124
|
|
*
|
|
* @return boolean True if parital sync is possible, false otherwise.
|
|
*/
|
|
public function initPartialSync()
|
|
{
|
|
// Need this for all PARTIAL sync requests.
|
|
$this->_tempSyncCache = clone $this->_cache;
|
|
|
|
// Short circuit if we only have a changed ping/wait interval.
|
|
if (empty($this->_collections)) {
|
|
$emptyCollections = true;
|
|
$this->_logger->meta('COLLECTIONS: No collections loaded, loading full collection set from cache.');
|
|
$this->loadCollectionsFromCache();
|
|
|
|
} else {
|
|
// Collect collection options sent from client and compare against
|
|
// last known collection options to determine which collections
|
|
// changed.
|
|
$emptyCollections = false;
|
|
$c = $this->_tempSyncCache->getCollections();
|
|
foreach ($this->_collections as $key => $value) {
|
|
// Collections from cache might not all have synckeys.
|
|
if (empty($c[$key])) {
|
|
continue;
|
|
}
|
|
$v1 = $value;
|
|
foreach ($v1 as $k => $o) {
|
|
if (is_null($o)) {
|
|
unset($v1[$k]);
|
|
}
|
|
}
|
|
unset($v1['id'], $v1['serverid'], $v1['clientids'], $v1['fetchids'],
|
|
$v1['getchanges'], $v1['changeids'], $v1['pingable'],
|
|
$v1['class'], $v1['synckey'], $v1['lastsynckey']
|
|
);
|
|
$v2 = $c[$key];
|
|
foreach ($v2 as $k => $o) {
|
|
if (is_null($o)) {
|
|
unset($v2[$k]);
|
|
}
|
|
}
|
|
unset($v2['id'], $v2['serverid'], $v2['pingable'], $v2['class'],
|
|
$v2['synckey'], $v2['lastsynckey']
|
|
);
|
|
ksort($v1);
|
|
if (isset($v1['bodyprefs'])) {
|
|
ksort($v1['bodyprefs']);
|
|
foreach (array_keys($v1['bodyprefs']) as $k) {
|
|
if (is_array($v1['bodyprefs'][$k])) {
|
|
ksort($v1['bodyprefs'][$k]);
|
|
}
|
|
}
|
|
}
|
|
ksort($v2);
|
|
if (isset($v2['bodyprefs'])) {
|
|
ksort($v2['bodyprefs']);
|
|
foreach (array_keys($v2['bodyprefs']) as $k) {
|
|
if (is_array($v2['bodyprefs'][$k])) {
|
|
ksort($v2['bodyprefs'][$k]);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (md5(serialize($v1)) == md5(serialize($v2))) {
|
|
$this->_unchangedCount++;
|
|
}
|
|
|
|
// Unset in tempSyncCache, since we have it from device.
|
|
$this->_tempSyncCache->removeCollection($key);
|
|
|
|
// Populate _collections with missing collection data not sent.
|
|
$this->_getMissingCollectionsFromCache();
|
|
}
|
|
}
|
|
|
|
// Ensure we are both talking about the same synckey.
|
|
foreach ($this->_collections as $value) {
|
|
if (isset($value['synckey'])) {
|
|
if (isset($this->_cache->confirmed_synckeys[$value['synckey']])) {
|
|
$this->_logger->meta(sprintf(
|
|
'COLLECTIONS: Removed %s from confirmed_synckeys',
|
|
$value['synckey'])
|
|
);
|
|
$this->_cache->removeConfirmedKey($value['synckey']);
|
|
}
|
|
$this->_synckeyCount++;
|
|
}
|
|
}
|
|
|
|
if (!$this->_checkConfirmedKeys()) {
|
|
$this->_logger->warn('COLLECTIONS: Some synckeys were not confirmed. Requesting full SYNC');
|
|
$this->save();
|
|
return false;
|
|
}
|
|
|
|
if (!$emptyCollections && $this->_haveNoChangesInPartialSync()) {
|
|
$this->_logger->warn('COLLECTIONS: Partial Request with completely unchanged collections. Request a full SYNC');
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
protected function _checkConfirmedKeys()
|
|
{
|
|
$csk = $this->_cache->confirmed_synckeys;
|
|
if ($csk) {
|
|
$this->_logger->meta(sprintf(
|
|
'COLLECTIONS: Confirmed Synckeys contains %s',
|
|
serialize($csk))
|
|
);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Return if we can do an empty response
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public function canSendEmptyResponse()
|
|
{
|
|
return !$this->_importedChanges &&
|
|
($this->_hangingSync && ($this->_cache->wait !== false || $this->_cache->hbinterval !== false));
|
|
}
|
|
|
|
/**
|
|
* Return if we have no changes to collection options, but have requested
|
|
* a partial sync. A partial sync must have either a wait, hbinterval,
|
|
* or some subset of collections to be valid.
|
|
*
|
|
* @return boolean
|
|
*/
|
|
protected function _haveNoChangesInPartialSync()
|
|
{
|
|
return $this->_synckeyCount > 0 &&
|
|
$this->_unchangedCount == $this->_synckeyCount &&
|
|
$this->_cache->wait == false && $this->_cache->hbinterval == false;
|
|
}
|
|
|
|
/**
|
|
* Populate the collections data with missing data from the syncCache during
|
|
* a PARTIAL SYNC.
|
|
*/
|
|
protected function _getMissingCollectionsFromCache()
|
|
{
|
|
if (empty($this->_tempSyncCache)) {
|
|
throw new Horde_ActiveSync_Exception('Did not initialize the PARTIAL sync.');
|
|
}
|
|
|
|
// Update _collections with all data that was not sent, but we
|
|
// have a synckey for in the sync_cache.
|
|
foreach ($this->_tempSyncCache->getCollections() as $value) {
|
|
// The collection might have been updated due to incoming
|
|
// changes. Some clients send COMMANDS in a PARTIAL sync and
|
|
// initializing the PARTIAL afterwards will overwrite the various
|
|
// flags stored in $collection['id'][]
|
|
if (!empty($this->_collections[$value['id']])) {
|
|
continue;
|
|
}
|
|
$this->_logger->meta(sprintf(
|
|
'COLLECTIONS: Using SyncCache State for %s',
|
|
$value['serverid']
|
|
));
|
|
if (empty($value['synckey'])) {
|
|
$value['synckey'] = $value['lastsynckey'];
|
|
}
|
|
$this->_collections[$value['id']] = $value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check for an update FILTERTYPE
|
|
*
|
|
* @param string $id The collection id to check
|
|
* @param string $filter The new filter value.
|
|
*
|
|
* @return boolean True if filtertype passed, false if it has changed.
|
|
*/
|
|
public function checkFilterType($id, $filter)
|
|
{
|
|
$cc = $this->_cache->getCollections();
|
|
if (!empty($cc[$id]['filtertype']) &&
|
|
!is_null($filter) &&
|
|
$cc[$id]['filtertype'] != $filter) {
|
|
$this->_logger->meta(sprintf(
|
|
'COLLECTIONS: Filtertype change from: %d to %d',
|
|
$cc[$id]['filtertype'], $filter)
|
|
);
|
|
$this->_cache->updateFiltertype($id, $filter);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Update the syncCache with current collection data.
|
|
*/
|
|
public function updateCache()
|
|
{
|
|
foreach ($this->_collections as $value) {
|
|
$this->_cache->updateCollection($value);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save the syncCache to storage.
|
|
*
|
|
* @param boolean $preserve_folders If true, ensure the folder cache is not
|
|
* overwritten. @since 2.18.0
|
|
* @todo Refactor this hack away. Requires a complete refactor of the cache.
|
|
*/
|
|
public function save($preserve_folders = false)
|
|
{
|
|
// HOTFIX. Need to check the timestamp to see if we should reload the
|
|
// folder cache before saving to ensure it isn't overwritten. See
|
|
// Bug: 13273
|
|
if ($preserve_folders && !$this->_cache->validateCache()) {
|
|
$this->_logger->meta('COLLECTIONS: Updating the foldercache before saving.');
|
|
$this->_cache->refreshFolderCache();
|
|
}
|
|
|
|
$this->_cache->save();
|
|
}
|
|
|
|
/**
|
|
* Attempt to initialize the sync state.
|
|
*
|
|
* @param array $collection The collection array.
|
|
* @param boolean $requireSyncKey Require collection to have a synckey and
|
|
* throw exception if it's not present.
|
|
*
|
|
* @throws Horde_ActiveSync_Exception_InvalidRequest
|
|
* @throws Horde_ActiveSync_Exception_FolderGone
|
|
*/
|
|
public function initCollectionState(array &$collection, $requireSyncKey = false)
|
|
{
|
|
// Clear the changes cache.
|
|
$this->_changes = null;
|
|
|
|
// Ensure we have a collection class.
|
|
if (empty($collection['class'])) {
|
|
if (!($collection['class'] = $this->getCollectionClass($collection['id']))) {
|
|
throw new Horde_ActiveSync_Exception_FolderGone('Could not load collection class for ' . $collection['id']);
|
|
}
|
|
}
|
|
|
|
// Load the collection's type if we can.
|
|
if (empty($collection['type'])) {
|
|
$collection['type'] = $this->getCollectionType($collection['id']);
|
|
}
|
|
|
|
// Get the backend serverid.
|
|
if (empty($collection['serverid'])) {
|
|
$collection['serverid'] = $this->getBackendIdForFolderUid($collection['id']);
|
|
}
|
|
|
|
|
|
if ($requireSyncKey && empty($collection['synckey'])) {
|
|
throw new Horde_ActiveSync_Exception_InvalidRequest(sprintf(
|
|
'Empty synckey for %s.',
|
|
$collection['id'])
|
|
);
|
|
}
|
|
|
|
// Initialize the state
|
|
$this->_logger->info(sprintf(
|
|
'COLLECTIONS: Initializing state for collection: %s, synckey: %s',
|
|
$collection['serverid'],
|
|
$collection['synckey'])
|
|
);
|
|
$this->_as->state->loadState(
|
|
$collection,
|
|
$collection['synckey'],
|
|
Horde_ActiveSync::REQUEST_TYPE_SYNC,
|
|
$collection['id']);
|
|
}
|
|
|
|
/**
|
|
* 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(
|
|
'COLLECTIONS: 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('COLLECTIONS: 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(
|
|
'COLLECTIONS: State not found for %s. Continuing by rquesting a SYNC.',
|
|
$id)
|
|
);
|
|
$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(
|
|
'COLLECTIONS: Unable to initialize state for %s. Ignoring during pollForChanges: %s.',
|
|
$id,
|
|
$e->getMessage())
|
|
);
|
|
continue;
|
|
} catch (Horde_ActiveSync_Exception_FolderGone $e) {
|
|
$this->_logger->warn('COLLECTIONS: Folder gone for collection ' . $collection['id']);
|
|
return self::COLLECTION_ERR_FOLDERSYNC_REQUIRED;
|
|
} catch (Horde_ActiveSync_Exception $e) {
|
|
$this->_logger->err('COLLECTIONS: 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('COLLECTIONS: Skipping %s because it is not PINGable.', $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(
|
|
'COLLECTIONS: SYNC terminating and force-clearing device state: %s',
|
|
$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(
|
|
'COLLECTIONS: SYNC terminating: %s',
|
|
$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('COLLECTIONS: Authentication lost during PING!!');
|
|
return self::COLLECTION_ERR_AUTHENTICATION;
|
|
} catch (Horde_ActiveSync_Exception $e) {
|
|
$this->_logger->err(sprintf(
|
|
'COLLECTIONS: Sync object cannot be configured, throttling: %s',
|
|
$e->getMessage())
|
|
);
|
|
$this->_sleep(30);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (!empty($dataavailable)) {
|
|
$this->_logger->info('COLLECTIONS: Found changes!');
|
|
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->meta('COLLECTIONS: Changes in cache determined during Sync Wait/Heartbeat, exiting here.');
|
|
|
|
return self::COLLECTION_ERR_STALE;
|
|
}
|
|
|
|
$this->_logger->meta(sprintf(
|
|
'COLLECTIONS: Looping Sync complete: DataAvailable: %s, DataImported: %s',
|
|
$dataavailable,
|
|
$this->importedChanges)
|
|
);
|
|
|
|
return $dataavailable;
|
|
}
|
|
|
|
/**
|
|
* Wait for specified interval, and close any backend connections while
|
|
* we wait.
|
|
*
|
|
* @param integer $interval The number of seconds to sleep.
|
|
*/
|
|
protected function _sleep($interval)
|
|
{
|
|
// Wait.
|
|
$this->_logger->info(sprintf(
|
|
'%sCOLLECTIONS: Sleeping for %s seconds.',
|
|
str_repeat('-', 10),
|
|
$interval));
|
|
|
|
// Close any backend connections.
|
|
$this->_cache->state->disconnect();
|
|
sleep ($interval);
|
|
$this->_cache->state->connect();
|
|
}
|
|
|
|
/**
|
|
* Check if we have any pingable collections.
|
|
*
|
|
* @return boolean True if we have collections marked as pingable.
|
|
*/
|
|
public function havePingableCollections()
|
|
{
|
|
foreach (array_keys($this->_collections) as $id) {
|
|
if ($this->_cache->collectionIsPingable($id)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Marks all loaded collections with a synckey as pingable.
|
|
*/
|
|
public function updatePingableFlag()
|
|
{
|
|
$collections = $this->_cache->getCollections(false);
|
|
foreach ($collections as $id => $collection) {
|
|
if (!empty($this->_collections[$id]['synckey'])) {
|
|
$this->_logger->meta(sprintf(
|
|
'COLLECTIONS: Setting collection %s (%s) PINGABLE.',
|
|
$collection['serverid'],
|
|
$id)
|
|
);
|
|
$this->_cache->setPingableCollection($id);
|
|
} else {
|
|
$this->_logger->meta(sprintf(
|
|
'COLLECTIONS: UNSETTING collection %s (%s) PINGABLE flag.',
|
|
$collection['serverid'],
|
|
$id)
|
|
);
|
|
$this->_cache->removePingableCollection($id);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Force reset all collection's PINGABLE flag. Used to force client
|
|
* to issue a non-empty PING request.
|
|
*
|
|
*/
|
|
public function resetPingCache()
|
|
{
|
|
$collections = $this->_cache->getCollections(false);
|
|
foreach ($collections as $id => $collection) {
|
|
$this->_logger->meta(sprintf(
|
|
'COLLECTIONS: UNSETTING collection %s (%s) PINGABLE flag.',
|
|
$collection['serverid'],
|
|
$id)
|
|
);
|
|
$this->_cache->removePingableCollection($id);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return any changes for the current collection.
|
|
*
|
|
* @param boolean $ping True if this is a PING request, false otherwise.
|
|
* If true, we only detect that a change has occured,
|
|
* not the data on all of the changes.
|
|
* @param array $ensure An array of UIDs that should be sent in the
|
|
* current response if possible, and not put off
|
|
* because of a MOREAVAILABLE situation.
|
|
* @deprecated and no longer used.
|
|
*
|
|
* @return array The changes array.
|
|
*/
|
|
public function getCollectionChanges($ping = false, array $ensure = array())
|
|
{
|
|
if (empty($this->_changes)) {
|
|
$this->_changes = $this->_as->state->getChanges(array('ping' => $ping));
|
|
}
|
|
|
|
return $this->_changes;
|
|
}
|
|
|
|
/**
|
|
* Return the count of the current collection's chagnes.
|
|
*
|
|
* @param boolean $ping Only ping the collection if true.
|
|
*
|
|
* @return integer The change count.
|
|
*/
|
|
public function getCollectionChangeCount($ping = false)
|
|
{
|
|
if (empty($this->_changes)) {
|
|
$this->getCollectionChanges($ping);
|
|
}
|
|
|
|
return count($this->_changes);
|
|
}
|
|
|
|
/**
|
|
* Iterator
|
|
*/
|
|
public function getIterator()
|
|
{
|
|
return new ArrayIterator($this->_collections);
|
|
}
|
|
|
|
}
|