Implement working policy enforcement and provisioning.
authorMichael J. Rubinsky <mrubinsk@horde.org>
Tue, 30 Mar 2010 03:22:57 +0000 (23:22 -0400)
committerMichael J. Rubinsky <mrubinsk@horde.org>
Tue, 30 Mar 2010 04:50:05 +0000 (00:50 -0400)
In the process, rework some state storage methods. For the file storage backend,
store the state files in a user directory to allow a (as yet non-existant) UI for
allowing the user and/or admin to request a remote device wipe. Also seperate out
the device-specific information such as the policykey, user-agent, device string etc...
into it's own state file. Again, this will be more efficient in a history/sql based state
storage driver.

framework/ActiveSync/lib/Horde/ActiveSync.php
framework/ActiveSync/lib/Horde/ActiveSync/Driver/Base.php
framework/ActiveSync/lib/Horde/ActiveSync/Request/Base.php
framework/ActiveSync/lib/Horde/ActiveSync/Request/FolderSync.php
framework/ActiveSync/lib/Horde/ActiveSync/Request/GetItemEstimate.php
framework/ActiveSync/lib/Horde/ActiveSync/Request/Provision.php
framework/ActiveSync/lib/Horde/ActiveSync/Request/Sync.php
framework/ActiveSync/lib/Horde/ActiveSync/State/Base.php
framework/ActiveSync/lib/Horde/ActiveSync/State/File.php
framework/ActiveSync/lib/Horde/ActiveSync/State/History.php
framework/Rpc/lib/Horde/Rpc/ActiveSync.php

index 8f24c76..2da5150 100644 (file)
@@ -447,17 +447,6 @@ define("SYNC_TRUNCATION_5K", 4);
 define("SYNC_TRUNCATION_SEVEN", 7);
 define("SYNC_TRUNCATION_ALL", 9);
 
-define("SYNC_PROVISION_STATUS_SUCCESS", 1);
-define("SYNC_PROVISION_STATUS_PROTERROR", 2);
-define("SYNC_PROVISION_STATUS_SERVERERROR", 3);
-define("SYNC_PROVISION_STATUS_DEVEXTMANAGED", 4);
-define("SYNC_PROVISION_STATUS_POLKEYMISM", 5);
-
-define("SYNC_PROVISION_RWSTATUS_NA", 0);
-define("SYNC_PROVISION_RWSTATUS_OK", 1);
-define("SYNC_PROVISION_RWSTATUS_PENDING", 2);
-define("SYNC_PROVISION_RWSTATUS_WIPED", 3);
-
 /**
  * Main ActiveSync class. Entry point for performing all ActiveSync operations
  *
@@ -868,12 +857,10 @@ class Horde_ActiveSync
           );
 
     /**
-     * Used to track what error code to send back to PIM on failure
+     * Provisioning support
      *
-     * @var integer
+     * @var string (TODO _constant this)
      */
-    protected $_statusCode = 0;
-
     protected $_provisioning;
 
     /**
@@ -1549,10 +1536,12 @@ class Horde_ActiveSync
      * @param $protocolversion
      * @return unknown_type
      */
