diff --git a/lib/Horde/ActiveSync/Collections.php b/lib/Horde/ActiveSync/Collections.php index bd43d8d0..c537388f 100644 --- a/lib/Horde/ActiveSync/Collections.php +++ b/lib/Horde/ActiveSync/Collections.php @@ -1232,10 +1232,14 @@ public function pollForChanges($heartbeat, $interval, array $options = []) $rw_check_countdown = 5; if ($this->_as->provisioning != Horde_ActiveSync::PROVISIONING_NONE) { $rwstatus = $this->_as->state->getDeviceRWStatus($this->_as->device->id, true); + $accountOnlyStatus = $this->_as->state->getAccountOnlyRWStatus( + $this->_as->device->id, + true + ); if ($rwstatus == Horde_ActiveSync::RWSTATUS_PENDING || $rwstatus == Horde_ActiveSync::RWSTATUS_WIPED - || $rwstatus == Horde_ActiveSync::RWSTATUS_ACCOUNTONLY_PENDING - || $rwstatus == Horde_ActiveSync::RWSTATUS_ACCOUNTONLY_WIPED) { + || $accountOnlyStatus == Horde_ActiveSync::RWSTATUS_ACCOUNTONLY_PENDING + || $accountOnlyStatus == Horde_ActiveSync::RWSTATUS_ACCOUNTONLY_WIPED) { return self::COLLECTION_ERR_FOLDERSYNC_REQUIRED; } } diff --git a/lib/Horde/ActiveSync/Request/Base.php b/lib/Horde/ActiveSync/Request/Base.php index b47a9590..211200e8 100644 --- a/lib/Horde/ActiveSync/Request/Base.php +++ b/lib/Horde/ActiveSync/Request/Base.php @@ -185,12 +185,21 @@ public function checkPolicyKey($sentKey, $requestType = null) // Did we request a remote wipe? $rwStatus = $this->_state->getDeviceRWStatus($this->_device->id); - if ($rwStatus == Horde_ActiveSync::RWSTATUS_PENDING - || $rwStatus == Horde_ActiveSync::RWSTATUS_ACCOUNTONLY_PENDING) { + if ($rwStatus == Horde_ActiveSync::RWSTATUS_PENDING) { $this->_requireProvisionWbxml($requestType, Horde_ActiveSync_Status::REMOTEWIPE_REQUESTED); return false; } + $accountOnlyStatus = $this->_state->getAccountOnlyRWStatus($this->_device->id); + if ($accountOnlyStatus == Horde_ActiveSync::RWSTATUS_ACCOUNTONLY_PENDING) { + $this->_requireProvisionWbxml($requestType, Horde_ActiveSync_Status::REMOTEWIPE_REQUESTED); + return false; + } + if ($accountOnlyStatus == Horde_ActiveSync::RWSTATUS_ACCOUNTONLY_WIPED) { + $this->_requireProvisionWbxml($requestType, Horde_ActiveSync_Status::DEVICE_NOT_PROVISIONED); + return false; + } + // Validate the stored key against the device key, honoring // the value of _provisioning. if ((empty($storedKey) || $storedKey != $sentKey) diff --git a/lib/Horde/ActiveSync/Request/Provision.php b/lib/Horde/ActiveSync/Request/Provision.php index 5f3721f7..b3df3134 100644 --- a/lib/Horde/ActiveSync/Request/Provision.php +++ b/lib/Horde/ActiveSync/Request/Provision.php @@ -89,15 +89,23 @@ protected function _handle() return $this->_globalError(self::STATUS_PROTERROR); } if ($status == self::STATUS_CLIENT_SUCCESS) { + if ($wipeTag === Horde_ActiveSync::PROVISION_ACCOUNTONLYREMOTEWIPE) { + return $this->_completeAccountOnlyWipeAck(); + } $this->_state->setDeviceRWStatus( - $this->_devId, - $wipeTag === Horde_ActiveSync::PROVISION_ACCOUNTONLYREMOTEWIPE - ? Horde_ActiveSync::RWSTATUS_ACCOUNTONLY_WIPED - : Horde_ActiveSync::RWSTATUS_WIPED + $this->_device->id, + Horde_ActiveSync::RWSTATUS_WIPED ); } $policytype = Horde_ActiveSync::POLICYTYPE_XML; } else { + if ($this->_state->getAccountOnlyRWStatus($this->_device->id) + == Horde_ActiveSync::RWSTATUS_ACCOUNTONLY_WIPED) { + $this->_sendNoProvisionNeededResponse(self::STATUS_SUCCESS); + + return true; + } + if ($deviceinfo = $this->_handleSettings()) { $deviceinfo['version'] = $this->_device->version; $this->_device->setDeviceProperties($deviceinfo); @@ -177,11 +185,12 @@ protected function _handle() return $this->_globalError(self::STATUS_PROTERROR); } if ($status == self::STATUS_CLIENT_SUCCESS) { + if ($wipeTag === Horde_ActiveSync::PROVISION_ACCOUNTONLYREMOTEWIPE) { + return $this->_completeAccountOnlyWipeAck(); + } $this->_state->setDeviceRWStatus( $this->_device->id, - $wipeTag === Horde_ActiveSync::PROVISION_ACCOUNTONLYREMOTEWIPE - ? Horde_ActiveSync::RWSTATUS_ACCOUNTONLY_WIPED - : Horde_ActiveSync::RWSTATUS_WIPED + Horde_ActiveSync::RWSTATUS_WIPED ); } } @@ -282,23 +291,22 @@ protected function _handle() // Remote wipe if requested. $rwstatus = $this->_state->getDeviceRWStatus($this->_device->id); if ($rwstatus == Horde_ActiveSync::RWSTATUS_PENDING - || $rwstatus == Horde_ActiveSync::RWSTATUS_WIPED - || $rwstatus == Horde_ActiveSync::RWSTATUS_ACCOUNTONLY_PENDING - || $rwstatus == Horde_ActiveSync::RWSTATUS_ACCOUNTONLY_WIPED) { - $isAccountOnly = ($rwstatus == Horde_ActiveSync::RWSTATUS_ACCOUNTONLY_PENDING - || $rwstatus == Horde_ActiveSync::RWSTATUS_ACCOUNTONLY_WIPED); + || $rwstatus == Horde_ActiveSync::RWSTATUS_WIPED) { $this->_encoder->startTag( - $isAccountOnly - ? Horde_ActiveSync::PROVISION_ACCOUNTONLYREMOTEWIPE - : Horde_ActiveSync::PROVISION_REMOTEWIPE, + Horde_ActiveSync::PROVISION_REMOTEWIPE, false, true ); $this->_state->setDeviceRWStatus( $this->_device->id, - $isAccountOnly - ? Horde_ActiveSync::RWSTATUS_ACCOUNTONLY_WIPED - : Horde_ActiveSync::RWSTATUS_WIPED + Horde_ActiveSync::RWSTATUS_WIPED + ); + } elseif ($this->_state->getAccountOnlyRWStatus($this->_device->id) + == Horde_ActiveSync::RWSTATUS_ACCOUNTONLY_PENDING) { + $this->_encoder->startTag( + Horde_ActiveSync::PROVISION_ACCOUNTONLYREMOTEWIPE, + false, + true ); } $this->_encoder->endTag(); //provision @@ -306,6 +314,23 @@ protected function _handle() return true; } + /** + * Finalize a successful account-only remote wipe acknowledgment. + * + * @return boolean + */ + protected function _completeAccountOnlyWipeAck() + { + $this->_state->setAccountOnlyRWStatus( + $this->_device->id, + $this->_device->user, + Horde_ActiveSync::RWSTATUS_ACCOUNTONLY_WIPED + ); + $this->_sendNoProvisionNeededResponse(self::STATUS_SUCCESS); + + return true; + } + /** * Send a WBXML response to the output stream indicating that no * provision requests are necessary. diff --git a/lib/Horde/ActiveSync/State/Base.php b/lib/Horde/ActiveSync/State/Base.php index a4ffe850..91e76270 100644 --- a/lib/Horde/ActiveSync/State/Base.php +++ b/lib/Horde/ActiveSync/State/Base.php @@ -371,6 +371,32 @@ public function getDeviceRWStatus($devId, $refresh = false) return $this->_deviceInfo->rwstatus; } + /** + * Obtain the account-only remote wipe status for the loaded device user. + * + * @param string $devId The device id. + * @param boolean $refresh If true, reload from storage. + * + * @return integer + */ + public function getAccountOnlyRWStatus($devId, $refresh = false) + { + if (empty($this->_deviceInfo) || $this->_deviceInfo->id != $devId) { + throw new Horde_ActiveSync_Exception('Device not loaded.'); + } + + if ($refresh) { + $this->loadDeviceInfo( + $this->_deviceInfo->id, + $this->_deviceInfo->user, + ['force' => true] + ); + } + + return $this->_deviceInfo->accountOnlyRwstatus + ?? Horde_ActiveSync::RWSTATUS_NA; + } + /** * Set the backend driver * (should really only be called by a backend object when passing this @@ -1317,12 +1343,25 @@ abstract public function resetAllPolicyKeys(); * Set a new remotewipe status for the device * * @param string $devId The device id. - * @param string $status A Horde_ActiveSync::RWSTATUS_* constant. + * @param string $status A device-level Horde_ActiveSync::RWSTATUS_* + * constant. Account-only wipe statuses must be set + * via setAccountOnlyRWStatus(). * * @throws Horde_ActiveSync_Exception */ abstract public function setDeviceRWStatus($devId, $status); + /** + * Set account-only remote wipe status for a device user. + * + * @param string $devId The device id. + * @param string $user The device user. + * @param string $status A Horde_ActiveSync::RWSTATUS_* constant. + * + * @throws Horde_ActiveSync_Exception + */ + abstract public function setAccountOnlyRWStatus($devId, $user, $status); + /** * Obtain the device object. * diff --git a/lib/Horde/ActiveSync/State/Mongo.php b/lib/Horde/ActiveSync/State/Mongo.php index de8dd8ca..101c7de0 100644 --- a/lib/Horde/ActiveSync/State/Mongo.php +++ b/lib/Horde/ActiveSync/State/Mongo.php @@ -111,6 +111,8 @@ class Horde_ActiveSync_State_Mongo extends Horde_ActiveSync_State_Base implement public const DEVICE_USER = 'device_user'; public const DEVICE_USERS_USER = 'users.device_user'; public const DEVICE_USERS_POLICYKEY = 'users.device_policykey'; + public const DEVICE_ACCOUNTONLY_RWSTATUS = 'device_accountonly_rwstatus'; + public const DEVICE_USERS_ACCOUNTONLY_RWSTATUS = 'users.device_accountonly_rwstatus'; public const DEVICE_POLICYKEY = 'device_policykey'; /** @@ -1120,6 +1122,8 @@ public function loadDeviceInfo($devId, $user = null, $params = []) foreach ($device_data['users'] as $user_entry) { if ($user_entry[self::DEVICE_USER] == $user) { $device['policykey'] = $user_entry[self::DEVICE_POLICYKEY]; + $device['accountOnlyRwstatus'] = $user_entry[self::DEVICE_ACCOUNTONLY_RWSTATUS] + ?? Horde_ActiveSync::RWSTATUS_NA; break; } } @@ -1347,6 +1351,13 @@ public function resetAllPolicyKeys() */ public function setDeviceRWStatus($devId, $status) { + if ($status == Horde_ActiveSync::RWSTATUS_ACCOUNTONLY_PENDING + || $status == Horde_ActiveSync::RWSTATUS_ACCOUNTONLY_WIPED) { + throw new Horde_ActiveSync_Exception( + 'Account-only remote wipe status must be set via setAccountOnlyRWStatus().' + ); + } + $query = [self::MONGO_ID => $devId]; $new_data = [self::DEVICE_RWSTATUS => $status]; $update = ['$set' => $new_data]; @@ -1357,8 +1368,7 @@ public function setDeviceRWStatus($devId, $status) throw new Horde_ActiveSync_Exception($e); } - if ($status == Horde_ActiveSync::RWSTATUS_PENDING - || $status == Horde_ActiveSync::RWSTATUS_ACCOUNTONLY_PENDING) { + if ($status == Horde_ActiveSync::RWSTATUS_PENDING) { $new_data[self::DEVICE_USERS_POLICYKEY] = 0; $cursor = $this->_db->selectCollection(self::COLLECTION_DEVICE) ->find($query, ['users' => true]); @@ -1379,6 +1389,52 @@ public function setDeviceRWStatus($devId, $status) } } + /** + * Set account-only remote wipe status for a device user. + * + * @param string $devId The device id. + * @param string $user The device user. + * @param string $status A Horde_ActiveSync::RWSTATUS_* constant. + * + * @throws Horde_ActiveSync_Exception + */ + public function setAccountOnlyRWStatus($devId, $user, $status) + { + $query = [ + self::MONGO_ID => $devId, + self::DEVICE_USERS_USER => $user, + ]; + $update = [ + '$set' => [ + self::DEVICE_USERS_ACCOUNTONLY_RWSTATUS => $status, + ], + ]; + try { + $this->_db->selectCollection(self::COLLECTION_DEVICE)->update($query, $update); + } catch (Exception $e) { + $this->_logger->err($e->getMessage()); + throw new Horde_ActiveSync_Exception($e); + } + + if ($status == Horde_ActiveSync::RWSTATUS_ACCOUNTONLY_PENDING) { + try { + $this->_db->selectCollection(self::COLLECTION_DEVICE)->update( + $query, + ['$set' => [self::DEVICE_USERS_POLICYKEY => 0]] + ); + } catch (Exception $e) { + $this->_logger->err($e->getMessage()); + throw new Horde_ActiveSync_Exception($e); + } + } + + if (!empty($this->_deviceInfo) + && $this->_deviceInfo->id == $devId + && $this->_deviceInfo->user == $user) { + $this->_deviceInfo->accountOnlyRwstatus = $status; + } + } + /** * Reset the sync state for this device, for the specified collection. * @@ -1518,8 +1574,6 @@ public function removeState(array $options) '$or' => [ [self::DEVICE_RWSTATUS => Horde_ActiveSync::RWSTATUS_PENDING], [self::DEVICE_RWSTATUS => Horde_ActiveSync::RWSTATUS_WIPED], - [self::DEVICE_RWSTATUS => Horde_ActiveSync::RWSTATUS_ACCOUNTONLY_PENDING], - [self::DEVICE_RWSTATUS => Horde_ActiveSync::RWSTATUS_ACCOUNTONLY_WIPED], ], ]; try { diff --git a/lib/Horde/ActiveSync/State/Sql.php b/lib/Horde/ActiveSync/State/Sql.php index 74b311e1..f780a549 100644 --- a/lib/Horde/ActiveSync/State/Sql.php +++ b/lib/Horde/ActiveSync/State/Sql.php @@ -1173,11 +1173,13 @@ public function loadDeviceInfo($devId, $user = null, $params = []) throw new Horde_ActiveSync_Exception($e); } + $duser = []; if (!empty($user)) { - $query = 'SELECT device_policykey FROM ' . $this->_syncUsersTable + $query = 'SELECT device_policykey, device_accountonly_rwstatus FROM ' + . $this->_syncUsersTable . ' WHERE device_id = ? AND device_user = ?'; try { - $duser = $this->_db->selectOne($query, [$devId, $user]); + $duser = $this->_db->selectOne($query, [$devId, $user]) ?: []; } catch (Horde_Db_Exception $e) { throw new Horde_ActiveSync_Exception($e); } @@ -1185,6 +1187,9 @@ public function loadDeviceInfo($devId, $user = null, $params = []) $this->_deviceInfo = new Horde_ActiveSync_Device($this); $this->_deviceInfo->rwstatus = $device['device_rwstatus']; + $this->_deviceInfo->accountOnlyRwstatus = !empty($duser['device_accountonly_rwstatus']) + ? $duser['device_accountonly_rwstatus'] + : Horde_ActiveSync::RWSTATUS_NA; $this->_deviceInfo->deviceType = $device['device_type']; $this->_deviceInfo->userAgent = $device['device_agent']; $this->_deviceInfo->id = $devId; @@ -1351,7 +1356,8 @@ public function deviceExists($devId, $user = null) public function listDevices($user = null, $filter = []) { $query = 'SELECT d.device_id AS device_id, device_type, device_agent,' - . ' device_policykey, device_rwstatus, device_user, device_properties FROM ' + . ' device_policykey, device_rwstatus, device_accountonly_rwstatus,' + . ' device_user, device_properties FROM ' . $this->_syncDeviceTable . ' d INNER JOIN ' . $this->_syncUsersTable . ' u ON d.device_id = u.device_id'; $values = []; @@ -1461,6 +1467,13 @@ public function resetAllPolicyKeys() */ public function setDeviceRWStatus($devId, $status) { + if ($status == Horde_ActiveSync::RWSTATUS_ACCOUNTONLY_PENDING + || $status == Horde_ActiveSync::RWSTATUS_ACCOUNTONLY_WIPED) { + throw new Horde_ActiveSync_Exception( + 'Account-only remote wipe status must be set via setAccountOnlyRWStatus().' + ); + } + $query = 'UPDATE ' . $this->_syncDeviceTable . ' SET device_rwstatus = ?' . ' WHERE device_id = ?'; $values = [$status, $devId]; @@ -1470,8 +1483,7 @@ public function setDeviceRWStatus($devId, $status) throw new Horde_ActiveSync_Exception($e); } - if ($status == Horde_ActiveSync::RWSTATUS_PENDING - || $status == Horde_ActiveSync::RWSTATUS_ACCOUNTONLY_PENDING) { + if ($status == Horde_ActiveSync::RWSTATUS_PENDING) { // Need to clear the policykey to force a PROVISION. Clear ALL // entries, to ensure the device is wiped. $query = 'UPDATE ' . $this->_syncUsersTable @@ -1484,6 +1496,44 @@ public function setDeviceRWStatus($devId, $status) } } + /** + * Set account-only remote wipe status for a device user. + * + * @param string $devId The device id. + * @param string $user The device user. + * @param string $status A Horde_ActiveSync::RWSTATUS_* constant. + * + * @throws Horde_ActiveSync_Exception + */ + public function setAccountOnlyRWStatus($devId, $user, $status) + { + $query = 'UPDATE ' . $this->_syncUsersTable + . ' SET device_accountonly_rwstatus = ?' + . ' WHERE device_id = ? AND device_user = ?'; + $values = [$status, $devId, $user]; + try { + $this->_db->update($query, $values); + } catch (Horde_Db_Exception $e) { + throw new Horde_ActiveSync_Exception($e); + } + + if ($status == Horde_ActiveSync::RWSTATUS_ACCOUNTONLY_PENDING) { + $query = 'UPDATE ' . $this->_syncUsersTable + . ' SET device_policykey = 0 WHERE device_id = ? AND device_user = ?'; + try { + $this->_db->update($query, [$devId, $user]); + } catch (Horde_Db_Exception $e) { + throw new Horde_ActiveSync_Exception($e); + } + } + + if (!empty($this->_deviceInfo) + && $this->_deviceInfo->id == $devId + && $this->_deviceInfo->user == $user) { + $this->_deviceInfo->accountOnlyRwstatus = $status; + } + } + /** * Explicitly remove a state from storage. * diff --git a/migration/Horde/ActiveSync/25_horde_activesync_peruser_accountonly_rwstatus.php b/migration/Horde/ActiveSync/25_horde_activesync_peruser_accountonly_rwstatus.php new file mode 100644 index 00000000..96b2d0e9 --- /dev/null +++ b/migration/Horde/ActiveSync/25_horde_activesync_peruser_accountonly_rwstatus.php @@ -0,0 +1,36 @@ +_connection->columns('horde_activesync_device_users'); + if (!isset($columns['device_accountonly_rwstatus'])) { + $this->addColumn( + 'horde_activesync_device_users', + 'device_accountonly_rwstatus', + 'integer', + ['default' => 0] + ); + } + + // Account-only wipe status was incorrectly stored per device_id. + $this->_connection->update( + 'UPDATE horde_activesync_device SET device_rwstatus = ?' + . ' WHERE device_rwstatus IN (?, ?)', + [ + Horde_ActiveSync::RWSTATUS_OK, + Horde_ActiveSync::RWSTATUS_ACCOUNTONLY_PENDING, + Horde_ActiveSync::RWSTATUS_ACCOUNTONLY_WIPED, + ] + ); + } + + public function down() + { + $this->removeColumn( + 'horde_activesync_device_users', + 'device_accountonly_rwstatus' + ); + } +} diff --git a/test/unit/Horde/ActiveSync/AccountOnlyRemoteWipeTest.php b/test/unit/Horde/ActiveSync/AccountOnlyRemoteWipeTest.php new file mode 100644 index 00000000..437628fc --- /dev/null +++ b/test/unit/Horde/ActiveSync/AccountOnlyRemoteWipeTest.php @@ -0,0 +1,190 @@ +markTestSkipped('PDO SQLite extension is not loaded'); + } + + $migrationDir = dirname(__DIR__, 4) . '/migration/Horde/ActiveSync'; + $this->db = new Horde_Db_Adapter_Pdo_Sqlite([ + 'dbname' => ':memory:', + 'charset' => 'utf-8', + ]); + + $migrator = new Horde_Db_Migration_Migrator( + $this->db, + new Horde_ActiveSync_Log_Logger(new Horde_Log_Handler_Null()), + [ + 'migrationsPath' => $migrationDir, + 'schemaTableName' => 'horde_activesync_schema_info', + ] + ); + $migrator->up(); + + $this->state = new Horde_ActiveSync_State_Sql(['db' => $this->db]); + } + + public function testLoadDeviceInfoWithoutUserDefaultsAccountOnlyStatusToNa() + { + $this->_saveDeviceUser('dev123', 'alice@example.com'); + + $device = $this->state->loadDeviceInfo('dev123'); + + $this->assertEquals(Horde_ActiveSync::RWSTATUS_NA, $device->accountOnlyRwstatus); + $this->assertEquals(0, $device->policykey); + } + + public function testGetAccountOnlyRWStatusReturnsNaWhenUnset() + { + $this->_saveDeviceUser('dev123', 'alice@example.com'); + $this->state->loadDeviceInfo('dev123'); + + $this->assertEquals( + Horde_ActiveSync::RWSTATUS_NA, + $this->state->getAccountOnlyRWStatus('dev123') + ); + } + + public function testSetAccountOnlyRWStatusPendingClearsOnlyTargetUserPolicyKey() + { + $this->_saveDeviceUser('dev123', 'alice@example.com', 100); + $this->_saveDeviceUser('dev123', 'bob@example.com', 200); + + $this->state->setAccountOnlyRWStatus( + 'dev123', + 'alice@example.com', + Horde_ActiveSync::RWSTATUS_ACCOUNTONLY_PENDING + ); + + $alice = $this->state->loadDeviceInfo('dev123', 'alice@example.com'); + $this->assertEquals( + Horde_ActiveSync::RWSTATUS_ACCOUNTONLY_PENDING, + $alice->accountOnlyRwstatus + ); + $this->assertEquals(0, $alice->policykey); + + $bob = $this->state->loadDeviceInfo('dev123', 'bob@example.com'); + $this->assertEquals(Horde_ActiveSync::RWSTATUS_NA, $bob->accountOnlyRwstatus); + $this->assertEquals(200, $bob->policykey); + } + + public function testSetAccountOnlyRWStatusWipedPersistsPerUser() + { + $this->_saveDeviceUser('dev123', 'alice@example.com'); + $this->_saveDeviceUser('dev123', 'bob@example.com'); + + $this->state->setAccountOnlyRWStatus( + 'dev123', + 'alice@example.com', + Horde_ActiveSync::RWSTATUS_ACCOUNTONLY_WIPED + ); + + $this->state->loadDeviceInfo('dev123', 'alice@example.com'); + $this->assertEquals( + Horde_ActiveSync::RWSTATUS_ACCOUNTONLY_WIPED, + $this->state->getAccountOnlyRWStatus('dev123', true) + ); + + $bob = $this->state->loadDeviceInfo('dev123', 'bob@example.com'); + $this->assertEquals(Horde_ActiveSync::RWSTATUS_NA, $bob->accountOnlyRwstatus); + } + + public function testSetAccountOnlyRWStatusUpdatesLoadedDeviceCache() + { + $this->_saveDeviceUser('dev123', 'alice@example.com'); + $this->state->loadDeviceInfo('dev123', 'alice@example.com'); + + $this->state->setAccountOnlyRWStatus( + 'dev123', + 'alice@example.com', + Horde_ActiveSync::RWSTATUS_ACCOUNTONLY_PENDING + ); + + $this->assertEquals( + Horde_ActiveSync::RWSTATUS_ACCOUNTONLY_PENDING, + $this->state->getAccountOnlyRWStatus('dev123') + ); + } + + public function testListDevicesIncludesAccountOnlyRwstatus() + { + $this->_saveDeviceUser('dev123', 'alice@example.com'); + $this->state->setAccountOnlyRWStatus( + 'dev123', + 'alice@example.com', + Horde_ActiveSync::RWSTATUS_ACCOUNTONLY_PENDING + ); + + $devices = $this->state->listDevices(); + $this->assertCount(1, $devices); + $this->assertEquals( + Horde_ActiveSync::RWSTATUS_ACCOUNTONLY_PENDING, + $devices[0]['device_accountonly_rwstatus'] + ); + } + + public function testSetDeviceRWStatusRejectsAccountOnlyPending() + { + $this->expectException(Horde_ActiveSync_Exception::class); + $this->expectExceptionMessage('setAccountOnlyRWStatus()'); + + $this->state->setDeviceRWStatus( + 'dev123', + Horde_ActiveSync::RWSTATUS_ACCOUNTONLY_PENDING + ); + } + + public function testSetDeviceRWStatusRejectsAccountOnlyWiped() + { + $this->expectException(Horde_ActiveSync_Exception::class); + $this->expectExceptionMessage('setAccountOnlyRWStatus()'); + + $this->state->setDeviceRWStatus( + 'dev123', + Horde_ActiveSync::RWSTATUS_ACCOUNTONLY_WIPED + ); + } + + protected function _saveDeviceUser($devId, $user, $policykey = 456) + { + $device = new Horde_ActiveSync_Device($this->state); + $device->rwstatus = Horde_ActiveSync::RWSTATUS_NA; + $device->deviceType = 'Test Device'; + $device->userAgent = 'Horde Tests'; + $device->id = $devId; + $device->user = $user; + $device->policykey = $policykey; + $device->supported = []; + $device->save(); + } +}