-    public function handleRequest($cmd, $devId, $version)
+    public function handleRequest($cmd, $devId)
     {
         $class = 'Horde_ActiveSync_Request_' . basename($cmd);
+        $version = $this->getProtocolVersion();
         if (class_exists($class)) {
+            //@TODO: Remove version - get it in handle() since we pass self to it anyway
             $request = new $class($this->_driver,
                                   $this->_decoder,
                                   $this->_encoder,
@@ -1564,9 +1553,6 @@ class Horde_ActiveSync
             return $request->handle($this);
         }
 
-        // GetHierarchy is used in v1.0 of the AS protocol, in v2, it is replaced
-        // by the FolderSync command
-
         // @TODO: Leave the following in place until all are refactored...then throw
         // an error if the class does not exist.
         switch($cmd) {
@@ -1653,6 +1639,7 @@ class Horde_ActiveSync
 
     /**
      * Send the MS_Server-ActiveSync header
+     * (This is the version Exchange 2003 implements)
      *
      * @return void
      */
@@ -1698,15 +1685,12 @@ class Horde_ActiveSync
      */
     public function getPolicyKey()
     {
-        if (isset($this->_policykey)) {
-            return $this->_policykey;
-        }
-
         /* Policy key headers may be sent in either of these forms: */
         $this->_policykey = $this->_request->getHeader('X-Ms-Policykey');
         if (empty($this->_policykey)) {
             $this->_policykey = $this->_request->getHeader('X-MS-PolicyKey');
         }
+
         if (empty($this->_policykey)) {
             $this->_policykey = 0;
         }
index aab1b64..e9d5538 100644 (file)
@@ -241,6 +241,16 @@ abstract class Horde_ActiveSync_Driver_Base
     }
 
     /**
+     * Get the username for this request.
+     *
+     * @return string  The current username
+     */
+    public function getUser()
+    {
+        return $this->_authUser;
+    }
+
+    /**
      * Any code to run on log off
      *
      * @return boolean
@@ -326,7 +336,8 @@ abstract class Horde_ActiveSync_Driver_Base
     /**
      * Will (eventually) return an appropriate state object based on the class
      * being sync'd.
-     * @param <type> $collection
+     *
+     * @param array $collection
      */
     public function &getStateObject($collection = array())
     {
@@ -477,42 +488,7 @@ abstract class Horde_ActiveSync_Driver_Base
     }
 
     /**
-     * Checks if the sent policykey matches the latest policykey on the server
-     * TODO: Revisit this once we have refactored state storage
-     * @param string $policykey
-     * @param string $devid
-     *
-     * @return status flag
-     */
-    public function CheckPolicy($policyKey, $devId)
-    {
-        $status = SYNC_PROVISION_STATUS_SUCCESS;
-//        $user_policykey = $this->getPolicyKey($this->_authUser, $this->_authPass, $devId);
-//        if ($user_policykey != $policyKey) {
-//            $status = SYNC_PROVISION_STATUS_POLKEYMISM;
-//        }
-//
-        return $status;
-    }
-
-    /**
-     * Return a policy key for given user with a given device id.
-     * If there is no combination user-deviceid available, a new key
-     * should be generated.
-     *
-     * @param string $user
-     * @param string $pass
-     * @param string $devid
-     *
-     * @return unknown
-     */
-    public function getPolicyKey($user, $pass, $devid)
-    {
-        return false;
-    }
-
-    /**
-     * Generate a random policy key. Right now it's a 10-digit number.
+     * Generate a random 10 digit policy key
      *
      * @return unknown
      */
@@ -522,18 +498,6 @@ abstract class Horde_ActiveSync_Driver_Base
     }
 
     /**
-     * Set a new policy key for the given device id.
-     *
-     * @param string $policykey
-     * @param string $devid
-     * @return unknown
-     */
-    public function setPolicyKey($policykey, $devid)
-    {
-        return false;
-    }
-
-    /**
      * Return a device wipe status
      *
      * @param string $user
index 4812255..65713ed 100644 (file)
@@ -50,7 +50,7 @@ abstract class Horde_ActiveSync_Request_Base
      *
      * @var mixed
      */
-    protected $_provisioning = false;
+    protected $_provisioning;
 
     /**
      * The ActiveSync Version
@@ -66,6 +66,9 @@ abstract class Horde_ActiveSync_Request_Base
      */
     protected $_devId;
 
+    protected $_userAgent;
+    protected $_deviceType;
+
     /**
      * Used to track what error code to send back to PIM on failure
      *
@@ -113,10 +116,40 @@ abstract class Horde_ActiveSync_Request_Base
         /* Provisioning support */
         $this->_provisioning = $provisioning;
 
+        /* Useragent and device identification */
+       $this->_userAgent = $request->getHeader('User-Agent');
+       $get = $request->getGetParams();
+       $this->_deviceType = $get['DeviceType'];
+
         /* Logger */
         $this->_logger;
     }
 
+    /**
+     * Ensure the PIM's policy key is current.
+     *
+     * @param <type> $devId
+     * @return <type>
+     */
+    public function checkPolicyKey($sentKey)
+    {
+        /* Don't attempt if we don't care */
+        if ($this->_provisioning !== false) {
+            $state = $this->_driver->getStateObject();
+            $storedKey = $state->getPolicyKey($this->_devId);
+            /* Loose provsioning should allow a blank key */
+            if ($storedKey != $sentKey &&
+               ($this->_provisioning !== 'loose' ||
+               ($this->_provisioning === 'loose' && !empty($this->_policyKey)))) {
+
+                    Horde_ActiveSync::provisioningRequired();
+                    return false;
+            }
+        }
+
+        return true;
+    }
+
     public function setLogger(Horde_Log_Logger $logger) {
         $this->_logger = $logger;
     }
index 7a59b46..f333a87 100644 (file)
@@ -23,10 +23,15 @@ class Horde_ActiveSync_Request_FolderSync extends Horde_ActiveSync_Request_Base
 
     public function handle(Horde_ActiveSync $activeSync)
     {
-        /* Be optomistic */
+        /* Be optimistic */
         $this->_statusCode = self::STATUS_SUCCESS;
         $this->_logger->info('[Horde_ActiveSync::handleFolderSync] Beginning FOLDERSYNC');
 
+        /* Check policy */
+        if (!$this->checkPolicyKey($activeSync->getPolicyKey())) {
+            return false;
+        }
+
         /* Maps serverid -> clientid for items that are received from the PIM */
         $map = array();
 
index ed9fec4..264374d 100644 (file)
@@ -14,7 +14,6 @@
  */
 class Horde_ActiveSync_Request_GetItemEstimate extends Horde_ActiveSync_Request_Base
 {
-
     /** Status Codes **/
     const STATUS_SUCCESS = 1;
     const STATUS_INVALIDCOL = 2;
@@ -29,6 +28,13 @@ class Horde_ActiveSync_Request_GetItemEstimate extends Horde_ActiveSync_Request_
      */
     public function handle(Horde_ActiveSync $activeSync)
     {
+        $this->_logger->info('[Horde_ActiveSync::handleFolderSync] Beginning GETITEMESTIMATE');
+
+        /* Check policy */
+        if (!$this->checkPolicyKey($activeSync->getPolicyKey())) {
+            return false;
+        }
+
         $status = array();
         $collections = array();
 
@@ -111,40 +117,29 @@ class Horde_ActiveSync_Request_GetItemEstimate extends Horde_ActiveSync_Request_
         $this->_encoder->startTag(SYNC_GETITEMESTIMATE_GETITEMESTIMATE);
         foreach ($collections as $collection) {
             $this->_encoder->startTag(SYNC_GETITEMESTIMATE_RESPONSE);
-
             $this->_encoder->startTag(SYNC_GETITEMESTIMATE_STATUS);
             $this->_encoder->content($status[$collection['id']]);
             $this->_encoder->endTag();
-
             $this->_encoder->startTag(SYNC_GETITEMESTIMATE_FOLDER);
-
             $this->_encoder->startTag(SYNC_GETITEMESTIMATE_FOLDERTYPE);
             $this->_encoder->content($collection['class']);
             $this->_encoder->endTag();
-
             $this->_encoder->startTag(SYNC_GETITEMESTIMATE_FOLDERID);
             $this->_encoder->content($collection['id']);
             $this->_encoder->endTag();
-
             $this->_encoder->startTag(SYNC_GETITEMESTIMATE_ESTIMATE);
 
             $importer = new Horde_ActiveSync_ContentsCache();
-
             $state = $this->_driver->getStateObject($collection);
             $state->loadState($collection['synckey']);
-
             $exporter = $this->_driver->GetExporter();
             $exporter->init($state, $importer, $collection);
 
             $this->_encoder->content($exporter->GetChangeCount());
-
             $this->_encoder->endTag();
-
             $this->_encoder->endTag();
-
             $this->_encoder->endTag();
         }
-
         $this->_encoder->endTag();
 
         return true;
index 86cb7b7..240f9df 100644 (file)
 class Horde_ActiveSync_Request_Provision extends Horde_ActiveSync_Request_Base
 {
 
+    /* Status Constants */
+    const STATUS_SUCCESS = 1;
+    const STATUS_PROTERROR = 2;  // Global status
+    const STATUS_NOTDEFINED = 2; // Policy status
+
+    const STATUS_SERVERERROR = 3; // Global
+    const STATUS_POLICYUNKNOWN = 3; // Policy
+
+    const STATUS_DEVEXTMANAGED = 4; // Global
+    const STATUS_POLICYCORRUPT = 4; // Policy
+
+    const STATUS_POLKEYMISM = 5;
+
+    /* Client -> Server Status */
+    const STATUS_CLIENT_SUCCESS = 1;
+    const STATUS_CLIENT_PARTIAL = 2; // Only pin was enabled.
+    const STATUS_CLIENT_FAILED = 3; // No policies applied at all.
+    const STATUS_CLIENT_THIRDPARTY = 4; // Client provisioned by 3rd party?
+
+    const RWSTATUS_NA = 0;
+    const RWSTATUS_OK = 1;
+    const RWSTATUS_PENDING = 2;
+    const RWSTATUS_WIPED = 3;
+
+    /**
+     * Handle the Provision request. This is a 3-phase process. Phase 1 is
+     * actually the enforcement, when the server rejects a request and forces
+     * the client to perform this PROVISION request...so we are handling phase
+     * 2 (download policies) and 3 (acknowledge policies) here.
+     *
+     * @param Horde_ActiveSync $activeSync  The activesync object.
+     *
+     * @return boolean
+     * @throws Horde_ActiveSync_Exception
+     */
     public function handle(Horde_ActiveSync $activeSync)
     {
+        /* Get the policy key if it was sent */
         $policykey = $activeSync->getPolicyKey();
 
-        $status = SYNC_PROVISION_STATUS_SUCCESS;
+        $this->_logger->debug('PIM PolicyKey: ' . $policykey);
+        /* Be optimistic */
+        $status = self::STATUS_SUCCESS;
+        $policyStatus = self::STATUS_SUCCESS;
+
+        /* Start by assuming we are in stage 2 */
         $phase2 = true;
         if (!$this->_decoder->getElementStartTag(SYNC_PROVISION_PROVISION)) {
-            return false;
+            return $this->_globalError(self::STATUS_PROTERROR);
         }
 
-        //handle android remote wipe.
+        /* Handle android remote wipe */
         if ($this->_decoder->getElementStartTag(SYNC_PROVISION_REMOTEWIPE)) {
             if (!$this->_decoder->getElementStartTag(SYNC_PROVISION_STATUS)) {
-                return false;
+                return $this->_globalError(self::STATUS_PROTERROR);
             }
-
+            // TODO: Look at $status here...
             $status = $this->_decoder->getElementContent();
-
-            if (!$this->_decoder->getElementEndTag()) {
-                return false;
-            }
-
-            if (!$this->_decoder->getElementEndTag()) {
-                return false;
+            if (!$this->_decoder->getElementEndTag() ||
+                !$this->_decoder->getElementEndTag()) {
+                return $this->_globalError(self::STATUS_PROTERROR);
             }
         } else {
-            if (!$this->_decoder->getElementStartTag(SYNC_PROVISION_POLICIES)) {
-                return false;
-            }
-
-            if (!$this->_decoder->getElementStartTag(SYNC_PROVISION_POLICY)) {
-                return false;
-            }
+            if (!$this->_decoder->getElementStartTag(SYNC_PROVISION_POLICIES) ||
+                !$this->_decoder->getElementStartTag(SYNC_PROVISION_POLICY) ||
+                !$this->_decoder->getElementStartTag(SYNC_PROVISION_POLICYTYPE)) {
 
-            if (!$this->_decoder->getElementStartTag(SYNC_PROVISION_POLICYTYPE)) {
-                return false;
+                return $this->_globalError(self::STATUS_PROTERROR);
             }
 
             $policytype = $this->_decoder->getElementContent();
             if ($policytype != 'MS-WAP-Provisioning-XML') {
-                $status = SYNC_PROVISION_STATUS_SERVERERROR;
+                $policyStatus = self::STATUS_POLICYUNKNOWN;
             }
             if (!$this->_decoder->getElementEndTag()) {//policytype
-                return false;
+                return $this->_globalError(self::STATUS_PROTERROR);
             }
 
+            /* POLICYKEY is only sent by client in phase 3 */
             if ($this->_decoder->getElementStartTag(SYNC_PROVISION_POLICYKEY)) {
-                // This should be Phase 3 of the Provision conversation...
-                // We get the intermediate policy key sent back from the client.
-                // TODO: Still need to verify the key once we have some kind of
-                // storage for it.
                 $policykey = $this->_decoder->getElementContent();
-                if (!$this->_decoder->getElementEndTag()) {
-                    return false;
-                }
+                if (!$this->_decoder->getElementEndTag() ||
+                    !$this->_decoder->getElementStartTag(SYNC_PROVISION_STATUS)) {
 
-                if (!$this->_decoder->getElementStartTag(SYNC_PROVISION_STATUS)) {
-                    return false;
+                    return $this->_globalError(self::STATUS_PROTERROR);
+                }
+                if ($this->_decoder->getElementContent() != self::STATUS_SUCCESS) {
+                    $policyStatus = self::STATUS_POLICYCORRUPT;
                 }
-
-                $status = $this->_decoder->getElementContent();
-                //do status handling
-                $status = SYNC_PROVISION_STATUS_SUCCESS;
 
                 if (!$this->_decoder->getElementEndTag()) {
-                    return false;
+                    return $this->_globalError(self::STATUS_PROTERROR);
                 }
                 $phase2 = false;
             }
 
-            if (!$this->_decoder->getElementEndTag()) {//policy
-                return false;
-            }
+            if (!$this->_decoder->getElementEndTag() ||
+                !$this->_decoder->getElementEndTag()) {
 
-            if (!$this->_decoder->getElementEndTag()) {//policies
-                return false;
+                return $this->_globalError(self::STATUS_PROTERROR);
             }
 
+            /* Handle remote wipe for other devices */
             if ($this->_decoder->getElementStartTag(SYNC_PROVISION_REMOTEWIPE)) {
                 if (!$this->_decoder->getElementStartTag(SYNC_PROVISION_STATUS)) {
-                    return false;
+                    return $this->_globalError(self::STATUS_PROTERROR);
                 }
-
+                // @TODO: look at status here??
                 $status = $this->_decoder->getElementContent();
-                if (!$this->_decoder->getElementEndTag()) {
-                    return false;
-                }
+                if (!$this->_decoder->getElementEndTag() ||
+                    !$this->_decoder->getElementEndTag()) {
 
-                if (!$this->_decoder->getElementEndTag()) {
-                    return false;
+                    return $this->_globalError(self::STATUS_PROTERROR);
                 }
             }
         }
 
         if (!$this->_decoder->getElementEndTag()) {//provision
-            return false;
+            return $this->_globalError(self::STATUS_PROTERROR);
         }
+
+        /* Start handling request and sending output */
         $this->_encoder->StartWBXML();
 
         // End of Phase 3 - We create the "final" policy key, store it, then
         // send it to the client.
+        $state = $this->_driver->getStateObject();
         if (!$phase2) {
+            /* Verify intermediate key */
+            if ($state->getPolicyKey($this->_devId) != $policykey) {
+                $policyStatus = self::STATUS_POLKEYMISM;
+            } else {
+                $policykey = $this->_driver->generatePolicyKey();
+                $info = array('policykey' => $policykey,
+                              'useragent' => $this->_userAgent,
+                              'devicetype' => $this->_deviceType);
+                $state->setDeviceInfo($this->_devId, $info);
+            }
+        } elseif (empty($policykey)) {
+            // This is phase2 - we need to set the intermediate key
             $policykey = $this->_driver->generatePolicyKey();
-            $this->_driver->setPolicyKey($policykey, $this->_devId);
+            $state->setPolicyKey($this->_devId, $policykey);
         }
 
         $this->_encoder->startTag(SYNC_PROVISION_PROVISION);
-
         $this->_encoder->startTag(SYNC_PROVISION_STATUS);
         $this->_encoder->content($status);
         $this->_encoder->endTag();
 
         $this->_encoder->startTag(SYNC_PROVISION_POLICIES);
         $this->_encoder->startTag(SYNC_PROVISION_POLICY);
-
         $this->_encoder->startTag(SYNC_PROVISION_POLICYTYPE);
         $this->_encoder->content($policytype);
         $this->_encoder->endTag();
         $this->_encoder->startTag(SYNC_PROVISION_STATUS);
-        $this->_encoder->content($status);
+        $this->_encoder->content($policyStatus);
         $this->_encoder->endTag();
         $this->_encoder->startTag(SYNC_PROVISION_POLICYKEY);
         $this->_encoder->content($policykey);
         $this->_encoder->endTag();
-        if ($phase2) {
-            // If we are in Phase 2, send the security policies.
-            // TODO: Configure this!
+
+        /* Send security policies - configure this/move to it's own method...*/
+        if ($phase2 && $status == self::STATUS_SUCCESS && $policyStatus == self::STATUS_SUCCESS) {
             $this->_encoder->startTag(SYNC_PROVISION_DATA);
             if ($policytype == 'MS-WAP-Provisioning-XML') {
                 // Set 4131 to 0 to require a PIN, 4133
                 $this->_encoder->content('<wap-provisioningdoc><characteristic type="SecurityPolicy"><parm name="4131" value="1"/><parm name="4133" value="0"/></characteristic></wap-provisioningdoc>');
-            } else {
-                $this->_logger->err('Wrong policy type');
-                return false;
             }
             $this->_encoder->endTag();//data
         }
@@ -159,9 +192,9 @@ class Horde_ActiveSync_Request_Provision extends Horde_ActiveSync_Request_Base
         $rwstatus = $this->_driver->getDeviceRWStatus($this->_devId);
 
         //wipe data if status is pending or wiped
-        if ($rwstatus == SYNC_PROVISION_RWSTATUS_PENDING || $rwstatus == SYNC_PROVISION_RWSTATUS_WIPED) {
+        if ($rwstatus == self::RWSTATUS_PENDING || $rwstatus == self::RWSTATUS_WIPED) {
             $this->_encoder->startTag(SYNC_PROVISION_REMOTEWIPE, false, true);
-            $this->_driver->setDeviceRWStatus($this->_devId, SYNC_PROVISION_RWSTATUS_WIPED);
+            $this->_driver->setDeviceRWStatus($this->_devId, self::RWSTATUS_WIPED);
             //$rwstatus = SYNC_PROVISION_RWSTATUS_WIPED;
         }
 
@@ -170,4 +203,16 @@ class Horde_ActiveSync_Request_Provision extends Horde_ActiveSync_Request_Base
         return true;
     }
 
+    private function _globalError($status)
+    {
+        $this->_encoder->StartWBXML();
+        $this->_encoder->startTag(SYNC_PROVISION_PROVISION);
+        $this->_encoder->startTag(SYNC_PROVISION_STATUS);
+        $this->_encoder->content($status);
+        $this->_encoder->endTag();
+        $this->_encoder->endTag();
+
+        return false;
+    }
+
 }
\ No newline at end of file
index 257e71b..2e7596c 100644 (file)
@@ -30,6 +30,11 @@ class Horde_ActiveSync_Request_Sync extends Horde_ActiveSync_Request_Base
     {
         $this->_logger->info('[Horde_ActiveSync::handleSync] Handling SYNC command.');
 
+        /* Check policy */
+        if (!$this->checkPolicyKey($activeSync->getPolicyKey())) {
+            return false;
+        }
+
         /* Be optimistic */
         $this->_statusCode = self::STATUS_SUCCESS;
 
index 77b3248..8a3be98 100644 (file)
@@ -113,13 +113,20 @@ abstract class Horde_ActiveSync_State_Base
      * Loads the initial state from storage for the specified syncKey and
      * intializes the stateMachine for use.
      *
-     * @param string $key  The key for the syncState or pingState to load.
+     * @param string $key       The key for the syncState or pingState to load.
      *
      * @return array The state array
      */
     abstract public function loadState($syncKey);
 
     /**
+     * Load/initialize the ping state for the specified device.
+     *
+     * @param string $devId
+     */
+    abstract public function initPingState($devId);
+
+    /**
      * Load the ping state for the given device id
      *
      * @param string $devid  The device id.
@@ -169,6 +176,25 @@ abstract class Horde_ActiveSync_State_Base
     abstract public function isConflict($stat, $type);
 
     /**
+     * Get the specified device's policy key.
+     *
+     * @param string $devId     The device id to get key for.
+     *
+     * @return integer  The policy key
+     */
+    abstract public function getPolicyKey($devId);
+
+    /**
+     * Set the policy key for the specified device id
+     *
+     * @param string $devId     The device id
+     * @param integer $key      The policy key
+     *
+     * @return void
+     */
+    abstract public function setPolicyKey($devId, $key);
+
+    /**
      * Set the backend driver
      * (should really only be called by a backend object when passing this
      * object to client code)
index a2e58d0..211f85e 100644 (file)
@@ -92,27 +92,37 @@ class Horde_ActiveSync_State_File extends Horde_ActiveSync_State_Base
     /**
      * Load the sync state
      *
+     * @param string $syncKey   The synckey
+     *
      * @return void
      * @throws Horde_ActiveSync_Exception
      */
     public function loadState($syncKey)
     {
+        /* Make sure this user's state directory exists */
+        $dir = $this->_stateDir . '/' . $this->_backend->getUser();
+        if (!file_exists($dir)) {
+            if (!mkdir($dir)) {
+                throw new Horde_ActiveSync_Exception('Failed to create user state storage');
+            }
+        }
+
         /* Prime the state cache for the first sync */
         if (empty($syncKey)) {
             $this->_stateCache = array();
             return;
         }
 
-        // Check if synckey is allowed
+        /* Check if synckey is allowed */
         if (!preg_match('/^s{0,1}\{([0-9A-Za-z-]+)\}([0-9]+)$/', $syncKey, $matches)) {
             throw new Horde_ActiveSync_Exception('Invalid sync key');
         }
 
-        // Cleanup all older syncstates
+        /* Cleanup all older syncstates */
         $this->_gc($syncKey);
 
-        // Read current sync state
-        $filename = $this->_stateDir . '/' . $syncKey;
+        /* Read current sync state */
+        $filename = $dir . '/' . $syncKey;
         if (!file_exists($filename)) {
             throw new Horde_ActiveSync_Exception('Sync state not found');
         }
@@ -164,7 +174,9 @@ class Horde_ActiveSync_State_File extends Horde_ActiveSync_State_Base
      */
     public function save()
     {
-        return file_put_contents($this->_stateDir . '/' . $this->_syncKey, !empty($this->_stateCache) ? serialize($this->_stateCache) : '');
+        return file_put_contents(
+            $this->_stateDir . '/' . $this->_backend->getUser() . '/' . $this->_syncKey,
+            !empty($this->_stateCache) ? serialize($this->_stateCache) : '');
     }
 
     /**
@@ -218,10 +230,11 @@ class Horde_ActiveSync_State_File extends Horde_ActiveSync_State_Base
     }
 
     /**
-     * Save folder data for a specific device
+     * Save folder data for a specific device. Used only for compatibility with
+     * older (version 1) ActiveSync requests.
      *
-     * @param string $devId  The device Id
-     * @param array $folders The folder data
+     * @param string $devId     The device Id
+     * @param array $folders    The folder data
      *
      * @return boolean
      * @throws Horde_ActiveSync_Exception
@@ -254,14 +267,15 @@ class Horde_ActiveSync_State_File extends Horde_ActiveSync_State_Base
             $unique_folders[SYNC_FOLDER_TYPE_CONTACT] = SYNC_FOLDER_TYPE_DUMMY;
 
         }
-        if (!file_put_contents($this->_stateDir . '/compat-' . $devId, serialize($unique_folders))) {
+        if (!file_put_contents($this->_stateDir . '/' . $this->_backend->getUser() . '/compat-' . $devId, serialize($unique_folders))) {
             $this->logError('_saveFolderData: Data could not be saved!');
             throw new Horde_ActiveSync_Exception('Folder data could not be saved');
         }
     }
 
     /**
-     * Get the folder data for a specific device
+     * Get the folder data for a specific collection for a specific device. Used
+     * only with older (version 1) ActiveSync requests.
      *
      * @param string $devId  The device id
      * @param string $class  The folder class to fetch (Calendar, Contacts etc.)
@@ -270,7 +284,7 @@ class Horde_ActiveSync_State_File extends Horde_ActiveSync_State_Base
      */
     public function getFolderData($devId, $class)
     {
-        $filename = $this->_stateDir . '/compat-' . $devId;
+        $filename = $this->_stateDir . '/' . $this->_backend->getUser() . '/compat-' . $devId;
         if (file_exists($filename)) {
             $arr = unserialize(file_get_contents($filename));
             if ($class == "Calendar") {
@@ -285,7 +299,9 @@ class Horde_ActiveSync_State_File extends Horde_ActiveSync_State_Base
     }
 
     /**
-     * Return an array of known folders.
+     * Return an array of known folders. This is essentially the state for a
+     * FOLDERSYNC request. AS uses a seperate synckey for FOLDERSYNC requests
+     * also, so need to treat it as any other collection.
      *
      * @return array
      */
@@ -305,14 +321,14 @@ class Horde_ActiveSync_State_File extends Horde_ActiveSync_State_Base
      * Perform any initialization needed to deal with pingStates
      * For this driver, it loads the device's state file.
      *
-     * @param string $devId  The device id of the PIM to load PING state for
+     * @param string $devId     The device id of the PIM to load PING state for
      *
-     * @return The $collection array
+     * @return array The $collection array
      */
     public function initPingState($devId)
     {
         $this->_devId = $devId;
-        $file = $this->_stateDir . '/' . $devId;
+        $file = $this->_stateDir . '/' . $this->_backend->getUser() . '/' . $devId;
         if (file_exists($file)) {
             $this->_pingState = unserialize(file_get_contents($file));
         } else {
@@ -325,7 +341,38 @@ class Horde_ActiveSync_State_File extends Horde_ActiveSync_State_Base
     }
 
     /**
-     * Load a specific collection's ping state
+     * Obtain the device info array.
+     *
+     * @param <type> $devId
+     */
+    public function getDeviceInfo($devId)
+    {
+        $this->_devId = $devId;
+        $file = $this->_stateDir . '/' . $this->_backend->getUser() . '/info-' . $devId;
+        if (file_exists($file)) {
+            return unserialize(file_get_contents($file));
+        }
+
+        return array('policykey' => 0,
+                     'rwstatus' => 0,
+                     'devicetype' => '',
+                     'useragent' => '');
+    }
+
+    /**
+     * Set new device info
+     *
+     */
+    public function setDeviceInfo($devId, $data)
+    {
+        $this->_devId = $devId;
+        $file = $this->_stateDir . '/' . $this->_backend->getUser() . '/info-' . $devId;
+        return file_put_contents($file, serialize($data));
+    }
+
+    /**
+     * Load a specific collection's ping state. Ping state must already have
+     * been loaded.
      *
      * @param array $pingCollection  The collection array from the PIM request
      *
@@ -354,9 +401,6 @@ class Horde_ActiveSync_State_File extends Horde_ActiveSync_State_Base
         if (!$haveState) {
             $this->_logger->debug('Empty state for '. $pingCollection['class']);
 
-            /* Start with empty state cache */
-            //$this->_stateCache[$pingCollection['id']] = array();
-
             /* Init members for the getChanges call */
             $this->_syncKey = $this->_devId;
             $this->_collection = $pingCollection;
@@ -406,9 +450,10 @@ class Horde_ActiveSync_State_File extends Horde_ActiveSync_State_Base
         if (empty($this->_pingState)) {
             throw new Horde_ActiveSync_Exception('PING state not initialized');
         }
-        $state = serialize(array('lifetime' => $this->_pingState['lifetime'], 'collections' => $this->_pingState['collections']));
+        $state = serialize(array('lifetime' => $this->_pingState['lifetime'],
+                                 'collections' => $this->_pingState['collections']));
 
-        return file_put_contents($this->_stateDir . '/' . $this->_devId, $state);
+        return file_put_contents($this->_stateDir . '/' . $this->_backend->getUser() . '/' . $this->_devId, $state);
     }
 
     /**
@@ -434,10 +479,38 @@ class Horde_ActiveSync_State_File extends Horde_ActiveSync_State_Base
     }
 
     /**
+     * Obtain the current policy key, if it exists.
      *
-     * @param <type> $collectionClass  The collection we are syncing
+     * @param string $devId     The device id to obtain policy key for.
      *
-     * @return <type>
+     * @return integer  The current policy key for this device, or 0 if none
+     *                  exists.
+     */
+    public function getPolicyKey($devId)
+    {
+        $info = $this->getDeviceInfo($devId);
+        return $info['policykey'];
+    }
+
+    /**
+     * Save a new device policy key to storage.
+     *
+     * @param string $devId  The device id
+     * @param integer $key   The new policy key
+     */
+    public function setPolicyKey($devId, $key)
+    {
+        $info = $this->getDeviceInfo($devId);
+        $info['policykey'] = $key;
+        $this->setDeviceInfo($devId, $info);
+    }
+
+    /**
+     * Get list of server changes
+     *
+     * @param integer $flags
+     *
+     * @return array
      */
     public function getChanges($flags = 0)
     {
@@ -485,12 +558,17 @@ class Horde_ActiveSync_State_File extends Horde_ActiveSync_State_Base
         return $this->_changes;
     }
 
+    /**
+     * Get the number of server changes.
+     *
+     * @return integer
+     */
     public function getChangeCount()
     {
         if (!isset($this->_changes)) {
             $this->getChanges();
-            //throw new Horde_ActiveSync_Exception('Changes not yet retrieved. Must call getChanges() first');
         }
+
         return count($this->_changes);
     }
 
@@ -500,8 +578,8 @@ class Horde_ActiveSync_State_File extends Horde_ActiveSync_State_Base
      *
      * @params string $syncKey  The sync key
      *
+     * @return boolean
      * @throws Horde_ActiveSync_Exception
-     * @return boolean?
      */
     protected function _gc($syncKey)
     {
@@ -511,14 +589,14 @@ class Horde_ActiveSync_State_File extends Horde_ActiveSync_State_Base
         $guid = $matches[1];
         $n = $matches[2];
 
-        $dir = opendir($this->_stateDir);
+        $dir = @opendir($this->_stateDir . '/' . $this->_backend->getUser());
         if (!$dir) {
             return false;
         }
         while ($entry = readdir($dir)) {
             if (preg_match('/^s{0,1}\{([0-9A-Za-z-]+)\}([0-9]+)$/', $entry, $matches)) {
                 if ($matches[1] == $guid && $matches[2] < $n) {
-                    unlink($this->_stateDir . '/' . $entry);
+                    unlink($this->_stateDir . '/' . $this->_backend->getUser() . '/' . $entry);
                 }
             }
         }
@@ -527,9 +605,12 @@ class Horde_ActiveSync_State_File extends Horde_ActiveSync_State_Base
     }
 
     /**
+     * Helper function that performs the actual diff between PIM state and
+     * server state arrays.
+     *
+     * @param array $old  The PIM state
+     * @param array $new  The current server state
      *
-     * @param $old
-     * @param $new
      * @return unknown_type
      */
     private function _getDiff($old, $new)
@@ -609,6 +690,7 @@ class Horde_ActiveSync_State_File extends Horde_ActiveSync_State_Base
     }
 
     /**
+     * Helper function for the _diff method
      *
      * @param $a
      * @param $b
index a50e8ab..c1b2c1c 100644 (file)
@@ -74,7 +74,7 @@ class Horde_ActiveSync_State_History extends Horde_ActiveSync_State_Base
      * @return void
      * @throws Horde_ActiveSync_Exception
      */
-    public function loadState($syncKey)
+    public function loadState($syncKey, $username)
     {
         if (empty($syncKey)) {
             return;
index 7e8d5ea..b9af31a 100644 (file)
@@ -42,15 +42,6 @@ class Horde_Rpc_ActiveSync extends Horde_Rpc
     private $_policykey;
 
     /**
-     * ActiveSync protocol version
-     * (Sent as either 'Ms-Asprotocolversion' or 'MS-ASProtocolVersion')
-     * Should default to '1.0' if not sent.
-     *
-     * @var string
-     */
-    private $_protocolVersion;
-
-    /**
      * Require provisioning? Valid values:
      *  true  - provisioning required
      *  false - not checked
@@ -61,6 +52,7 @@ class Horde_Rpc_ActiveSync extends Horde_Rpc
      */
     private $_provisioning;
 
+
     /**
      * Constructor.
      * Parameters in addition to Horde_Rpc's:
@@ -96,9 +88,6 @@ class Horde_Rpc_ActiveSync extends Horde_Rpc
             $this->_policykey = $this->_server->getPolicyKey();
         }
         $this->_server->setProvisioning = $this->_provisioning;
-
-        /* Protocol Version */
-        $this->_protocolVersion = $this->_server->getProtocolVersion();
     }
 
     /**
@@ -145,19 +134,8 @@ class Horde_Rpc_ActiveSync extends Horde_Rpc
 
             // Do the actual request
             $this->_logger->debug('Horde_Rpc_ActiveSync::getResponse() starting for ' . $this->_get['Cmd']);
-            if (!$this->_server->handleRequest($this->_get['Cmd'], $this->_get['DeviceId'], $this->_protocolVersion)) {
-                /* @TODO If request failed, try to output a reasonable error to the
-                 * device if we can...and this should be done from the ActiveSync
-                 * objects anyway...
-                 */
-                if(!headers_sent()) {
-                    header('Content-type: text/html');
-                    echo("<BODY>\n");
-                    echo("<h3>Error</h3><p>\n");
-                    echo("There was a problem processing the <i>{$this->_get['Cmd']}</i> command from your PDA.\n");
-                    echo("</BODY>\n");
-                }
-            }
+            $this->_server->handleRequest($this->_get['Cmd'], $this->_get['DeviceId']);
+
             break;
 
         case 'GET':
@@ -244,18 +222,6 @@ class Horde_Rpc_ActiveSync extends Horde_Rpc
             echo 'Access denied or user ' . $this->_get['User'] . ' unknown.';
         }
 
-        /* Policies / Provisioning */
-        if ($this->_provisioning !== false && $this->_request->getServer('REQUEST_METHOD') != 'OPTIONS' &&
-            $this->_get['Cmd'] != 'Ping' && $this->_get['Cmd'] != 'Provision' &&
-            $this->_backend->CheckPolicy($this->_policykey, $this->_get['DeviceId']) != SYNC_PROVISION_STATUS_SUCCESS &&
-            ($this->_provisioning !== 'loose' ||
-            ($this->_provisioning === 'loose' && !empty($this->_policyKey)))) {
-
-            Horde_ActiveSync::provisioningRequired();
-
-            return false;
-        }
-
         $this->_logger->debug('Horde_Rpc_ActiveSync::authorize() exiting');
 
         return true;