initial import
authorLars Kneschke <l.kneschke@metaways.de>
Fri, 20 Jan 2012 14:28:50 +0000 (15:28 +0100)
committerLars Kneschke <l.kneschke@metaways.de>
Fri, 20 Jan 2012 14:28:50 +0000 (15:28 +0100)
108 files changed:
docs/syncope.sql [new file with mode: 0644]
lib/Syncope/Backend/ContentState.php [new file with mode: 0644]
lib/Syncope/Backend/Device.php [new file with mode: 0644]
lib/Syncope/Backend/FolderState.php [new file with mode: 0644]
lib/Syncope/Backend/IContentState.php [new file with mode: 0644]
lib/Syncope/Backend/IDevice.php [new file with mode: 0644]
lib/Syncope/Backend/IFolderState.php [new file with mode: 0755]
lib/Syncope/Backend/ISyncState.php [new file with mode: 0644]
lib/Syncope/Backend/SyncState.php [new file with mode: 0644]
lib/Syncope/Command/FolderSync.php [new file with mode: 0644]
lib/Syncope/Command/Interface.php [new file with mode: 0644]
lib/Syncope/Command/Provision.php [new file with mode: 0644]
lib/Syncope/Command/Sync.php [new file with mode: 0644]
lib/Syncope/Command/Wbxml.php [new file with mode: 0644]
lib/Syncope/Data/Calendar.php [new file with mode: 0644]
lib/Syncope/Data/Contacts.php [new file with mode: 0644]
lib/Syncope/Data/Email.php [new file with mode: 0644]
lib/Syncope/Data/Factory.php [new file with mode: 0644]
lib/Syncope/Data/IData.php [new file with mode: 0644]
lib/Syncope/Data/Tasks.php [new file with mode: 0644]
lib/Syncope/Exception/NotFound.php [new file with mode: 0644]
lib/Syncope/Model/ContentState.php [new file with mode: 0644]
lib/Syncope/Model/Device.php [new file with mode: 0644]
lib/Syncope/Model/FolderState.php [new file with mode: 0644]
lib/Syncope/Model/IContentState.php [new file with mode: 0644]
lib/Syncope/Model/IDevice.php [new file with mode: 0644]
lib/Syncope/Model/IFolderState.php [new file with mode: 0644]
lib/Syncope/Model/ISyncState.php [new file with mode: 0644]
lib/Syncope/Model/SyncState.php [new file with mode: 0644]
lib/Zend/Config.php [new file with mode: 0644]
lib/Zend/Config/Exception.php [new file with mode: 0644]
lib/Zend/Config/Ini.php [new file with mode: 0644]
lib/Zend/Config/Writer.php [new file with mode: 0644]
lib/Zend/Config/Writer/Array.php [new file with mode: 0644]
lib/Zend/Config/Writer/Ini.php [new file with mode: 0644]
lib/Zend/Config/Writer/Xml.php [new file with mode: 0644]
lib/Zend/Config/Xml.php [new file with mode: 0644]
lib/Zend/Db.php [new file with mode: 0644]
lib/Zend/Db/Adapter/Abstract.php [new file with mode: 0644]
lib/Zend/Db/Adapter/Db2.php [new file with mode: 0644]
lib/Zend/Db/Adapter/Db2/Exception.php [new file with mode: 0644]
lib/Zend/Db/Adapter/Exception.php [new file with mode: 0644]
lib/Zend/Db/Adapter/Mysqli.php [new file with mode: 0644]
lib/Zend/Db/Adapter/Mysqli/Exception.php [new file with mode: 0644]
lib/Zend/Db/Adapter/Oracle.php [new file with mode: 0644]
lib/Zend/Db/Adapter/Oracle/Exception.php [new file with mode: 0644]
lib/Zend/Db/Adapter/Pdo/Abstract.php [new file with mode: 0644]
lib/Zend/Db/Adapter/Pdo/Ibm.php [new file with mode: 0644]
lib/Zend/Db/Adapter/Pdo/Ibm/Db2.php [new file with mode: 0644]
lib/Zend/Db/Adapter/Pdo/Ibm/Ids.php [new file with mode: 0644]
lib/Zend/Db/Adapter/Pdo/Mssql.php [new file with mode: 0644]
lib/Zend/Db/Adapter/Pdo/Mysql.php [new file with mode: 0644]
lib/Zend/Db/Adapter/Pdo/Oci.php [new file with mode: 0644]
lib/Zend/Db/Adapter/Pdo/Pgsql.php [new file with mode: 0644]
lib/Zend/Db/Adapter/Pdo/Sqlite.php [new file with mode: 0644]
lib/Zend/Db/Adapter/Sqlsrv.php [new file with mode: 0644]
lib/Zend/Db/Adapter/Sqlsrv/Exception.php [new file with mode: 0644]
lib/Zend/Db/Exception.php [new file with mode: 0644]
lib/Zend/Db/Expr.php [new file with mode: 0644]
lib/Zend/Db/Profiler.php [new file with mode: 0644]
lib/Zend/Db/Profiler/Exception.php [new file with mode: 0644]
lib/Zend/Db/Profiler/Firebug.php [new file with mode: 0644]
lib/Zend/Db/Profiler/Query.php [new file with mode: 0644]
lib/Zend/Db/Select.php [new file with mode: 0644]
lib/Zend/Db/Select/Exception.php [new file with mode: 0644]
lib/Zend/Db/Statement.php [new file with mode: 0644]
lib/Zend/Db/Statement/Db2.php [new file with mode: 0644]
lib/Zend/Db/Statement/Db2/Exception.php [new file with mode: 0644]
lib/Zend/Db/Statement/Exception.php [new file with mode: 0644]
lib/Zend/Db/Statement/Interface.php [new file with mode: 0644]
lib/Zend/Db/Statement/Mysqli.php [new file with mode: 0644]
lib/Zend/Db/Statement/Mysqli/Exception.php [new file with mode: 0644]
lib/Zend/Db/Statement/Oracle.php [new file with mode: 0644]
lib/Zend/Db/Statement/Oracle/Exception.php [new file with mode: 0644]
lib/Zend/Db/Statement/Pdo.php [new file with mode: 0644]
lib/Zend/Db/Statement/Pdo/Ibm.php [new file with mode: 0644]
lib/Zend/Db/Statement/Pdo/Oci.php [new file with mode: 0644]
lib/Zend/Db/Statement/Sqlsrv.php [new file with mode: 0644]
lib/Zend/Db/Statement/Sqlsrv/Exception.php [new file with mode: 0644]
lib/Zend/Db/Table.php [new file with mode: 0644]
lib/Zend/Db/Table/Abstract.php [new file with mode: 0644]
lib/Zend/Db/Table/Definition.php [new file with mode: 0644]
lib/Zend/Db/Table/Exception.php [new file with mode: 0644]
lib/Zend/Db/Table/Row.php [new file with mode: 0644]
lib/Zend/Db/Table/Row/Abstract.php [new file with mode: 0644]
lib/Zend/Db/Table/Row/Exception.php [new file with mode: 0644]
lib/Zend/Db/Table/Rowset.php [new file with mode: 0644]
lib/Zend/Db/Table/Rowset/Abstract.php [new file with mode: 0644]
lib/Zend/Db/Table/Rowset/Exception.php [new file with mode: 0644]
lib/Zend/Db/Table/Select.php [new file with mode: 0644]
lib/Zend/Db/Table/Select/Exception.php [new file with mode: 0644]
lib/Zend/Exception.php [new file with mode: 0644]
lib/Zend/Loader.php [new file with mode: 0644]
lib/Zend/Loader/Autoloader.php [new file with mode: 0644]
lib/Zend/Loader/Autoloader/Interface.php [new file with mode: 0644]
lib/Zend/Loader/Autoloader/Resource.php [new file with mode: 0644]
lib/Zend/Loader/Exception.php [new file with mode: 0644]
lib/Zend/Loader/PluginLoader.php [new file with mode: 0644]
lib/Zend/Loader/PluginLoader/Exception.php [new file with mode: 0644]
lib/Zend/Loader/PluginLoader/Interface.php [new file with mode: 0644]
lib/Zend/Registry.php [new file with mode: 0644]
tests/Syncope/Backend/DeviceTests.php [new file with mode: 0644]
tests/Syncope/Backend/FolderStateTests.php [new file with mode: 0644]
tests/Syncope/Backend/SyncStateTests.php [new file with mode: 0644]
tests/Syncope/Command/FolderSyncTests.php [new file with mode: 0644]
tests/Syncope/Command/SyncTests.php [new file with mode: 0644]
tests/bootstrap.php [new file with mode: 0644]
tests/config.xml [new file with mode: 0644]

diff --git a/docs/syncope.sql b/docs/syncope.sql
new file mode 100644 (file)
index 0000000..6e11706
--- /dev/null
@@ -0,0 +1,48 @@
+CREATE TABLE IF NOT EXISTS `syncope_devices` (
+    `id` varchar(40) NOT NULL, `name`,
+    `deviceid` varchar(64) NOT NULL,                                                                                                                                                                         
+    `devicetype` varchar(64) NOT NULL,
+    `policykey` varchar(64) DEFAULT NULL,
+    `owner_id` varchar(40) NOT NULL,
+    `acsversion` varchar(40) NOT NULL,
+    `pinglifetime` int(11) DEFAULT NULL,
+    `remotewipe` int(11) DEFAULT '0',
+    PRIMARY KEY (`id`)
+);
+
+CREATE TABLE IF NOT EXISTS `syncope_folderstates` (
+  `id` varchar(40) NOT NULL,
+  `device_id` varchar(64) NOT NULL,
+  `class` varchar(64) NOT NULL,
+  `folderid` varchar(254) NOT NULL,
+  `creation_time` datetime NOT NULL,
+  `lastfiltertype` int(11) DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `device_id--class--folderid` (`device_id`(40),`class`(40),`folderid`(40)),
+  KEY `folderstates::device_id--devices::id` (`device_id`),
+  CONSTRAINT `folderstates::device_id--devices::id` FOREIGN KEY (`device_id`) REFERENCES `devices` (`id`) ON DELETE CASCADE ON UPDATE CASCADE 
+);
+
+CREATE TABLE `syncope_synckeys` (
+  `device_id` varchar(64) NOT NULL DEFAULT '',
+  `type` varchar(64) NOT NULL DEFAULT '',
+  `counter` int(11) unsigned NOT NULL DEFAULT '0',
+  `lastsync` datetime DEFAULT NULL,
+  `pendingdata` longblob,
+  PRIMARY KEY (`device_id`,`type`,`counter`),
+  CONSTRAINT `syncope_synckeys::device_id--syncope_devices::id` FOREIGN KEY (`device_id`) REFERENCES `syncope_devices` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+CREATE TABLE `syncope_contents` (                                                                                                                                           
+  `id` varchar(40) NOT NULL,                                                                                                                                                                               
+  `device_id` varchar(64) DEFAULT NULL,                                                                                                                                                                    
+  `class` varchar(64) DEFAULT NULL,                                                                                                                                                                        
+  `contentid` varchar(64) DEFAULT NULL,                                                                                                                                                                    
+  `collectionid` varchar(254) DEFAULT NULL,                                                                                                                                                                
+  `creation_time` datetime DEFAULT NULL,                                                                                                                                                                   
+  `is_deleted` tinyint(1) unsigned DEFAULT '0',                                                                                                                                                            
+  PRIMARY KEY (`id`),                                                                                                                                                                                      
+  UNIQUE KEY `device_id--class--collectionid--contentid` (`device_id`(40),`class`(40),`collectionid`(40),`contentid`(40)),                                                                                 
+  KEY `acsync_contents::device_id--acsync_devices::id` (`device_id`),                                                                                                                                        
+  CONSTRAINT `acsync_contents::device_id--acsync_devices::id` FOREIGN KEY (`device_id`) REFERENCES `syncope_devices` (`id`) ON DELETE CASCADE ON UPDATE CASCADE                                         
+);
\ No newline at end of file
diff --git a/lib/Syncope/Backend/ContentState.php b/lib/Syncope/Backend/ContentState.php
new file mode 100644 (file)
index 0000000..9787aef
--- /dev/null
@@ -0,0 +1,138 @@
+<?php
+/**
+ * Tine 2.0
+ *
+ * @package     Syncope
+ * @subpackage  Backend
+ * @license     http://www.tine20.org/licenses/agpl-nonus.txt AGPL Version 1 (Non-US)
+ *              NOTE: According to sec. 8 of the AFFERO GENERAL PUBLIC LICENSE (AGPL), 
+ *              Version 1, the distribution of the Syncope module in or to the 
+ *              United States of America is excluded from the scope of this license.
+ * @author      Lars Kneschke <l.kneschke@metaways.de>
+ * @copyright   Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * 
+ */
+
+/**
+ * sql backend class for the folder state
+ *
+ * @package     Syncope
+ * @subpackage  Backend
+ */
+class Syncope_Backend_ContentState implements Syncope_Backend_IContentState
+{
+    /**
+     * the database adapter
+     *
+     * @var Zend_Db_Adapter_Abstract
+     */
+    protected $_db;
+    
+    public function __construct(Zend_Db_Adapter_Abstract $_db)
+    {
+        $this->_db = $_db;
+    }
+    
+    /**
+     * create new content state
+     *
+     * @param Syncope_Model_IContentState $_state
+     * @return Syncope_Model_IContentState
+     */
+    public function create(Syncope_Model_IContentState $_state)
+    {
+        $id = sha1(mt_rand(). microtime());
+        $deviceId = $_state->device_id instanceof Syncope_Model_IDevice ? $_state->device_id->id : $_state->device_id;
+    
+        $this->_db->insert('syncope_contentstates', array(
+               'id'            => $id, 
+               'device_id'     => $deviceId,
+               'class'         => $_state->class,
+               'collectionid'  => $_state->collectionid,
+               'contentid'     => $_state->contentid,
+               'creation_time' => $_state->creation_time->format('Y-m-d H:i:s'),
+               'is_deleted'    => isset($_state->is_deleted) ? (int)!!$_state->is_deleted : 0
+        ));
+        
+        return $this->get($id);
+    }
+    
+    /**
+     * mark state as deleted. The state gets removed finally, 
+     * when the synckey gets validated during next sync.
+     * 
+     * @param Syncope_Model_IContentState|string $_id
+     */
+    public function delete($_id)
+    {
+        $id = $_id instanceof Syncope_Model_IContentState ? $_id->id : $_id;
+        
+        $this->_db->update('syncope_contentstates', array(
+                       'is_deleted' => 1
+        ), array(
+                       'id = ?' => $id
+        ));
+        
+    }
+    
+    /**
+     * @param string  $_id
+     * @throws Syncope_Exception_NotFound
+     * @return Syncope_Model_IContentState
+     */
+    public function get($_id)
+    {
+        $select = $this->_db->select()
+            ->from('syncope_contentstates')
+            ->where('id = ?', $_id);
+    
+        $stmt = $this->_db->query($select);
+        $state = $stmt->fetchObject('Syncope_Model_ContentState');
+        
+        if (! $state instanceof Syncope_Model_IContentState) {
+            throw new Syncope_Exception_NotFound('id not found');
+        }
+        
+        if (!empty($state->creation_time)) {
+            $state->creation_time = new DateTime($state->creation_time, new DateTimeZone('utc'));
+        }
+    
+        return $state;
+    }
+    /**
+    * get array of ids which got send to the client for a given class
+    *
+    * @param Syncope_Model_IDevice $_deviceId
+    * @param string $_class
+    * @return array
+    */
+    public function getClientState($_deviceId, $_class, $_collectionId)
+    {
+        $deviceId = $_deviceId instanceof Syncope_Model_IDevice ? $_deviceId->id : $_deviceId;
+                
+        $select = $this->_db->select()
+            ->from('syncope_contentstates', 'contentid')
+            ->where($this->_db->quoteIdentifier('device_id') . ' = ?',    $deviceId)
+            ->where($this->_db->quoteIdentifier('class') . ' = ?',        $_class)
+            ->where($this->_db->quoteIdentifier('collectionid') . ' = ?', $_collectionId);
+
+        
+        $stmt = $this->_db->query($select);
+        $result = $stmt->fetchAll(Zend_Db::FETCH_COLUMN);
+    
+        return $result;
+    }
+    
+    public function resetState($_deviceId, $_class, $_collectionId)
+    {
+        $deviceId = $_deviceId instanceof Syncope_Model_IDevice ? $_deviceId->id : $_deviceId;
+         
+        $where = array(
+            $this->_db->quoteInto($this->_db->quoteIdentifier('device_id') . ' = ?',    $deviceId),
+            $this->_db->quoteInto($this->_db->quoteIdentifier('class') . ' = ?',        $_class),
+            $this->_db->quoteInto($this->_db->quoteIdentifier('collectionid') . ' = ?', $_collectionId)
+        );
+        
+        $this->_db->delete('syncope_contentstates', $where);
+    }
+}
diff --git a/lib/Syncope/Backend/Device.php b/lib/Syncope/Backend/Device.php
new file mode 100644 (file)
index 0000000..2a2b76d
--- /dev/null
@@ -0,0 +1,94 @@
+<?php
+
+/**
+ * Syncope
+ *
+ * @package     Command
+ * @license     http://www.tine20.org/licenses/agpl-nonus.txt AGPL Version 1 (Non-US)
+ *              NOTE: According to sec. 8 of the AFFERO GENERAL PUBLIC LICENSE (AGPL), 
+ *              Version 1, the distribution of the Tine 2.0 Syncope module in or to the 
+ *              United States of America is excluded from the scope of this license.
+ * @copyright   Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Lars Kneschke <l.kneschke@metaways.de>
+ */
+
+/**
+ * class to handle ActiveSync Sync command
+ *
+ * @package     Backend
+ */
+class Syncope_Backend_Device implements Syncope_Backend_IDevice
+{
+    /**
+     * the database adapter
+     * 
+     * @var Zend_Db_Adapter_Abstract
+     */
+    protected $_db;
+    
+    public function __construct(Zend_Db_Adapter_Abstract $_db)
+    {
+        $this->_db = $_db;
+    }
+
+    /**
+     * create new device
+     * 
+     * @param Syncope_Model_IDevice $_device
+     * @return Syncope_Model_IDevice
+     */
+    public function create(Syncope_Model_IDevice $_device)
+    {
+        $id = sha1(mt_rand(). microtime());
+        
+        $this->_db->insert('syncope_devices', array(
+               'id'         => $id, 
+               'deviceid'   => $_device->deviceid,
+               'devicetype' => $_device->devicetype,
+               'policykey'  => $_device->policykey,
+               'owner_id'   => $_device->owner_id,
+               'acsversion' => $_device->acsversion,
+               'remotewipe' => $_device->remotewipe
+        ));
+        
+        return $this->get($id);
+    }
+    
+    /**
+     * @param string  $_id
+     * @throws Syncope_Exception_NotFound
+     * @return Syncope_Model_IDevice
+     */
+    public function get($_id)
+    {
+        $select = $this->_db->select()
+            ->from('syncope_devices')
+            ->where('id = ?', $_id);
+            
+        $stmt = $select->query();
+        $result = $stmt->fetchAll();
+        
+        if (count($result) == 0) {
+            throw new Syncope_Exception_NotFound('id not found');
+        }
+        
+        $device = new Syncope_Model_Device($result[0]);
+        
+        return $device;
+    }
+    
+    public function delete($_id)
+    {
+        $id = $_id instanceof Syncope_Model_IDevice ? $_id->id : $id;
+        
+        $result = $this->_db->delete('syncope_devices', array('id' => $id));
+        
+        return (bool) $result;
+    }
+    
+    public function update(Syncope_Model_IDevice $_device)
+    {
+        
+    }
+}
diff --git a/lib/Syncope/Backend/FolderState.php b/lib/Syncope/Backend/FolderState.php
new file mode 100644 (file)
index 0000000..1756c01
--- /dev/null
@@ -0,0 +1,165 @@
+<?php
+/**
+ * Tine 2.0
+ *
+ * @package     Syncope
+ * @subpackage  Backend
+ * @license     http://www.tine20.org/licenses/agpl-nonus.txt AGPL Version 1 (Non-US)
+ *              NOTE: According to sec. 8 of the AFFERO GENERAL PUBLIC LICENSE (AGPL), 
+ *              Version 1, the distribution of the Syncope module in or to the 
+ *              United States of America is excluded from the scope of this license.
+ * @author      Lars Kneschke <l.kneschke@metaways.de>
+ * @copyright   Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * 
+ */
+
+/**
+ * sql backend class for the folder state
+ *
+ * @package     Syncope
+ * @subpackage  Backend
+ */
+class Syncope_Backend_FolderState implements Syncope_Backend_IFolderState
+{
+    /**
+     * the database adapter
+     *
+     * @var Zend_Db_Adapter_Abstract
+     */
+    protected $_db;
+    
+    public function __construct(Zend_Db_Adapter_Abstract $_db)
+    {
+        $this->_db = $_db;
+    }
+    
+    /**
+     * create new folder state
+     *
+     * @param Syncope_Model_IFolderState $_folderState
+     * @return Syncope_Model_IFolderState
+     */
+    public function create(Syncope_Model_IFolderState $_folderState)
+    {
+        $id = sha1(mt_rand(). microtime());
+        $deviceId = $_folderState->device_id instanceof Syncope_Model_IDevice ? $_folderState->device_id->id : $_folderState->device_id;
+    
+        $this->_db->insert('syncope_folderstates', array(
+               'id'             => $id, 
+               'device_id'      => $deviceId,
+               'class'          => $_folderState->class,
+               'folderid'       => $_folderState->folderid,
+               'creation_time'  => $_folderState->creation_time->format('Y-m-d H:i:s'),
+               'lastfiltertype' => $_folderState->lastfiltertype
+        ));
+        
+        return $this->get($id);
+    }
+    
+    /**
+     * @param string  $_id
+     * @throws Syncope_Exception_NotFound
+     * @return Syncope_Model_IFolderState
+     */
+    public function get($_id)
+    {
+        $select = $this->_db->select()
+            ->from('syncope_folderstates')
+            ->where('id = ?', $_id);
+    
+        $stmt = $this->_db->query($select);
+        $state = $stmt->fetchObject('Syncope_Model_FolderState');
+        
+        if (! $state instanceof Syncope_Model_IFolderState) {
+            throw new Syncope_Exception_NotFound('id not found');
+        }
+        
+        if (!empty($state->creation_time)) {
+            $state->creation_time = new DateTime($state->creation_time, new DateTimeZone('utc'));
+        }
+    
+        return $state;
+    }
+    
+    /**
+     * delete all stored folderId's for given device
+     *
+     * @param Syncope_Model_Device|string $_deviceId
+     * @param string $_class
+     */
+    public function resetState($_deviceId)
+    {
+        $deviceId = $_deviceId instanceof Syncope_Model_IDevice ? $_deviceId->id : $_deviceId;
+         
+        $where = array(
+            $this->_db->quoteInto($this->_db->quoteIdentifier('device_id') . ' = ?', $deviceId)
+        );
+        
+        $this->_db->delete('syncope_folderstates', $where);
+    }
+    
+    public function update(Syncope_Model_IFolderState $_state)
+    {
+        $deviceId = $_state->device_id instanceof Syncope_Model_IDevice ? $_state->device_id->id : $_state->device_id;
+    
+        $this->_db->update('syncope_folderstates', array(
+               'lastfiltertype'     => $_state->lastfiltertype
+        ), array(
+               'id = ?' => $_state->id
+        ));
+    
+        return $this->get($_state->id);
+    }
+    
+    /**
+     * get array of ids which got send to the client for a given class
+     *
+     * @param Syncope_Model_Device|string $_deviceId
+     * @param string $_class
+     * @return array
+     */
+    public function getClientState($_deviceId, $_class)
+    {
+        $deviceId = $_deviceId instanceof Syncope_Model_IDevice ? $_deviceId->id : $_deviceId;
+        
+        $select = $this->_db->select()
+            ->from('syncope_folderstates', array('folderid'))
+            ->where($this->_db->quoteIdentifier('device_id') . ' = ?', $deviceId)
+            ->where($this->_db->quoteIdentifier('class') . ' = ?', $_class);
+        
+        $stmt = $this->_db->query($select);
+        $result = $stmt->fetchAll(Zend_Db::FETCH_COLUMN);
+
+        return $result;
+    }
+    
+    /**
+     * get array of ids which got send to the client for a given class
+     *
+     * @param Syncope_Model_Device|string $_deviceId
+     * @param string $_class
+     * @return array
+     */
+    public function getFolderState($_deviceId, $_folderId)
+    {
+        $deviceId = $_deviceId instanceof Syncope_Model_IDevice ? $_deviceId->id : $_deviceId;
+        
+        $select = $this->_db->select()
+            ->from('syncope_folderstates')
+            ->where($this->_db->quoteIdentifier('device_id') . ' = ?', $deviceId)
+            ->where($this->_db->quoteIdentifier('folderid') . ' = ?', $_folderId);
+        
+        $stmt = $this->_db->query($select);
+        $state = $stmt->fetchObject('Syncope_Model_FolderState');
+        
+        if (! $state instanceof Syncope_Model_IFolderState) {
+            throw new Syncope_Exception_NotFound('id not found');
+        }
+        
+        if (!empty($state->creation_time)) {
+            $state->creation_time = new DateTime($state->creation_time, new DateTimeZone('utc'));
+        }
+        
+        return $state;
+    }
+}
diff --git a/lib/Syncope/Backend/IContentState.php b/lib/Syncope/Backend/IContentState.php
new file mode 100644 (file)
index 0000000..9906fe1
--- /dev/null
@@ -0,0 +1,31 @@
+<?php
+/**
+ * Tine 2.0
+ *
+ * @package     Syncope
+ * @subpackage  Backend
+ * @license     http://www.tine20.org/licenses/agpl-nonus.txt AGPL Version 1 (Non-US)
+ *              NOTE: According to sec. 8 of the AFFERO GENERAL PUBLIC LICENSE (AGPL), 
+ *              Version 1, the distribution of the Syncope module in or to the 
+ *              United States of America is excluded from the scope of this license.
+ * @author      Lars Kneschke <l.kneschke@metaways.de>
+ * @copyright   Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * 
+ */
+
+/**
+ * sql backend class for the folder state
+ *
+ * @package     Syncope
+ * @subpackage  Backend
+ */
+interface Syncope_Backend_IContentState
+{
+    /**
+     * create new content state
+     *
+     * @param Syncope_Model_IContentState $_contentState
+     * @return Syncope_Model_IContentState
+     */
+    public function create(Syncope_Model_IContentState $_contentState);    
+}
diff --git a/lib/Syncope/Backend/IDevice.php b/lib/Syncope/Backend/IDevice.php
new file mode 100644 (file)
index 0000000..efc3569
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * Syncope
+ *
+ * @package     Command
+ * @license     http://www.tine20.org/licenses/agpl-nonus.txt AGPL Version 1 (Non-US)
+ *              NOTE: According to sec. 8 of the AFFERO GENERAL PUBLIC LICENSE (AGPL), 
+ *              Version 1, the distribution of the Tine 2.0 Syncope module in or to the 
+ *              United States of America is excluded from the scope of this license.
+ * @copyright   Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Lars Kneschke <l.kneschke@metaways.de>
+ */
+
+/**
+ * class to handle ActiveSync Sync command
+ *
+ * @package     Backend
+ */
+interface Syncope_Backend_IDevice
+{
+    /**
+    * Create a new device
+    *
+    * @param  Syncope_Model_IDevice $_device
+    * @return Syncope_Model_IDevice
+    */
+    public function create(Syncope_Model_IDevice $_device);
+    
+    /**
+     * Deletes one or more existing devices
+     *
+     * @param string|array $_id
+     * @return void
+     */
+    public function delete($_id);
+    
+    /**
+     * Return a single device
+     *
+     * @param string $_id
+     * @return Syncope_Model_IDevice
+     */
+    public function get($_id);
+    
+    /**
+     * Upates an existing persistent record
+     *
+     * @param  Syncope_Model_IDevice $_device
+     * @return Syncope_Model_IDevice
+     */
+    public function update(Syncope_Model_IDevice $_device);
+}
diff --git a/lib/Syncope/Backend/IFolderState.php b/lib/Syncope/Backend/IFolderState.php
new file mode 100755 (executable)
index 0000000..a72a6e4
--- /dev/null
@@ -0,0 +1,46 @@
+<?php
+/**
+ * Tine 2.0
+ *
+ * @package     Syncope
+ * @subpackage  Backend
+ * @license     http://www.tine20.org/licenses/agpl-nonus.txt AGPL Version 1 (Non-US)
+ *              NOTE: According to sec. 8 of the AFFERO GENERAL PUBLIC LICENSE (AGPL), 
+ *              Version 1, the distribution of the Syncope module in or to the 
+ *              United States of America is excluded from the scope of this license.
+ * @author      Lars Kneschke <l.kneschke@metaways.de>
+ * @copyright   Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * 
+ */
+
+/**
+ * sql backend class for the folder state
+ *
+ * @package     Syncope
+ * @subpackage  Backend
+ */
+interface Syncope_Backend_IFolderState
+{
+    /**
+     * @param Syncope_Model_IFolderState $_folderState
+     * @return Syncope_Model_IFolderState
+     */
+    public function create(Syncope_Model_IFolderState $_folderState);
+    
+    /**
+     * delete all stored folderId's for given device
+     *
+     * @param Syncope_Model_Device $_deviceId
+     * @param string $_class
+     */
+    public function resetState($_deviceId);
+    
+    /**
+     * get array of ids which got send to the client for a given class
+     *
+     * @param Syncope_Model_Device $_deviceId
+     * @param string $_class
+     * @return array
+     */
+    public function getClientState($_deviceId, $_class);
+}
diff --git a/lib/Syncope/Backend/ISyncState.php b/lib/Syncope/Backend/ISyncState.php
new file mode 100644 (file)
index 0000000..280442f
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+/**
+ * Tine 2.0
+ *
+ * @package     Syncope
+ * @subpackage  Backend
+ * @license     http://www.tine20.org/licenses/agpl-nonus.txt AGPL Version 1 (Non-US)
+ *              NOTE: According to sec. 8 of the AFFERO GENERAL PUBLIC LICENSE (AGPL), 
+ *              Version 1, the distribution of the Syncope module in or to the 
+ *              United States of America is excluded from the scope of this license.
+ * @author      Lars Kneschke <l.kneschke@metaways.de>
+ * @copyright   Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * 
+ */
+
+/**
+ * sql backend class for the folder state
+ *
+ * @package     Syncope
+ * @subpackage  Backend
+ */
+interface Syncope_Backend_ISyncState
+{
+    /**
+     * create new sync state
+     *
+     * @param Syncope_Model_ISyncState $_syncState
+     * @return Syncope_Model_ISyncState
+     */
+    public function create(Syncope_Model_ISyncState $_syncState);
+    
+    public function resetState($_deviceId, $_type);
+    
+    public function update(Syncope_Model_ISyncState $_syncState);
+    
+    /**
+     * get array of ids which got send to the client for a given class
+     *
+     * @param Syncope_Model_Device $_deviceId
+     * @param string $_class
+     * @return array
+     */
+    public function validate($_deviceId, $_syncKey, $_type);
+}
diff --git a/lib/Syncope/Backend/SyncState.php b/lib/Syncope/Backend/SyncState.php
new file mode 100644 (file)
index 0000000..9e786fa
--- /dev/null
@@ -0,0 +1,220 @@
+<?php
+/**
+ * Tine 2.0
+ *
+ * @package     Syncope
+ * @subpackage  Backend
+ * @license     http://www.tine20.org/licenses/agpl-nonus.txt AGPL Version 1 (Non-US)
+ *              NOTE: According to sec. 8 of the AFFERO GENERAL PUBLIC LICENSE (AGPL), 
+ *              Version 1, the distribution of the Syncope module in or to the 
+ *              United States of America is excluded from the scope of this license.
+ * @author      Lars Kneschke <l.kneschke@metaways.de>
+ * @copyright   Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * 
+ */
+
+/**
+ * sql backend class for the folder state
+ *
+ * @package     Syncope
+ * @subpackage  Backend
+ */
+class Syncope_Backend_SyncState implements Syncope_Backend_ISyncState
+{
+    /**
+     * the database adapter
+     *
+     * @var Zend_Db_Adapter_Abstract
+     */
+    protected $_db;
+    
+    public function __construct(Zend_Db_Adapter_Abstract $_db)
+    {
+        $this->_db = $_db;
+    }
+    
+    /**
+     * create new sync state
+     *
+     * @param Syncope_Model_ISyncState $_syncState
+     * @return Syncope_Model_ISyncState
+     */
+    public function create(Syncope_Model_ISyncState $_syncState, $_keepPreviousSyncState = true)
+    {
+        $id = sha1(mt_rand(). microtime());
+        $deviceId = $_syncState->device_id instanceof Syncope_Model_IDevice ? $_syncState->device_id->id : $_syncState->device_id;
+    
+        $this->_db->insert('syncope_synckeys', array(
+               'id'          => $id, 
+               'device_id'   => $deviceId,
+               'type'        => $_syncState->type,
+               'counter'     => $_syncState->counter,
+               'lastsync'    => $_syncState->lastsync->format('Y-m-d H:i:s'),
+               'pendingdata' => isset($_syncState->pendingdata) && is_array($_syncState->pendingdata) ? Zend_Json::encode($_syncState->pendingdata) : null
+        ));
+
+        $state = $this->get($id);
+        
+        if ($_keepPreviousSyncState !== true) {
+            // remove all other synckeys
+            $this->_deleteOtherStates($state);
+        }
+        
+        return $state;
+    }
+    
+    protected function _deleteOtherStates(Syncope_Model_ISyncState $_state)
+    {
+        // remove all other synckeys
+        $where = array(
+            'device_id = ?' => $_state->device_id,
+            'type = ?'      => $_state->type,
+            'counter != ?'  => $_state->counter
+        );
+    
+        $this->_db->delete('syncope_synckeys', $where);
+    
+        return true;
+    
+    }
+    
+    /**
+     * @param string  $_id
+     * @throws Syncope_Exception_NotFound
+     * @return Syncope_Model_ISyncState
+     */
+    public function get($_id)
+    {
+        $select = $this->_db->select()
+            ->from('syncope_synckeys')
+            ->where('id = ?', $_id);
+    
+        $stmt = $this->_db->query($select);
+        $state = $stmt->fetchObject('Syncope_Model_SyncState');
+        
+        if (! $state instanceof Syncope_Model_ISyncState) {
+            throw new Syncope_Exception_NotFound('id not found');
+        }
+        
+        if (!empty($state->lastsync)) {
+            $state->lastsync = new DateTime($state->lastsync, new DateTimeZone('utc'));
+        }
+        if (!empty($state->pendingdata)) {
+            $state->pendingdata = Zend_Json::encode($state->pendingdata);
+        }
+        
+        return $state;
+    }
+    
+    /**
+     * delete all stored folderId's for given device
+     *
+     * @param Syncope_Model_Device|string $_deviceId
+     * @param string $_class
+     */
+    public function resetState($_deviceId, $_type)
+    {
+        $deviceId = $_deviceId instanceof Syncope_Model_IDevice ? $_deviceId->id : $_deviceId;
+         
+        $where = array(
+            $this->_db->quoteInto($this->_db->quoteIdentifier('device_id') . ' = ?', $deviceId),
+            $this->_db->quoteInto($this->_db->quoteIdentifier('type') . ' = ?',      $_type)
+        );
+    
+        $this->_db->delete('syncope_synckeys', $where);
+    }
+    
+    public function update(Syncope_Model_ISyncState $_syncState)
+    {
+        $deviceId = $_syncState->device_id instanceof Syncope_Model_IDevice ? $_syncState->device_id->id : $_syncState->device_id;
+        
+        $this->_db->update('syncope_synckeys', array(
+               'counter'     => $_syncState->counter,
+               'lastsync'    => $_syncState->lastsync->format('Y-m-d H:i:s'),
+               'pendingdata' => isset($_syncState->pendingdata) ? $_syncState->pendingdata : null
+        ), array(
+               'id = ?' => $_syncState->id
+        ));
+        
+        return $this->get($_syncState->id);
+    }
+    
+    /**
+     * get array of ids which got send to the client for a given class
+     *
+     * @param Syncope_Model_Device|string $_deviceId
+     * @param string $_class
+     * @return Syncope_Model_ISyncState
+     */
+    public function validate($_deviceId, $_syncKey, $_class, $_collectionId = NULL)
+    {
+        $deviceId = $_deviceId instanceof Syncope_Model_IDevice ? $_deviceId->id : $_deviceId;
+        $type     = $_collectionId !== NULL ? $_class . '-' . $_collectionId : $_class;
+        
+        $select = $this->_db->select()
+            ->from('syncope_synckeys')
+            ->where($this->_db->quoteIdentifier('device_id') . ' = ?', $deviceId)
+            ->where($this->_db->quoteIdentifier('counter') . ' = ?', $_syncKey)
+            ->where($this->_db->quoteIdentifier('type') . ' = ?', $type);
+        
+        $stmt = $this->_db->query($select);
+        $state = $stmt->fetchObject('Syncope_Model_SyncState');
+        
+        if (! $state instanceof Syncope_Model_ISyncState) {
+            return false;
+        }
+
+        if (!empty($state->lastsync)) {
+            $state->lastsync = new DateTime($state->lastsync, new DateTimeZone('utc'));
+        }
+        if (!empty($state->pendingdata)) {
+            $state->pendingdata = Zend_Json::encode($state->pendingdata);
+        }
+        
+        // check if this was the latest syncKey
+        $select = $this->_db->select()
+            ->from('syncope_synckeys')
+            ->where($this->_db->quoteIdentifier('device_id') . ' = ?', $deviceId)
+            ->where($this->_db->quoteIdentifier('counter') . ' = ?', $_syncKey + 1)
+            ->where($this->_db->quoteIdentifier('type') . ' = ?', $type);
+        
+        $stmt = $this->_db->query($select);
+        $moreRecentState = $stmt->fetchObject('Syncope_Model_SyncState');
+        
+        // found more recent synckey => the last sync repsone got not received by the client
+        if ($moreRecentState instanceof Syncope_Model_ISyncState) {
+            // undelete entries marked as deleted in syncope_contentstates table
+            $this->_db->update('syncope_contentstates', array(
+               'is_deleted'  => 0,
+            ), array(
+               'device_id = ?'    => $deviceId,
+               'class = ?'        => $_class,
+               'collectionid = ?' => $_collectionId,
+               'is_deleted = ?'   => 1
+            ));
+            
+            // remove entries added during latest sync in syncope_contentstates table
+            $this->_db->delete('syncope_contentstates', array(
+               'device_id = ?'     => $deviceId,
+               'class = ?'         => $_class,
+               'collectionid = ?'  => $_collectionId,
+               'creation_time > ?' => $state->lastsync->format('Y-m-d H:i:s'),
+            ));
+            
+        } else {
+            // finaly delete all entries marked for removal in syncope_contentstates table    
+            $this->_db->delete('syncope_contentstates', array(
+               'device_id = ?'     => $deviceId,
+               'class = ?'         => $_class,
+               'collectionid = ?'  => $_collectionId,
+               'is_deleted = ?'    => 1
+            ));
+            
+        }
+        
+        // remove all other synckeys
+        $this->_deleteOtherStates($state);
+        
+        return $state;
+    }
+}
diff --git a/lib/Syncope/Command/FolderSync.php b/lib/Syncope/Command/FolderSync.php
new file mode 100644 (file)
index 0000000..7755f59
--- /dev/null
@@ -0,0 +1,238 @@
+<?php
+/**
+ * Tine 2.0
+ *
+ * @package     Command
+ * @license     http://www.tine20.org/licenses/agpl-nonus.txt AGPL Version 1 (Non-US)
+ *              NOTE: According to sec. 8 of the AFFERO GENERAL PUBLIC LICENSE (AGPL), 
+ *              Version 1, the distribution of the Tine 2.0 ActiveSync module in or to the 
+ *              United States of America is excluded from the scope of this license.
+ * @copyright   Copyright (c) 2008-2009 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Lars Kneschke <l.kneschke@metaways.de>
+ */
+
+/**
+ * class documentation
+ *
+ * @package     Command
+ */
+class Syncope_Command_FolderSync extends Syncope_Command_Wbxml 
+{
+    const STATUS_SUCCESS                = 1;
+    const STATUS_FOLDER_EXISTS          = 2;
+    const STATUS_IS_SPECIAL_FOLDER      = 3;
+    const STATUS_FOLDER_NOT_FOUND       = 4;
+    const STATUS_PARENT_FOLDER_NOT_FOUND = 5;
+    const STATUS_SERVER_ERROR           = 6;
+    const STATUS_ACCESS_DENIED          = 7;
+    const STATUS_REQUEST_TIMED_OUT      = 8;
+    const STATUS_INVALID_SYNC_KEY       = 9;
+    const STATUS_MISFORMATTED           = 10;
+    const STATUS_UNKNOWN_ERROR          = 11;
+
+    /**
+     * some usefull constants for working with the xml files
+     *
+     */
+    const FOLDERTYPE_GENERIC_USER_CREATED   = 1;
+    const FOLDERTYPE_INBOX                  = 2;
+    const FOLDERTYPE_DRAFTS                 = 3;
+    const FOLDERTYPE_DELETEDITEMS           = 4;
+    const FOLDERTYPE_SENTMAIL               = 5;
+    const FOLDERTYPE_OUTBOX                 = 6;
+    const FOLDERTYPE_TASK                   = 7;
+    const FOLDERTYPE_CALENDAR               = 8;
+    const FOLDERTYPE_CONTACT                = 9;
+    const FOLDERTYPE_NOTE                   = 10;
+    const FOLDERTYPE_JOURNAL                = 11;
+    const FOLDERTYPE_MAIL_USER_CREATED      = 12;
+    const FOLDERTYPE_CALENDAR_USER_CREATED  = 13;
+    const FOLDERTYPE_CONTACT_USER_CREATED   = 14;
+    const FOLDERTYPE_TASK_USER_CREATED      = 15;
+    const FOLDERTYPE_JOURNAL_USER_CREATED   = 16;
+    const FOLDERTYPE_NOTES_USER_CREATED     = 17;
+    const FOLDERTYPE_UNKOWN                 = 18;
+    
+    protected $_defaultNameSpace    = 'uri:FolderHierarchy';
+    protected $_documentElement     = 'FolderSync';
+    
+    protected $_classes             = array(
+        Syncope_Data_Factory::CALENDAR,
+        Syncope_Data_Factory::CONTACTS,
+        Syncope_Data_Factory::EMAIL,
+        Syncope_Data_Factory::TASKS
+    );
+
+    /**
+     * @var string
+     */
+    protected $_syncKey;
+    
+    /**
+     * parse FolderSync request
+     *
+     */
+    public function handle()
+    {
+        $xml = simplexml_import_dom($this->_inputDom);
+        $syncKey = (int)$xml->SyncKey;
+        
+        #if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) 
+        #    Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " synckey is $this->_syncKey");
+        
+        if ($syncKey == 0) {
+            $this->_syncState = new Syncope_Model_SyncState(array(
+                'device_id' => $this->_device,
+                'counter'   => 0,
+                'type'      => 'FolderSync', // this is not the complete type the class is missing, but thats ok in this case
+                'lastsync'  => $this->_syncTimeStamp
+            ));
+        } else {
+            if (($this->_syncState = $this->_syncStateBackend->validate($this->_device, $syncKey, 'FolderSync')) instanceof Syncope_Model_SyncState) {
+                $this->_syncState->lastsync = $this->_syncTimeStamp;
+            } else  {
+                $this->_folderStateBackend->resetState($this->_device);
+                $this->_syncStateBackend->resetState($this->_device, 'FolderSync');
+            }
+        }
+    }
+    
+    /**
+     * generate FolderSync response
+     *
+     * @todo currently we support only the main folder which contains all contacts/tasks/events/notes per class
+     * 
+     * @param boolean $_keepSession keep session active(don't logout user) when true
+     */
+    public function getResponse($_keepSession = FALSE)
+    {
+        $folderSync = $this->_outputDom->documentElement;
+        
+        if($this->_syncState === false) {
+            #Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " INVALID synckey provided");
+            $folderSync->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Status', self::STATUS_INVALID_SYNC_KEY));
+
+        } else {
+            $folderSync->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Status', self::STATUS_SUCCESS));
+            
+            $adds = array();
+            $deletes = array();
+            $count = 0;
+            
+            foreach($this->_classes as $class) {
+                $dataController = Syncope_Data_Factory::factory($class, $this->_device, $this->_syncTimeStamp);
+
+                $folders = $dataController->getAllFolders();
+
+                if($this->_syncState->counter == 0) {
+                    foreach($folders as $folderId => $folder) {
+                        $adds[$class][$folderId] = $folder;
+                        $count++;
+                    }
+                } else {
+                    $allServerEntries = array_keys($folders);
+                    $allClientEntries = $this->_folderStateBackend->getClientState($this->_device, $class);
+                    
+                    // added entries
+                    $serverDiff = array_diff($allServerEntries, $allClientEntries);
+                    foreach($serverDiff as $folderId) {
+                        #if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " add $class $folderId");
+                        $adds[$class][$folderId] = $folders[$folderId];
+                        $count++;
+                    }
+                    
+                    // deleted entries
+                    $serverDiff = array_diff($allClientEntries, $allServerEntries);
+                    foreach($serverDiff as $folderId) {
+                        #if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " delete $class $folderId");
+                        $deletes[$class][$folderId] = $folderId;
+                        $count++;
+                    }
+                }                
+            }
+            
+            
+            if($count > 0) {
+                $this->_syncState->counter++;
+            }
+            
+            // create xml output
+            $folderSync->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'SyncKey', $this->_syncState->counter));
+            
+            $changes = $folderSync->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Changes'));            
+            $changes->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Count', $count));
+            foreach($adds as $class => $folders) {
+                foreach((array)$folders as $folder) {
+                    $add = $changes->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Add'));
+                    $add->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'ServerId', $folder['folderId']));
+                    $add->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'ParentId', $folder['parentId']));
+                    $add->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'DisplayName', $folder['displayName']));
+                    $add->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Type', $folder['type']));
+                    
+                    // store state in backend
+                    $this->_folderStateBackend->create(new Syncope_Model_FolderState(array(
+                        'device_id'         => $this->_device,
+                        'class'             => $class,
+                        'folderid'          => $folder['folderId'],
+                        'creation_time'     => $this->_syncTimeStamp,
+                        'lastfiltertype'    => null
+                    )));
+                    
+                    #if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " $class => " . $folder['folderId']);
+                }
+            }
+            
+            foreach($deletes as $class => $folders) {
+                if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " $class");
+                foreach((array)$folders as $folderId) {
+                    $delete = $changes->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Delete'));
+                    $delete->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'ServerId', $folderId));
+                    
+                    $this->_deleteFolderState($class, $folderId);
+                }
+            }
+            
+            if (empty($this->_syncState->id)) {
+                $this->_syncStateBackend->create($this->_syncState);
+            } else {
+                $this->_syncStateBackend->update($this->_syncState);
+            }
+        }
+        
+        return $this->_outputDom;
+    }
+    
+    /**
+     * delete folderstate (aka: forget that we have sent the folder to the client)
+     *
+     * @param string $_class the class from the xml
+     * @param string $_contentId the Tine 2.0 id of the folder
+     */
+    protected function __deleteFolderState($_class, $_folderId)
+    {
+        $folderStateFilter = new Syncope_Model_FolderStateFilter(array(
+            array(
+                    'field'     => 'device_id',
+                    'operator'  => 'equals',
+                    'value'     => $this->_device->getId()
+            ),
+            array(
+                    'field'     => 'class',
+                    'operator'  => 'equals',
+                    'value'     => $_class
+            ),
+            array(
+                    'field'     => 'folderid',
+                    'operator'  => 'equals',
+                    'value'     => $_folderId
+            )
+        ));
+        $state = $this->_folderStateBackend->search($folderStateFilter, NULL, true);
+        
+        if(count($state) > 0) {
+            $this->_folderStateBackend->delete($state[0]);
+        } else {
+            Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . " no folderstate found for " . print_r($folderStateFilter->toArray(), true));
+        }
+    }    
+}
diff --git a/lib/Syncope/Command/Interface.php b/lib/Syncope/Command/Interface.php
new file mode 100644 (file)
index 0000000..4f4786a
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+/**
+ * Tine 2.0
+ *
+ * @package     ActiveSync
+ * @subpackage  ActiveSync
+ * @license     http://www.tine20.org/licenses/agpl-nonus.txt AGPL Version 1 (Non-US)
+ *              NOTE: According to sec. 8 of the AFFERO GENERAL PUBLIC LICENSE (AGPL), 
+ *              Version 1, the distribution of the Tine 2.0 ActiveSync module in or to the 
+ *              United States of America is excluded from the scope of this license.
+ * @copyright   Copyright (c) 2008-2009 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Lars Kneschke <l.kneschke@metaways.de>
+ */
+
+/**
+ * class documentation
+ *
+ * @package     ActiveSync
+ * @subpackage  ActiveSync
+ */
+interface Syncope_Command_Interface 
+{
+    /**
+     * the constructor
+     *
+     * @param  mixed                    $_requestBody
+     */
+    public function __construct($_requestBody, Syncope_Model_Device $_device, $_policyKey);
+    
+    /**
+     * process the XML file and add, change, delete or fetches data 
+     */
+    public function handle();
+
+    public function getResponse();
+}
diff --git a/lib/Syncope/Command/Provision.php b/lib/Syncope/Command/Provision.php
new file mode 100644 (file)
index 0000000..af3360d
--- /dev/null
@@ -0,0 +1,192 @@
+<?php
+/**
+ * Tine 2.0
+ *
+ * @package     Syncope
+ * @subpackage  Command
+ * @license     http://www.tine20.org/licenses/agpl-nonus.txt AGPL Version 1 (Non-US)
+ *              NOTE: According to sec. 8 of the AFFERO GENERAL PUBLIC LICENSE (AGPL), 
+ *              Version 1, the distribution of the Syncope module in or to the 
+ *              United States of America is excluded from the scope of this license.
+ * @copyright   Copyright (c) 2008-2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Lars Kneschke <l.kneschke@metaways.de>
+ */
+
+/**
+ * class documentation
+ *
+ * @package     Syncope
+ * @subpackage  Command
+ */
+class Syncope_Command_Provision extends Syncope_Command_Wbxml
+{
+    protected $_defaultNameSpace = 'uri:Provision';
+    protected $_documentElement  = 'Provision';
+    
+    const POLICYTYPE_XML   = 'MS-WAP-Provisioning-XML';
+    const POLICYTYPE_WBXML = 'MS-EAS-Provisioning-WBXML';
+    
+    const STATUS_SUCCESS                   = 1;
+    const STATUS_PROTOCOL_ERROR            = 2;
+    const STATUS_GENERAL_SERVER_ERROR      = 3;
+    const STATUS_DEVICE_MANAGED_EXTERNALLY = 4;
+    
+    const REMOTEWIPE_REQUESTED = 1;
+    const REMOTEWIPE_CONFIRMED = 2;
+    
+    protected $_skipValidatePolicyKey = true;
+    
+    protected $_policyType;
+    protected $_policyKey;
+    
+    /**
+     * process the XML file and add, change, delete or fetches data 
+     *
+     * @return resource
+     */
+    public function handle()
+    {
+        $controller = Syncope_Controller::getInstance();
+        
+        $xml = simplexml_import_dom($this->_inputDom);
+        
+        $this->_policyType = isset($xml->Policies->Policy->PolicyType) ? (string) $xml->Policies->Policy->PolicyType : null;
+        $this->_policyKey  = isset($xml->Policies->Policy->PolicyKey)  ? (int) $xml->Policies->Policy->PolicyKey     : null;
+        
+        if ($this->_device->remotewipe == self::REMOTEWIPE_REQUESTED && isset($xml->RemoteWipe->Status) && (int)$xml->RemoteWipe->Status == self::STATUS_SUCCESS) {
+            $this->_device->remotewipe = self::REMOTEWIPE_CONFIRMED;
+            $this->_device = Syncope_Controller_Device::getInstance()->update($this->_device);
+        }
+        
+    }
+    
+    /**
+     * generate search command response
+     *
+     */
+    public function getResponse()
+    {
+        // should we wipe the device
+        if ($this->_device->remotewipe >= self::REMOTEWIPE_REQUESTED) {
+            $this->_sendRemoteWipe();
+        } else {
+            if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
+                Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' PolicyType: ' . $this->_policyType . ' PolicyKey: ' . $this->_policyKey);
+            
+            if($this->_policyKey === NULL) {
+                $this->_sendPolicy();
+            } else {
+                $this->_acknowledgePolicy();
+            }       
+        } 
+            
+        return $this->_outputDom;
+    }
+    
+    /**
+     * function the send policy to client
+     * 
+     * 4131 (Enforce password on device) 0: enabled 1: disabled
+     * 4133 (Unlock from computer) 0: disabled 1: enabled
+     * AEFrequencyType 0: no inactivity time 1: inactivity time is set
+     * AEFrequencyValue inactivity time in minutes
+     * DeviceWipeThreshold after how many worng password to device should get wiped
+     * CodewordFrequency validate every 3 wrong passwords, that a person is using the device which is able to read and write. should be half of DeviceWipeThreshold
+     * MinimumPasswordLength minimum password length
+     * PasswordComplexity 0: Require alphanumeric 1: Require only numeric, 2: anything goes
+     *
+     */
+    protected function _sendPolicy()
+    {
+        if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
+            Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' send policy to device');
+        
+        $policyData = '<wap-provisioningdoc>
+            <characteristic type="SecurityPolicy">
+                <parm name="4131" value="0"/>
+                <parm name="4133" value="0"/>
+            </characteristic>
+            <characteristic type="Registry">
+                <characteristic type="HKLM\Comm\Security\Policy\LASSD\AE\{50C13377-C66D-400C-889E-C316FC4AB374}">
+                    <parm name="AEFrequencyType" value="1"/>
+                    <parm name="AEFrequencyValue" value="3"/>
+                </characteristic>
+                <characteristic type="HKLM\Comm\Security\Policy\LASSD">
+                    <parm name="DeviceWipeThreshold" value="6"/>
+                </characteristic>
+                <characteristic type="HKLM\Comm\Security\Policy\LASSD">
+                    <parm name="CodewordFrequency" value="3"/>
+                </characteristic>
+                <characteristic type="HKLM\Comm\Security\Policy\LASSD\LAP\lap_pw">
+                    <parm name="MinimumPasswordLength" value="5"/>
+                </characteristic>
+                <characteristic type="HKLM\Comm\Security\Policy\LASSD\LAP\lap_pw">
+                    <parm name="PasswordComplexity" value="2"/>
+                </characteristic>
+            </characteristic>
+        </wap-provisioningdoc>';
+        
+        $newPolicyKey = $this->generatePolicyKey();
+                
+        $provision = $sync = $this->_outputDom->documentElement;
+        $provision->appendChild($this->_outputDom->createElementNS('uri:Provision', 'Status', 1));
+        $policies = $provision->appendChild($this->_outputDom->createElementNS('uri:Provision', 'Policies'));
+        $policy = $policies->appendChild($this->_outputDom->createElementNS('uri:Provision', 'Policy'));
+        $policy->appendChild($this->_outputDom->createElementNS('uri:Provision', 'PolicyType', $this->_policyType));
+        $policy->appendChild($this->_outputDom->createElementNS('uri:Provision', 'Status', 1));
+        $policy->appendChild($this->_outputDom->createElementNS('uri:Provision', 'PolicyKey', $newPolicyKey));
+        if ($this->_policyType == self::POLICYTYPE_XML) {
+            $data = $policy->appendChild($this->_outputDom->createElementNS('uri:Provision', 'Data', $policyData));
+        } else {
+            $data = $policy->appendChild($this->_outputDom->createElementNS('uri:Provision', 'Data'));
+            $easProvisionDoc = $data->appendChild($this->_outputDom->createElementNS('uri:Provision', 'EASProvisionDoc'));
+            $easProvisionDoc->appendChild($this->_outputDom->createElementNS('uri:Provision', 'DevicePasswordEnabled', 1));
+            #$easProvisionDoc->appendChild($this->_outputDom->createElementNS('uri:Provision', 'MinDevicePasswordLength', 4));
+            #$easProvisionDoc->appendChild($this->_outputDom->createElementNS('uri:Provision', 'MaxDevicePasswordFailedAttempts', 4));
+            #$easProvisionDoc->appendChild($this->_outputDom->createElementNS('uri:Provision', 'MaxInactivityTimeDeviceLock', 60));
+        }
+        
+        $this->_device->policykey = $newPolicyKey;
+        Syncope_Controller::getInstance()->updateDevice($this->_device);
+    }
+    
+    /**
+     * function the send remote wipe command
+     */
+    protected function _sendRemoteWipe()
+    {
+        if (Tinebase_Core::isLogLevel(Zend_Log::WARN))
+            Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' send remote wipe to device');
+        
+        $provision = $sync = $this->_outputDom->documentElement;
+        $provision->appendChild($this->_outputDom->createElementNS('uri:Provision', 'Status', 1));
+        $provision->appendChild($this->_outputDom->createElementNS('uri:Provision', 'RemoteWipe'));
+    }
+    
+    protected function _acknowledgePolicy()
+    {
+        if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
+            Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' acknowledge policy');
+        
+        $newPolicyKey = $this->generatePolicyKey();
+        
+        $provision = $sync = $this->_outputDom->documentElement;
+        $provision->appendChild($this->_outputDom->createElementNS('uri:Provision', 'Status', 1));
+        $policies = $provision->appendChild($this->_outputDom->createElementNS('uri:Provision', 'Policies'));
+        $policy = $policies->appendChild($this->_outputDom->createElementNS('uri:Provision', 'Policy'));
+        $policy->appendChild($this->_outputDom->createElementNS('uri:Provision', 'PolicyType', $this->_policyType));
+        $policy->appendChild($this->_outputDom->createElementNS('uri:Provision', 'Status', 1));
+        $policy->appendChild($this->_outputDom->createElementNS('uri:Provision', 'PolicyKey', $newPolicyKey));
+
+        $this->_device->policykey = $newPolicyKey;
+        Syncope_Controller::getInstance()->updateDevice($this->_device);
+    }
+    
+    public static function generatePolicyKey()
+    {
+        $policyKey = mt_rand(1, mt_getrandmax());
+        
+        return $policyKey;
+    }
+}
diff --git a/lib/Syncope/Command/Sync.php b/lib/Syncope/Command/Sync.php
new file mode 100644 (file)
index 0000000..07f891c
--- /dev/null
@@ -0,0 +1,729 @@
+<?php
+/**
+ * Syncope
+ *
+ * @package     Command
+ * @license     http://www.tine20.org/licenses/agpl-nonus.txt AGPL Version 1 (Non-US)
+ *              NOTE: According to sec. 8 of the AFFERO GENERAL PUBLIC LICENSE (AGPL), 
+ *              Version 1, the distribution of the Tine 2.0 Syncope module in or to the 
+ *              United States of America is excluded from the scope of this license.
+ * @copyright   Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Lars Kneschke <l.kneschke@metaways.de>
+ */
+
+/**
+ * class to handle ActiveSync Sync command
+ *
+ * @package     Command
+ */
+class Syncope_Command_Sync extends Syncope_Command_Wbxml 
+{
+    const STATUS_SUCCESS                                = 1;
+    const STATUS_PROTOCOL_VERSION_MISMATCH              = 2;
+    const STATUS_INVALID_SYNC_KEY                       = 3;
+    const STATUS_PROTOCOL_ERROR                         = 4;
+    const STATUS_SERVER_ERROR                           = 5;
+    const STATUS_ERROR_IN_CLIENT_SERVER_CONVERSION      = 6;
+    const STATUS_CONFLICT_MATCHING_THE_CLIENT_AND_SERVER_OBJECT = 7;
+    const STATUS_OBJECT_NOT_FOUND                       = 8;
+    const STATUS_USER_ACCOUNT_MAYBE_OUT_OF_DISK_SPACE   = 9;
+    const STATUS_ERROR_SETTING_NOTIFICATION_GUID        = 10;
+    const STATUS_DEVICE_NOT_PROVISIONED_FOR_NOTIFICATIONS = 11;
+    const STATUS_FOLDER_HIERARCHY_HAS_CHANGED           = 12;
+    const STATUS_RESEND_FULL_XML                        = 13;
+    const STATUS_WAIT_INTERVAL_OUT_OF_RANGE             = 14;
+    
+    const CONFLICT_OVERWRITE_SERVER                     = 0;
+    const CONFLICT_OVERWRITE_PIM                        = 1;
+    
+    const MIMESUPPORT_DONT_SEND_MIME                    = 0;
+    const MIMESUPPORT_SMIME_ONLY                        = 1;
+    const MIMESUPPORT_SEND_MIME                         = 2;
+    
+    const BODY_TYPE_PLAIN_TEXT                          = 1;
+    const BODY_TYPE_HTML                                = 2;
+    const BODY_TYPE_RTF                                 = 3;
+    const BODY_TYPE_MIME                                = 4;
+    
+    /**
+     * truncate types
+     */
+    const TRUNCATE_ALL                                  = 0;
+    const TRUNCATE_4096                                 = 1;
+    const TRUNCATE_5120                                 = 2;
+    const TRUNCATE_7168                                 = 3;
+    const TRUNCATE_10240                                = 4;
+    const TRUNCATE_20480                                = 5;
+    const TRUNCATE_51200                                = 6;
+    const TRUNCATE_102400                               = 7;
+    const TRUNCATE_NOTHING                              = 8;
+
+    /**
+     * filter types
+     */
+    const FILTER_NOTHING        = 0;
+    const FILTER_1_DAY_BACK     = 1;
+    const FILTER_3_DAYS_BACK    = 2;
+    const FILTER_1_WEEK_BACK    = 3;
+    const FILTER_2_WEEKS_BACK   = 4;
+    const FILTER_1_MONTH_BACK   = 5;
+    const FILTER_3_MONTHS_BACK  = 6;
+    const FILTER_6_MONTHS_BACK  = 7;
+    const FILTER_INCOMPLETE     = 8;
+    
+
+    protected $_defaultNameSpace    = 'uri:AirSync';
+    protected $_documentElement     = 'Sync';
+    
+    /**
+     * list of collections
+     *
+     * @var array
+     */
+    protected $_collections = array();
+
+    /**
+     * total count of items in all collections
+     *
+     * @var integer
+     */
+    protected $_totalCount;
+    
+    /**
+     * there are more entries than WindowSize available
+     * the MoreAvailable tag hot added to the xml output
+     *
+     * @var boolean
+     */
+    protected $_moreAvailable = false;
+    
+    /**
+     * @var Syncope_Model_SyncState
+     */
+    protected $_syncState;
+    
+    /**
+     * process the XML file and add, change, delete or fetches data 
+     */
+    public function handle()
+    {
+        // input xml
+        $xml = new SimpleXMLElement($this->_inputDom->saveXML());
+        #$xml = simplexml_import_dom($this->_inputDom);
+        
+        foreach ($xml->Collections->Collection as $xmlCollection) {
+            $collectionData = array(
+                'syncKey'         => (int)$xmlCollection->SyncKey,
+                'syncKeyValid'    => true,
+                'class'           => isset($xmlCollection->Class) ? (string)$xmlCollection->Class : null,
+                'collectionId'    => (string)$xmlCollection->CollectionId,
+                'windowSize'      => isset($xmlCollection->WindowSize) ? (int)$xmlCollection->WindowSize : 100,
+                'deletesAsMoves'  => isset($xmlCollection->DeletesAsMoves) ? true : false,
+                'getChanges'      => isset($xmlCollection->GetChanges) ? true : false,
+                'added'           => array(),
+                'changed'         => array(),
+                'deleted'         => array(),
+                'forceAdd'        => array(),
+                'forceChange'     => array(),
+                'toBeFetched'     => array(),
+                'filterType'      => 0,
+                'mimeSupport'     => self::MIMESUPPORT_DONT_SEND_MIME,
+                'mimeTruncation'  => Syncope_Command_Sync::TRUNCATE_NOTHING,
+                'bodyPreferences' => array(),
+            );
+            
+            // process options
+            if (isset($xmlCollection->Options)) {
+                // optional parameters
+                if (isset($xmlCollection->Options->FilterType)) {
+                    $collectionData['filterType'] = (int)$xmlCollection->Options->FilterType;
+                }
+                if (isset($xmlCollection->Options->MIMESupport)) {
+                    $collectionData['mimeSupport'] = (int)$xmlCollection->Options->MIMESupport;
+                }
+                if (isset($xmlCollection->Options->MIMETruncation)) {
+                    $collectionData['mimeTruncation'] = (int)$xmlCollection->Options->MIMETruncation;
+                }
+                
+                // try to fetch element from AirSyncBase:BodyPreference
+                $airSyncBase = $xmlCollection->Options->children('uri:AirSyncBase');
+                
+                if (isset($airSyncBase->BodyPreference)) {
+                    
+                    foreach ($airSyncBase->BodyPreference as $bodyPreference) {
+                        $type = (int) $bodyPreference->Type;
+                        $collectionData['bodyPreferences'][$type] = array(
+                            'type' => $type
+                        );
+                        
+                        // optional
+                        if (isset($bodyPreference->TruncationSize)) {
+                            $collectionData['bodyPreferences'][$type]['truncationSize'] = (int) $bodyPreference->TruncationSize;
+                        }
+                    }
+                }
+            }
+            
+            // got the folder synchronized to the device already
+            try {
+                $folder = $this->_folderStateBackend->getFolderState($this->_device, $collectionData['collectionId']);
+                
+                $collectionData['class'] = $folder->class;
+                
+            } catch (Syncope_Exception_NotFound $senf) {
+                #if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) 
+                #    Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . " folder {$collectionData['collectionId']} not found");
+                
+                $collectionData['syncState']    = new Syncope_Model_SyncState(array(
+                    'device_id' => $this->_device,
+                    'counter'   => 0,
+                    'type'      => $collectionData['collectionId'], // this is not the complete type the class is missing, but thats ok in this case
+                    'lastsync'  => $this->_syncTimeStamp
+                ));
+                
+                $this->_collections['collectionNotFound'][$collectionData['collectionId']] = $collectionData;
+                continue;
+            }
+            
+            #if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) 
+            #    Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " SyncKey is {$collectionData['syncKey']} Class: {$collectionData['class']} CollectionId: {$collectionData['collectionId']}");
+            
+            // initial synckey
+            if($collectionData['syncKey'] === 0) {
+                #if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
+                #    Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " initial client synckey 0 provided");
+                
+                $collectionData['syncState']    = new Syncope_Model_SyncState(array(
+                    'device_id' => $this->_device,
+                    'counter'   => $collectionData['syncKey'],
+                    'type'      => $collectionData['class'] . '-' . $collectionData['collectionId'],
+                    'lastsync'  => $this->_syncTimeStamp
+                ));
+                
+                $this->_collections[$collectionData['class']][$collectionData['collectionId']] = $collectionData;
+                
+                continue;
+            }
+            
+            // check for invalid sycnkey
+            if(($collectionData['syncState'] = $this->_syncStateBackend->validate($this->_device, $collectionData['syncKey'], $collectionData['class'], $collectionData['collectionId'])) === false) {
+                if (Tinebase_Core::isLogLevel(Zend_Log::WARN))
+                    Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . " invalid synckey {$collectionData['syncKey']} provided");
+                
+                $collectionData['syncKeyValid'] = false;
+                $collectionData['syncState']    = new Syncope_Model_SyncState(array(
+                    'device_id' => $this->_device->getId(),
+                    'counter'   => $collectionData['syncKey'],
+                    'type'      => $collectionData['class'] . '-' . $collectionData['collectionId'],
+                    'lastsync'  => $this->_syncTimeStamp
+                ));
+                
+                $this->_collections[$collectionData['class']][$collectionData['collectionId']] = $collectionData;
+                
+                continue;
+            }
+            
+            $dataController = Syncope_Data_Factory::factory($collectionData['class'] , $this->_device, $this->_syncTimeStamp);
+            
+            // handle incoming data
+            if(isset($xmlCollection->Commands->Add)) {
+                $adds = $xmlCollection->Commands->Add;
+                if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
+                    Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " found " . count($adds) . " entries to be added to server");
+                
+                foreach ($adds as $add) {
+                       if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) 
+                           Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " add entry with clientId " . (string) $add->ClientId);
+                    // search for existing entries if first sync
+                    // @todo: maybe this can be removed
+                    // good phones don't send entries at synckey 1
+                    // and if they sent, maybe they also send entries at synckey 2 too
+                    if($collectionData['syncKey'] == 1) {
+                        $existing = $dataController->search($collectionData['collectionId'], $add->ApplicationData);
+                    } else {
+                        $existing = array(); // count() == 0
+                    }
+                    
+                    try {
+                        if(count($existing) === 0) {
+                            if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
+                                Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " entry not found. adding as new");
+                            $added = $dataController->add($collectionData['collectionId'], $add->ApplicationData);
+                        } else {
+                            if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
+                                Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " found matching entry. reuse existing entry");
+                            // use the first found entry
+                            $added = $existing[0];
+                        }
+                        $collectionData['added'][(string)$add->ClientId]['serverId'] = $added->getId(); 
+                        $collectionData['added'][(string)$add->ClientId]['status'] = self::STATUS_SUCCESS;
+                        $this->_addContentState($collectionData['class'], $collectionData['collectionId'], $added->getId());
+                    } catch (Exception $e) {
+                        if (Tinebase_Core::isLogLevel(Zend_Log::WARN))
+                            Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . " failed to add entry " . $e->getMessage());
+                        $collectionData['added'][(string)$add->ClientId]['status'] = self::STATUS_SERVER_ERROR;
+                    }
+                }
+            }
+        
+            // handle changes, but only if not first sync
+            if($collectionData['syncKey'] > 1 && isset($xmlCollection->Commands->Change)) {
+                $changes = $xmlCollection->Commands->Change;
+                if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
+                    Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " found " . count($changes) . " entries to be updated on server");
+                
+                foreach ($changes as $change) {
+                    $serverId = (string)$change->ServerId;
+                    
+                    try {
+                        $changed = $dataController->change($collectionData['collectionId'], $serverId, $change->ApplicationData);
+                        $collectionData['changed'][$serverId] = self::STATUS_SUCCESS;
+                    } catch (Tinebase_Exception_AccessDenied $e) {
+                        $collectionData['changed'][$serverId] = self::STATUS_CONFLICT_MATCHING_THE_CLIENT_AND_SERVER_OBJECT;
+                        $collectionData['forceChange'][$serverId] = $serverId;
+                    } catch (Tinebase_Exception_NotFound $e) {
+                        // entry does not exist anymore, will get deleted automaticaly
+                        $collectionData['changed'][$serverId] = self::STATUS_OBJECT_NOT_FOUND;
+                    } catch (Exception $e) {
+                        if (Tinebase_Core::isLogLevel(Zend_Log::WARN))
+                            Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . " failed to update entry " . $e);
+                        // something went wrong while trying to update the entry
+                        $collectionData['changed'][$serverId] = self::STATUS_SERVER_ERROR;
+                    }
+                }
+            }
+        
+            // handle deletes, but only if not first sync
+            if(isset($xmlCollection->Commands->Delete)) {
+                $deletes = $xmlCollection->Commands->Delete;
+                if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
+                    Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " found " . count($deletes) . " entries to be deleted on server");
+                
+                foreach ($deletes as $delete) {
+                    $serverId = (string)$delete->ServerId;
+                    
+                    try {
+                        // check if we have send this entry to the phone
+                        $this->_controller->getContentState($this->_device, $collectionData['class'], $collectionData['collectionId'], $serverId);
+                        
+                        try {
+                            $dataController->delete($collectionData['collectionId'], $serverId, $collectionData);
+                        } catch(Tinebase_Exception_NotFound $e) {
+                            if (Tinebase_Core::isLogLevel(Zend_Log::CRIT))
+                                Tinebase_Core::getLogger()->crit(__METHOD__ . '::' . __LINE__ . ' tried to delete entry ' . $serverId . ' but entry was not found');
+                        } catch (Tinebase_Exception $e) {
+                            if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
+                                Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' tried to delete entry ' . $serverId . ' but a error occured: ' . $e->getMessage());
+                            $collectionData['forceAdd'][$serverId] = $serverId;
+                        }
+                    } catch (Tinebase_Exception_NotFound $tenf) {
+                        if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
+                            Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' ' . $serverId . ' should have been removed from client already');
+                        // should we send a special status???
+                        //$collectionData['deleted'][$serverId] = self::STATUS_SUCCESS;
+                    }
+                    
+                    $collectionData['deleted'][$serverId] = self::STATUS_SUCCESS;
+                    $this->_deleteContentState($collectionData['class'], $collectionData['collectionId'], $serverId);
+                }
+            }
+                        
+            // handle fetches, but only if not first sync
+            if($collectionData['syncKey'] > 1 && isset($xmlCollection->Commands->Fetch)) {
+                // the default value for GetChanges is 1. If the phone don't want the changes it must set GetChanges to 0
+                // unfortunately the iPhone dont set GetChanges to 0 when fetching email body, but is confused when we send
+                // changes
+                if (! isset($xmlCollection->GetChanges)) {
+                    $collectionData['getChanges'] = false;
+                }
+                
+                $fetches = $xmlCollection->Commands->Fetch;
+                if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
+                    Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " found " . count($fetches) . " entries to be fetched from server");
+                foreach ($fetches as $fetch) {
+                    $serverId = (string)$fetch->ServerId;
+                    
+                    $collectionData['toBeFetched'][$serverId] = $serverId;
+                }
+            }            
+            
+            $this->_collections[$collectionData['class']][$collectionData['collectionId']] = $collectionData;
+        }  
+    }    
+    
+    /**
+     * (non-PHPdoc)
+     * @see Syncope_Command_Wbxml::getResponse()
+     */
+    public function getResponse()
+    {
+        // add aditional namespaces for tasks and email
+        $this->_outputDom->documentElement->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:Tasks'       , 'uri:Tasks');
+        $this->_outputDom->documentElement->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:Email'       , 'uri:Email');
+        $this->_outputDom->documentElement->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:AirSyncBase' , 'uri:AirSyncBase');
+        
+        $sync = $this->_outputDom->documentElement;
+        
+        $collections = $sync->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Collections'));
+
+        foreach($this->_collections as $class => $classCollections) {
+            foreach($classCollections as $collectionId => $collectionData) {
+                if ($class == 'collectionNotFound') {
+                    $collection = $collections->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Collection'));
+                    $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Class', $collectionData['class']));
+                    $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'SyncKey', 0));
+                    $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'CollectionId', $collectionData['collectionId']));
+                    $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_FOLDER_HIERARCHY_HAS_CHANGED));
+                    
+                } elseif ($collectionData['syncKeyValid'] !== true) {
+                    $collectionData['syncState']->counter = 0;
+    
+                    // set synckey to 0
+                    $collection = $collections->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Collection'));
+                    $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Class', $collectionData['class']));
+                    $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'SyncKey', $collectionData['syncState']->counter));
+                    $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'CollectionId', $collectionData['collectionId']));
+                    $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_INVALID_SYNC_KEY));
+                    
+                    $this->_contentStateBackend->resetState($this->_device, $collectionData['class'], $collectionData['collectionId']);
+                    $this->_syncStateBackend->resetState($this->_device, $collectionData['class'] . '-' . $collectionData['collectionId']);
+                    
+                } elseif ($collectionData['syncState']->counter === 0) {
+                    $collectionData['syncState']->counter++;
+    
+                    // initial sync
+                    // send back a new SyncKey only
+                    $collection = $collections->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Collection'));
+                    $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Class', $collectionData['class']));
+                    $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'SyncKey', $collectionData['syncState']->counter));
+                    $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'CollectionId', $collectionData['collectionId']));
+                    $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_SUCCESS));
+                    
+                    $this->_contentStateBackend->resetState($this->_device, $collectionData['class'], $collectionData['collectionId']);
+                    $this->_syncStateBackend->resetState($this->_device, $collectionData['class'] . '-' . $collectionData['collectionId']);
+                    
+                } else {
+                    if (empty($collectionData['added']) && empty($collectionData['changed']) && empty($collectionData['deleted']) && $collectionData['getChanges'] === false) {
+                        // keep synckey during fetch requests
+                    } else {
+                        $collectionData['syncState']->counter++;
+                    }
+                    
+                    // collection header
+                    $collection = $collections->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Collection'));
+                    $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Class', $collectionData['class']));
+                    $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'SyncKey', $collectionData['syncState']->counter));
+                    $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'CollectionId', $collectionData['collectionId']));
+                    $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_SUCCESS));
+                    
+                    $responses = NULL;
+                    // sent reponse for newly added entries
+                    if(!empty($collectionData['added'])) {
+                        if($responses === NULL) {
+                            $responses = $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Responses'));
+                        }
+                        foreach($collectionData['added'] as $clientId => $entryData) {
+                            $add = $responses->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Add'));
+                            $add->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ClientId', $clientId));
+                            if(isset($entryData['serverId'])) {
+                                $add->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $entryData['serverId']));
+                            }
+                            $add->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', $entryData['status']));
+                        }
+                    }
+                    
+                    // sent reponse for changed entries
+                    // not really needed
+                    if(!empty($collectionData['changed'])) {
+                        if($responses === NULL) {
+                            $responses = $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Responses'));
+                        }
+                        foreach($collectionData['changed'] as $serverId => $status) {
+                            $change = $responses->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Change'));
+                            $change->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $serverId));
+                            $change->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', $status));
+                        }
+                    }
+                    
+                    $dataController = Syncope_Data_Factory::factory($collectionData['class'] , $this->_device, $this->_syncTimeStamp);
+                    
+                    // sent response for to be fetched entries
+                    if(!empty($collectionData['toBeFetched'])) {
+                        if($responses === NULL) {
+                            $responses = $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Responses'));
+                        }
+                        foreach($collectionData['toBeFetched'] as $serverId) {
+                            $fetch = $responses->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Fetch'));
+                            $fetch->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $serverId));
+
+                            
+                            try {
+                                $applicationData = $this->_outputDom->createElementNS('uri:AirSync', 'ApplicationData');
+                                $dataController->appendXML($applicationData, $collectionData['collectionId'], $serverId, $collectionData, true);
+                                
+                                $fetch->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_SUCCESS));
+                                
+                                $fetch->appendChild($applicationData);
+                            } catch (Exception $e) {
+                                if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
+                                    Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getMessage());
+                                $fetch->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_OBJECT_NOT_FOUND));
+                            }
+                        }
+                    }
+                    
+                    if($collectionData['getChanges'] === true) {
+                        if($collectionData['syncState']->counter === 1) {
+                            // all entries available
+                            $serverAdds    = $dataController->getServerEntries($collectionData['collectionId'], $collectionData['filterType']);
+                            $serverChanges = array();
+                            $serverDeletes = array();
+                        } else {
+                            // continue sync session
+                            if(is_array($collectionData['syncState']->pendingdata)) {
+                                if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
+                                    Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " restored from sync state ");
+                                $serverAdds    = $collectionData['syncState']->pendingdata['serverAdds'];
+                                $serverChanges = $collectionData['syncState']->pendingdata['serverChanges'];
+                                $serverDeletes = $collectionData['syncState']->pendingdata['serverDeletes'];
+                            } else {
+                                // fetch entries added since last sync
+                                
+                                $allClientEntries = $this->_contentStateBackend->getClientState($this->_device, $collectionData['class'], $collectionData['collectionId']);
+                                $allServerEntries = $dataController->getServerEntries($collectionData['collectionId'], $collectionData['filterType']);
+                                
+                                // add entries
+                                $serverDiff = array_diff($allServerEntries, $allClientEntries);
+                                // add entries which produced problems during delete from client
+                                $serverAdds = $this->_collections[$class][$collectionId]['forceAdd'];
+                                // add entries not yet sent to client
+                                $serverAdds = array_unique(array_merge($serverAdds, $serverDiff));
+                                
+                                foreach($serverAdds as $id => $serverId) {
+                                    // skip entries added by client during this sync session
+                                    // @todo $this->_collections[$class][$collectionId] should be equal to $collectionData ???
+                                    if(isset($collectionData['added'][$serverId]) && !isset($this->_collections[$class][$collectionId]['forceAdd'][$serverId])) {
+                                        if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
+                                            Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " skipped added entry: " . $serverId);
+                                        unset($serverAdds[$id]);
+                                    }
+                                }
+                                
+                                // entries to be deleted
+                                $serverDeletes = array_diff($allClientEntries, $allServerEntries);
+                                
+                                // fetch entries changed since last sync
+                                $serverChanges = $dataController->getChanged($collectionData['collectionId'], $collectionData['syncState']->lastsync, $this->_syncTimeStamp);
+                                $serverChanges = array_merge($serverChanges, $this->_collections[$class][$collectionId]['forceChange']);
+                                
+                                foreach($serverChanges as $id => $serverId) {
+                                    // skip entry, if it got changed by client during current sync
+                                    // @todo $this->_collections[$class][$collectionId] should be equal to $collectionData ???
+                                    if(isset($collectionData['changed'][$serverId]) && !isset($this->_collections[$class][$collectionId]['forceChange'][$serverId])) {
+                                        if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
+                                            Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " skipped changed entry: " . $serverId);
+                                        unset($serverChanges[$id]);
+                                    }
+                                }
+                                
+                                // entries comeing in scope are already in $serverAdds and do not need to
+                                // be send with $serverCanges
+                                $serverChanges = array_diff($serverChanges, $serverAdds);
+                            }                        
+                        }
+                        
+                        #if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
+                        #    Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " found (added/changed/deleted) " . count($serverAdds) . '/' . count($serverChanges) . '/' . count($serverDeletes)  . ' entries for sync from server to client');
+    
+                        if ((count($serverAdds) + count($serverChanges) + count($serverDeletes)) > $collectionData['windowSize'] ) {
+                            $this->_moreAvailable = true;
+                            $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'MoreAvailable'));
+                        }
+                        
+                        if (count($serverAdds) > 0 || count($serverChanges) > 0 || count($serverDeletes) > 0) {
+                            $commands = $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Commands'));
+                        }
+                        
+                        /**
+                         * process added entries
+                         */
+                        foreach($serverAdds as $id => $serverId) {
+                            if($this->_totalCount === $collectionData['windowSize']) {
+                                break;
+                            }
+                            
+                            try {
+                                $add = $this->_outputDom->createElementNS('uri:AirSync', 'Add');
+                                $add->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $serverId));
+                                
+                                $applicationData = $add->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ApplicationData'));
+                                $dataController->appendXML($applicationData, $collectionData, $serverId);
+        
+                                $commands->appendChild($add);
+                                
+                                $this->_totalCount++;
+                            } catch (Exception $e) {
+                                #if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
+                                #    Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getMessage());
+                            }
+                            
+                            // mark as send to the client, even the conversion to xml might have failed 
+                            $contentState = $this->_contentStateBackend->create(new Syncope_Model_ContentState(array(
+                                'device_id'     => $this->_device,
+                                'class'         => $collectionData['class'],
+                                'collectionid'  => $collectionData['collectionId'],
+                                'contentid'     => $serverId,
+                                'creation_time' => $this->_syncTimeStamp
+                            )));                
+                            
+                            unset($serverAdds[$id]);    
+                        }
+    
+                        /**
+                         * process changed entries
+                         */
+                        foreach($serverChanges as $id => $serverId) {
+                            if($this->_totalCount === $collectionData['windowSize']) {
+                                break;
+                            }
+
+                            try {
+                                $change = $this->_outputDom->createElementNS('uri:AirSync', 'Change');
+                                $change->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $serverId));
+                                
+                                $applicationData = $change->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ApplicationData'));
+                                $dataController->appendXML($applicationData, $collectionData, $serverId);
+        
+                                $commands->appendChild($change);
+                                
+                                $this->_totalCount++;
+                            } catch (Exception $e) {
+                                #if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
+                                #    Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getMessage());
+                            }
+
+                            unset($serverChanges[$id]);    
+                        }
+    
+                        /**
+                         * process deleted entries
+                         */
+                        foreach($serverDeletes as $id => $serverId) {
+                            if($this->_totalCount === $collectionData['windowSize']) {
+                                break;
+                            }
+                                                        
+                            try {
+                                $delete = $this->_outputDom->createElementNS('uri:AirSync', 'Delete');
+                                $delete->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $serverId));
+                                
+                                $this->_markContentStateAsDeleted($collectionData['class'], $collectionData['collectionId'], $serverId);
+                                
+                                $commands->appendChild($delete);
+                                
+                                $this->_totalCount++;
+                            } catch (Exception $e) {
+                                #if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
+                                #    Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getMessage());
+                            }
+                            
+                            unset($serverDeletes[$id]);    
+                        }
+                    }
+                    #if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
+                    #    Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " new synckey is ". $collectionData['syncState']->counter);                
+                }
+                
+                if ($class != 'collectionNotFound') {
+                    // save data to sync state if more data available
+                    if($this->_moreAvailable === true) {
+                        $collectionData['syncState']->pendingdata = array(
+                            'serverAdds'    => (array)$serverAdds,
+                            'serverChanges' => (array)$serverChanges,
+                            'serverDeletes' => (array)$serverDeletes
+                        );
+                    } else {
+                        $collectionData['syncState']->pendingdata = null;
+                    }
+                    
+                    $keepPreviousSyncKey = true;
+                    // increment sync timestamp by 1 second
+                    $this->_syncTimeStamp->modify('+1 sec');
+                    if (!empty($collectionData['added'])) {
+                        if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
+                            Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " remove previous synckey as client added new entries");
+                        $keepPreviousSyncKey = false;
+                    }
+                    $collectionData['syncState']->lastsync = $this->_syncTimeStamp;
+                    $this->_syncStateBackend->create($collectionData['syncState'], $keepPreviousSyncKey);
+                    
+                    // store current filter type
+                    try {
+                        $folderState = $this->_folderStateBackend->getFolderState($this->_device, $collectionData['collectionId']);
+                        $folderState->lastfiltertype = $collectionData['filterType'];
+                        $this->_folderStateBackend->update($folderState);
+                    } catch (Syncope_Exception_NotFound $senf) {
+                        // failed to get folderstate => should not happen but is also no problem in this state
+                        if (Tinebase_Core::isLogLevel(Zend_Log::CRIT)) 
+                            Tinebase_Core::getLogger()->crit(__METHOD__ . '::' . __LINE__ . ' failed to get content state for: ' . $collectionData['collectionId']);
+                    }
+                }
+            }
+        }
+        
+        return $this->_outputDom;
+    }
+
+    /**
+     * delete contentstate (aka: forget that we have sent the entry to the client)
+     *
+     * @param string $_class the class from the xml
+     * @param string $_collectionId the collection id from the xml
+     * @param string $_contentId the Tine 2.0 id of the entry
+     */
+    protected function _markContentStateAsDeleted($_class, $_collectionId, $_contentId)
+    {
+        $contentState = new Syncope_Model_ContentState(array(
+                'device_id'     => $this->_device->getId(),
+                'class'         => $_class,
+                'collectionid'  => $_collectionId,
+                'contentid'     => $_contentId,
+                'creation_time' => $this->_syncTimeStamp
+        ));
+    
+        $this->_controller->markContentStateAsDeleted($contentState);
+    }
+    
+    /**
+     * @param unknown_type $_deviceId
+     * @param unknown_type $_class
+     * @param unknown_type $_folderId
+     * @return Syncope_Model_FolderState
+     */
+    public function __getFolderState($_deviceId, $_folderId)
+    {
+        $deviceId = $_deviceId instanceof Syncope_Model_Device ? $_deviceId->getId() : $_deviceId;
+        
+        // store current filter type
+        $filter = new Syncope_Model_FolderStateFilter(array(
+            array(
+                'field'     => 'device_id',
+                'operator'  => 'equals',
+                'value'     => $deviceId,
+            ),
+            array(
+                'field'     => 'folderid',
+                'operator'  => 'equals',
+                'value'     => $_folderId
+            )
+        ));
+        $folderStates = $this->_folderStateBackend->search($filter);
+
+        if ($folderStates->count() == 0) {
+            throw new Tinebase_Exception_NotFound('folderstate for device not found');
+        }
+        
+        return $folderStates->getFirstRecord();
+    }
+}
diff --git a/lib/Syncope/Command/Wbxml.php b/lib/Syncope/Command/Wbxml.php
new file mode 100644 (file)
index 0000000..bea5db0
--- /dev/null
@@ -0,0 +1,160 @@
+<?php
+/**
+ * Tine 2.0
+ *
+ * @package     Syncope
+ * @subpackage  Command
+ * @license     http://www.tine20.org/licenses/agpl-nonus.txt AGPL Version 1 (Non-US)
+ *              NOTE: According to sec. 8 of the AFFERO GENERAL PUBLIC LICENSE (AGPL), 
+ *              Version 1, the distribution of the Syncope module in or to the 
+ *              United States of America is excluded from the scope of this license.
+ * @copyright   Copyright (c) 2008-2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Lars Kneschke <l.kneschke@metaways.de>
+ */
+
+/**
+ * abstract class for all commands using wbxml encoded content
+ *
+ * @package     Syncope
+ * @subpackage  Command
+ */
+abstract class Syncope_Command_Wbxml implements Syncope_Command_Interface
+{
+    /**
+     * informations about the currently device
+     *
+     * @var Syncope_Model_Device
+     */
+    protected $_device;
+    
+    /**
+     * informations about the currently device
+     *
+     * @var Syncope_Backend_IDevice
+     */
+    protected $_deviceBackend;
+    
+    /**
+     * informations about the currently device
+     *
+     * @var Syncope_Backend_IFolderState
+     */
+    protected $_folderStateBackend;
+    
+    /**
+     * @var Syncope_Backend_ISyncState
+     */
+    protected $_syncStateBackend;
+    
+    /**
+     * @var Syncope_Backend_IContentState
+     */
+    protected $_contentStateBackend;
+    
+    /**
+     * the domDocument containing the xml response from the server
+     *
+     * @var DOMDocument
+     */
+    protected $_outputDom;
+
+    /**
+     * the domDocucment containing the xml request from the client
+     *
+     * @var DOMDocument
+     */
+    protected $_inputDom;
+        
+    /**
+     * the default namespace
+     *
+     * @var string
+     */
+    protected $_defaultNameSpace;
+    
+    /**
+     * the main xml tag
+     *
+     * @var string
+     */
+    protected $_documentElement;
+    
+    /**
+     * @var string
+     */
+    protected $_policyKey;
+    
+    protected $_skipValidatePolicyKey = false;
+    /**
+     * timestamp to use for all sync requests
+     *
+     * @var Tinebase_DateTime
+     */
+    protected $_syncTimeStamp;
+        
+    const FILTERTYPE_ALL            = 0;
+    const FILTERTYPE_1DAY           = 1;
+    const FILTERTYPE_3DAYS          = 2;
+    const FILTERTYPE_1WEEK          = 3;
+    const FILTERTYPE_2WEEKS         = 4;
+    const FILTERTYPE_1MONTH         = 5;
+    const FILTERTYPE_3MONTHS        = 6;
+    const FILTERTYPE_6MONTHS        = 7;
+
+    const TRUNCATION_HEADERS        = 0;
+    const TRUNCATION_512B           = 1;
+    const TRUNCATION_1K             = 2;
+    const TRUNCATION_5K             = 4;
+    const TRUNCATION_ALL            = 9;
+    
+    /**
+     * the constructor
+     *
+     * @param  mixed                    $_requestBody
+     * @param  Syncope_Model_Device  $_device
+     * @param  string                   $_policyKey
+     */
+    public function __construct($_requestBody, Syncope_Model_Device $_device, $_policyKey)
+    {
+        $this->_policyKey = $_policyKey;
+        $this->_device    = $_device;
+        
+        $this->_deviceBackend       = Zend_Registry::get('deviceBackend');
+        $this->_folderStateBackend  = Zend_Registry::get('folderStateBackend');
+        $this->_syncStateBackend    = Zend_Registry::get('syncStateBackend');
+        $this->_contentStateBackend = Zend_Registry::get('contentStateBackend');
+        
+        if ($this->_skipValidatePolicyKey !== true && $this->_policyKey === null) {
+            #throw new Syncope_Exception_PolicyKeyMissing();
+        }
+        
+        if ($this->_skipValidatePolicyKey !== true && ($this->_policyKey === 0 || $this->_device->policykey != $this->_policyKey)) {
+            #throw new Syncope_Exception_ProvisioningNeeded();
+        }
+        
+        // should we wipe the mobile phone?
+        if ($this->_skipValidatePolicyKey !== true && !empty($this->_policyKey) && $this->_device->remotewipe >= Syncope_Command_Provision::REMOTEWIPE_REQUESTED) {
+            throw new Syncope_Exception_ProvisioningNeeded();
+        }
+        
+        $this->_inputDom = $_requestBody;
+        
+        $this->_syncTimeStamp = new DateTime(null, new DateTimeZone('UTC'));
+        
+        #if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) 
+        #    Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " sync timestamp: " . $this->_syncTimeStamp->get(Tinebase_Record_Abstract::ISO8601LONG));
+        
+        // Creates an instance of the DOMImplementation class
+        $imp = new DOMImplementation();
+        
+        // Creates a DOMDocumentType instance
+        $dtd = $imp->createDocumentType('AirSync', "-//AIRSYNC//DTD AirSync//EN", "http://www.microsoft.com/");
+
+        // Creates a DOMDocument instance
+        $this->_outputDom = $imp->createDocument($this->_defaultNameSpace, $this->_documentElement, $dtd);
+        $this->_outputDom->formatOutput = false;
+        $this->_outputDom->encoding     = 'utf-8';
+        
+    }    
+}
diff --git a/lib/Syncope/Data/Calendar.php b/lib/Syncope/Data/Calendar.php
new file mode 100644 (file)
index 0000000..86752ae
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * Syncope
+ *
+ * @package     Model
+ * @license     http://www.tine20.org/licenses/agpl-nonus.txt AGPL Version 1 (Non-US)
+ *              NOTE: According to sec. 8 of the AFFERO GENERAL PUBLIC LICENSE (AGPL),
+ *              Version 1, the distribution of the Tine 2.0 Syncope module in or to the
+ *              United States of America is excluded from the scope of this license.
+ * @copyright   Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Lars Kneschke <l.kneschke@metaways.de>
+ */
+
+/**
+ * class to handle ActiveSync Sync command
+ *
+ * @package     Model
+ */
+
+class Syncope_Data_Calendar implements Syncope_Data_IData
+{
+    public function getAllFolders()
+    {
+        return array(
+            'calenderFolderId' => array(
+                'folderId'    => 'calenderFolderId',
+                'parentId'    => null,
+                'displayName' => 'Default Calendar Folder',
+                'type'        => Syncope_Command_FolderSync::FOLDERTYPE_CALENDAR
+            )
+        );
+    }
+}
+
diff --git a/lib/Syncope/Data/Contacts.php b/lib/Syncope/Data/Contacts.php
new file mode 100644 (file)
index 0000000..5068b1c
--- /dev/null
@@ -0,0 +1,56 @@
+<?php
+
+/**
+ * Syncope
+ *
+ * @package     Model
+ * @license     http://www.tine20.org/licenses/agpl-nonus.txt AGPL Version 1 (Non-US)
+ *              NOTE: According to sec. 8 of the AFFERO GENERAL PUBLIC LICENSE (AGPL),
+ *              Version 1, the distribution of the Tine 2.0 Syncope module in or to the
+ *              United States of America is excluded from the scope of this license.
+ * @copyright   Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Lars Kneschke <l.kneschke@metaways.de>
+ */
+
+/**
+ * class to handle ActiveSync Sync command
+ *
+ * @package     Model
+ */
+
+class Syncope_Data_Contacts implements Syncope_Data_IData
+{
+    public function appendXML(DOMElement $_domParrent, $_collectionData, $_serverId)
+    {
+        $_domParrent->ownerDocument->documentElement->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:Contacts', 'uri:Contacts');
+        
+    }
+    
+    public function getAllFolders()
+    {
+        return array(
+            'addressbookFolderId' => array(
+                'folderId'    => 'addressbookFolderId',
+                'parentId'    => null,
+                'displayName' => 'Default Contacts Folder',
+                'type'        => Syncope_Command_FolderSync::FOLDERTYPE_CONTACT
+            )
+        );
+    }
+    
+    public function getServerEntries()
+    {
+        return array('serverContactId1', 'serverContactId2');
+    }
+    
+    public function getChanged()
+    {
+        return array();
+    }
+    
+    public function getMultiple()
+    {
+        return array();
+    }
+}
+
diff --git a/lib/Syncope/Data/Email.php b/lib/Syncope/Data/Email.php
new file mode 100644 (file)
index 0000000..63c2076
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * Syncope
+ *
+ * @package     Model
+ * @license     http://www.tine20.org/licenses/agpl-nonus.txt AGPL Version 1 (Non-US)
+ *              NOTE: According to sec. 8 of the AFFERO GENERAL PUBLIC LICENSE (AGPL),
+ *              Version 1, the distribution of the Tine 2.0 Syncope module in or to the
+ *              United States of America is excluded from the scope of this license.
+ * @copyright   Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Lars Kneschke <l.kneschke@metaways.de>
+ */
+
+/**
+ * class to handle ActiveSync Sync command
+ *
+ * @package     Model
+ */
+
+class Syncope_Data_Email implements Syncope_Data_IData
+{
+    public function getAllFolders()
+    {
+        return array(
+            'emailInboxFolderId' => array(
+                'folderId'    => 'emailInboxFolderId',
+                'parentId'    => null,
+                'displayName' => 'Inbox',
+                'type'        => Syncope_Command_FolderSync::FOLDERTYPE_INBOX
+            ),
+            'emailSentFolderId' => array(
+                'folderId'    => 'emailSentFolderId',
+                'parentId'    => null,
+                'displayName' => 'Sent',
+                'type'        => Syncope_Command_FolderSync::FOLDERTYPE_SENTMAIL
+            )
+        );
+    }
+}
+
diff --git a/lib/Syncope/Data/Factory.php b/lib/Syncope/Data/Factory.php
new file mode 100644 (file)
index 0000000..fc68ce8
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+
+/**
+ * Syncope
+ *
+ * @package     Model
+ * @license     http://www.tine20.org/licenses/agpl-nonus.txt AGPL Version 1 (Non-US)
+ *              NOTE: According to sec. 8 of the AFFERO GENERAL PUBLIC LICENSE (AGPL),
+ *              Version 1, the distribution of the Tine 2.0 Syncope module in or to the
+ *              United States of America is excluded from the scope of this license.
+ * @copyright   Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Lars Kneschke <l.kneschke@metaways.de>
+ */
+
+/**
+ * class to handle ActiveSync Sync command
+ *
+ * @package     Model
+ */
+
+class Syncope_Data_Factory
+{
+    const CALENDAR = 'Calendar';
+    const CONTACTS = 'Contacts';
+    const EMAIL    = 'Email';
+    const TASKS    = 'Tasks';
+    
+    /**
+     * @param unknown_type $_class
+     * @param Syncope_Model_IDevice $_device
+     * @param DateTime $_timeStamp
+     * @throws InvalidArgumentException
+     * @return Syncope_Data_IData
+     */
+    public static function factory($_class, Syncope_Model_IDevice $_device, DateTime $_timeStamp)
+    {
+        switch($_class) {
+            case self::CALENDAR:
+                $class = new Syncope_Data_Calendar($_device, $_timeStamp);
+                break;
+                
+            case self::CONTACTS:
+                $class = new Syncope_Data_Contacts($_device, $_timeStamp);
+                break;
+                
+            case self::EMAIL:
+                $class = new Syncope_Data_Email($_device, $_timeStamp);
+                break;
+                
+            case self::TASKS:
+                $class = new Syncope_Data_Tasks($_device, $_timeStamp);
+                break;
+                
+            default:
+                throw new InvalidArgumentException('invalid class name provided');
+                breeak;
+        }
+            
+        return $class;
+    }
+}
+
diff --git a/lib/Syncope/Data/IData.php b/lib/Syncope/Data/IData.php
new file mode 100644 (file)
index 0000000..9b3e920
--- /dev/null
@@ -0,0 +1,25 @@
+<?php
+
+/**
+ * Syncope
+ *
+ * @package     Model
+ * @license     http://www.tine20.org/licenses/agpl-nonus.txt AGPL Version 1 (Non-US)
+ *              NOTE: According to sec. 8 of the AFFERO GENERAL PUBLIC LICENSE (AGPL),
+ *              Version 1, the distribution of the Tine 2.0 Syncope module in or to the
+ *              United States of America is excluded from the scope of this license.
+ * @copyright   Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Lars Kneschke <l.kneschke@metaways.de>
+ */
+
+/**
+ * class to handle ActiveSync Sync command
+ *
+ * @package     Model
+ */
+
+interface Syncope_Data_IData
+{
+    public function getAllFolders();
+}
+
diff --git a/lib/Syncope/Data/Tasks.php b/lib/Syncope/Data/Tasks.php
new file mode 100644 (file)
index 0000000..2798578
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * Syncope
+ *
+ * @package     Model
+ * @license     http://www.tine20.org/licenses/agpl-nonus.txt AGPL Version 1 (Non-US)
+ *              NOTE: According to sec. 8 of the AFFERO GENERAL PUBLIC LICENSE (AGPL),
+ *              Version 1, the distribution of the Tine 2.0 Syncope module in or to the
+ *              United States of America is excluded from the scope of this license.
+ * @copyright   Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Lars Kneschke <l.kneschke@metaways.de>
+ */
+
+/**
+ * class to handle ActiveSync Sync command
+ *
+ * @package     Model
+ */
+
+class Syncope_Data_Tasks implements Syncope_Data_IData
+{
+    public function getAllFolders()
+    {
+        return array(
+            'tasksFolderId' => array(
+                'folderId'    => 'tasksFolderId',
+                'parentId'    => null,
+                'displayName' => 'Default Tasks Folder',
+                'type'        => Syncope_Command_FolderSync::FOLDERTYPE_TASK
+            )
+        );
+    }
+}
+
diff --git a/lib/Syncope/Exception/NotFound.php b/lib/Syncope/Exception/NotFound.php
new file mode 100644 (file)
index 0000000..072f99a
--- /dev/null
@@ -0,0 +1,23 @@
+<?php
+/**
+ * Tine 2.0
+ *
+ * @package     Syncope
+ * @subpackage  Exception
+ * @license     http://www.tine20.org/licenses/agpl-nonus.txt AGPL Version 1 (Non-US)
+ *              NOTE: According to sec. 8 of the AFFERO GENERAL PUBLIC LICENSE (AGPL), 
+ *              Version 1, the distribution of the Tine 2.0 ActiveSync module in or to the 
+ *              United States of America is excluded from the scope of this license.
+ * @copyright   Copyright (c) 2012-2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Lars Kneschke <l.kneschke@metaways.de>
+ */
+
+/**
+ * class documentation
+ *
+ * @package     Syncope
+ * @subpackage  Exception
+ */
+class Syncope_Exception_NotFound extends Exception
+{
+}
diff --git a/lib/Syncope/Model/ContentState.php b/lib/Syncope/Model/ContentState.php
new file mode 100644 (file)
index 0000000..5fb9203
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * Syncope
+ *
+ * @package     Model
+ * @license     http://www.tine20.org/licenses/agpl-nonus.txt AGPL Version 1 (Non-US)
+ *              NOTE: According to sec. 8 of the AFFERO GENERAL PUBLIC LICENSE (AGPL),
+ *              Version 1, the distribution of the Tine 2.0 Syncope module in or to the
+ *              United States of America is excluded from the scope of this license.
+ * @copyright   Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Lars Kneschke <l.kneschke@metaways.de>
+ */
+
+/**
+ * class to handle ActiveSync Sync command
+ *
+ * @package     Model
+ */
+
+class Syncope_Model_ContentState implements Syncope_Model_IContentState
+{
+    public function __construct(array $_data = array())
+    {
+        $this->setFromArray($_data);
+    }
+    
+    public function setFromArray(array $_data)
+    {
+        foreach($_data as $key => $value) {
+            $this->$key = $value;
+        }
+    }
+}
+
diff --git a/lib/Syncope/Model/Device.php b/lib/Syncope/Model/Device.php
new file mode 100644 (file)
index 0000000..524010f
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * Syncope
+ *
+ * @package     Model
+ * @license     http://www.tine20.org/licenses/agpl-nonus.txt AGPL Version 1 (Non-US)
+ *              NOTE: According to sec. 8 of the AFFERO GENERAL PUBLIC LICENSE (AGPL),
+ *              Version 1, the distribution of the Tine 2.0 Syncope module in or to the
+ *              United States of America is excluded from the scope of this license.
+ * @copyright   Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Lars Kneschke <l.kneschke@metaways.de>
+ */
+
+/**
+ * class to handle ActiveSync Sync command
+ *
+ * @package     Model
+ */
+
+class Syncope_Model_Device implements Syncope_Model_IDevice
+{
+    public function __construct(array $_data = array())
+    {
+        $this->setFromArray($_data);
+    }
+    
+    public function setFromArray(array $_data)
+    {
+        foreach($_data as $key => $value) {
+            $this->$key = $value;
+        }
+    }
+}
+
diff --git a/lib/Syncope/Model/FolderState.php b/lib/Syncope/Model/FolderState.php
new file mode 100644 (file)
index 0000000..39fa2f7
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * Syncope
+ *
+ * @package     Model
+ * @license     http://www.tine20.org/licenses/agpl-nonus.txt AGPL Version 1 (Non-US)
+ *              NOTE: According to sec. 8 of the AFFERO GENERAL PUBLIC LICENSE (AGPL),
+ *              Version 1, the distribution of the Tine 2.0 Syncope module in or to the
+ *              United States of America is excluded from the scope of this license.
+ * @copyright   Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Lars Kneschke <l.kneschke@metaways.de>
+ */
+
+/**
+ * class to handle ActiveSync Sync command
+ *
+ * @package     Model
+ */
+
+class Syncope_Model_FolderState implements Syncope_Model_IFolderState
+{
+    public function __construct(array $_data = array())
+    {
+        $this->setFromArray($_data);
+    }
+    
+    public function setFromArray(array $_data)
+    {
+        foreach($_data as $key => $value) {
+            $this->$key = $value;
+        }
+    }
+}
+
diff --git a/lib/Syncope/Model/IContentState.php b/lib/Syncope/Model/IContentState.php
new file mode 100644 (file)
index 0000000..4721abc
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * Syncope
+ *
+ * @package     Model
+ * @license     http://www.tine20.org/licenses/agpl-nonus.txt AGPL Version 1 (Non-US)
+ *              NOTE: According to sec. 8 of the AFFERO GENERAL PUBLIC LICENSE (AGPL),
+ *              Version 1, the distribution of the Tine 2.0 Syncope module in or to the
+ *              United States of America is excluded from the scope of this license.
+ * @copyright   Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Lars Kneschke <l.kneschke@metaways.de>
+ */
+
+/**
+ * class to handle ActiveSync Sync command
+ *
+ * @package     Model
+ * @property    string    id
+ * @property    string    device_id
+ * @property    string    class
+ * @property    string    collectionid
+ * @property    string    contentid
+ * @property    DateTime  creation_time
+ * @property    string    is_deleted
+ */
+
+interface Syncope_Model_IContentState
+{
+    
+}
+
diff --git a/lib/Syncope/Model/IDevice.php b/lib/Syncope/Model/IDevice.php
new file mode 100644 (file)
index 0000000..47c028d
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+
+/**
+ * Syncope
+ *
+ * @package     Model
+ * @license     http://www.tine20.org/licenses/agpl-nonus.txt AGPL Version 1 (Non-US)
+ *              NOTE: According to sec. 8 of the AFFERO GENERAL PUBLIC LICENSE (AGPL),
+ *              Version 1, the distribution of the Tine 2.0 Syncope module in or to the
+ *              United States of America is excluded from the scope of this license.
+ * @copyright   Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Lars Kneschke <l.kneschke@metaways.de>
+ */
+
+/**
+ * class to handle ActiveSync Sync command
+ *
+ * @package     Model
+ * @property    string   id
+ * @property    string   deviceid
+ * @property    string   devicetype
+ * @property    string   policykey
+ * @property    string   owner_id
+ * @property    string   acsversion
+ * @property    string   pinglifetime
+ * @property    string   remotewipe
+ */
+
+interface Syncope_Model_IDevice
+{
+    
+}
+
diff --git a/lib/Syncope/Model/IFolderState.php b/lib/Syncope/Model/IFolderState.php
new file mode 100644 (file)
index 0000000..240d63a
--- /dev/null
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * Syncope
+ *
+ * @package     Model
+ * @license     http://www.tine20.org/licenses/agpl-nonus.txt AGPL Version 1 (Non-US)
+ *              NOTE: According to sec. 8 of the AFFERO GENERAL PUBLIC LICENSE (AGPL),
+ *              Version 1, the distribution of the Tine 2.0 Syncope module in or to the
+ *              United States of America is excluded from the scope of this license.
+ * @copyright   Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Lars Kneschke <l.kneschke@metaways.de>
+ */
+
+/**
+ * class to handle ActiveSync Sync command
+ *
+ * @package     Model
+ * @property    string   id
+ * @property    string   device_id
+ * @property    string   class
+ * @property    string   folderid
+ * @property    string   creation_time
+ * @property    string   lastfiltertype
+ */
+
+interface Syncope_Model_IFolderState
+{
+    
+}
+
diff --git a/lib/Syncope/Model/ISyncState.php b/lib/Syncope/Model/ISyncState.php
new file mode 100644 (file)
index 0000000..e5ac632
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+
+/**
+ * Syncope
+ *
+ * @package     Model
+ * @license     http://www.tine20.org/licenses/agpl-nonus.txt AGPL Version 1 (Non-US)
+ *              NOTE: According to sec. 8 of the AFFERO GENERAL PUBLIC LICENSE (AGPL),
+ *              Version 1, the distribution of the Tine 2.0 Syncope module in or to the
+ *              United States of America is excluded from the scope of this license.
+ * @copyright   Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Lars Kneschke <l.kneschke@metaways.de>
+ */
+
+/**
+ * class to handle ActiveSync Sync command
+ *
+ * @package     Model
+ * @property    string    device_id
+ * @property    string    type
+ * @property    string    counter
+ * @property    DateTime  lastsync
+ * @property    string    pendingdata
+ */
+
+interface Syncope_Model_ISyncState
+{
+    
+}
+
diff --git a/lib/Syncope/Model/SyncState.php b/lib/Syncope/Model/SyncState.php
new file mode 100644 (file)
index 0000000..7448872
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * Syncope
+ *
+ * @package     Model
+ * @license     http://www.tine20.org/licenses/agpl-nonus.txt AGPL Version 1 (Non-US)
+ *              NOTE: According to sec. 8 of the AFFERO GENERAL PUBLIC LICENSE (AGPL),
+ *              Version 1, the distribution of the Tine 2.0 Syncope module in or to the
+ *              United States of America is excluded from the scope of this license.
+ * @copyright   Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Lars Kneschke <l.kneschke@metaways.de>
+ */
+
+/**
+ * class to handle ActiveSync Sync command
+ *
+ * @package     Model
+ */
+
+class Syncope_Model_SyncState implements Syncope_Model_ISyncState
+{
+    public function __construct(array $_data = array())
+    {
+        $this->setFromArray($_data);
+    }
+    
+    public function setFromArray(array $_data)
+    {
+        foreach($_data as $key => $value) {
+            $this->$key = $value;
+        }
+    }
+}
+
diff --git a/lib/Zend/Config.php b/lib/Zend/Config.php
new file mode 100644 (file)
index 0000000..c1d20e0
--- /dev/null
@@ -0,0 +1,456 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Config
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Config.php 10020 2009-08-18 14:34:09Z j.fischer@metaways.de $
+ */
+
+
+/**
+ * @category   Zend
+ * @package    Zend_Config
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Config implements Countable, Iterator
+{
+    /**
+     * Whether in-memory modifications to configuration data are allowed
+     *
+     * @var boolean
+     */
+    protected $_allowModifications;
+
+    /**
+     * Iteration index
+     *
+     * @var integer
+     */
+    protected $_index;
+
+    /**
+     * Number of elements in configuration data
+     *
+     * @var integer
+     */
+    protected $_count;
+
+    /**
+     * Contains array of configuration data
+     *
+     * @var array
+     */
+    protected $_data;
+
+    /**
+     * Used when unsetting values during iteration to ensure we do not skip
+     * the next element
+     *
+     * @var boolean
+     */
+    protected $_skipNextIteration;
+
+    /**
+     * Contains which config file sections were loaded. This is null
+     * if all sections were loaded, a string name if one section is loaded
+     * and an array of string names if multiple sections were loaded.
+     *
+     * @var mixed
+     */
+    protected $_loadedSection;
+
+    /**
+     * This is used to track section inheritance. The keys are names of sections that
+     * extend other sections, and the values are the extended sections.
+     *
+     * @var array
+     */
+    protected $_extends = array();
+
+    /**
+     * Load file error string.
+     * 
+     * Is null if there was no error while file loading
+     *
+     * @var string
+     */
+    protected $_loadFileErrorStr = null;
+
+    /**
+     * Zend_Config provides a property based interface to
+     * an array. The data are read-only unless $allowModifications
+     * is set to true on construction.
+     *
+     * Zend_Config also implements Countable and Iterator to
+     * facilitate easy access to the data.
+     *
+     * @param  array   $array
+     * @param  boolean $allowModifications
+     * @return void
+     */
+    public function __construct(array $array, $allowModifications = false)
+    {
+        $this->_allowModifications = (boolean) $allowModifications;
+        $this->_loadedSection = null;
+        $this->_index = 0;
+        $this->_data = array();
+        foreach ($array as $key => $value) {
+            if (is_array($value)) {
+                $this->_data[$key] = new self($value, $this->_allowModifications);
+            } else {
+                $this->_data[$key] = $value;
+            }
+        }
+        $this->_count = count($this->_data);
+    }
+
+    /**
+     * Retrieve a value and return $default if there is no element set.
+     *
+     * @param string $name
+     * @param mixed $default
+     * @return mixed
+     */
+    public function get($name, $default = null)
+    {
+        $result = $default;
+        if (array_key_exists($name, $this->_data)) {
+            $result = $this->_data[$name];
+        }
+        return $result;
+    }
+
+    /**
+     * Magic function so that $obj->value will work.
+     *
+     * @param string $name
+     * @return mixed
+     */
+    public function __get($name)
+    {
+        return $this->get($name);
+    }
+
+    /**
+     * Only allow setting of a property if $allowModifications
+     * was set to true on construction. Otherwise, throw an exception.
+     *
+     * @param  string $name
+     * @param  mixed  $value
+     * @throws Zend_Config_Exception
+     * @return void
+     */
+    public function __set($name, $value)
+    {
+        if ($this->_allowModifications) {
+            if (is_array($value)) {
+                $this->_data[$name] = new self($value, true);
+            } else {
+                $this->_data[$name] = $value;
+            }
+            $this->_count = count($this->_data);
+        } else {
+            /** @see Zend_Config_Exception */
+            require_once 'Zend/Config/Exception.php';
+            throw new Zend_Config_Exception('Zend_Config is read only');
+        }
+    }
+    
+    /**
+     * Deep clone of this instance to ensure that nested Zend_Configs
+     * are also cloned.
+     * 
+     * @return void
+     */
+    public function __clone()
+    {
+      $array = array();
+      foreach ($this->_data as $key => $value) {
+          if ($value instanceof Zend_Config) {
+              $array[$key] = clone $value;
+          } else {
+              $array[$key] = $value;
+          }
+      }
+      $this->_data = $array;
+    }
+
+    /**
+     * Return an associative array of the stored data.
+     *
+     * @return array
+     */
+    public function toArray()
+    {
+        $array = array();
+        $data = $this->_data;
+        foreach ($data as $key => $value) {
+            if ($value instanceof Zend_Config) {
+                $array[$key] = $value->toArray();
+            } else {
+                $array[$key] = $value;
+            }
+        }
+        return $array;
+    }
+
+    /**
+     * Support isset() overloading on PHP 5.1
+     *
+     * @param string $name
+     * @return boolean
+     */
+    public function __isset($name)
+    {
+        return isset($this->_data[$name]);
+    }
+
+    /**
+     * Support unset() overloading on PHP 5.1
+     *
+     * @param  string $name
+     * @throws Zend_Config_Exception
+     * @return void
+     */
+    public function __unset($name)
+    {
+        if ($this->_allowModifications) {
+            unset($this->_data[$name]);
+            $this->_count = count($this->_data);
+            $this->_skipNextIteration = true;
+        } else {
+            /** @see Zend_Config_Exception */
+            require_once 'Zend/Config/Exception.php';
+            throw new Zend_Config_Exception('Zend_Config is read only');
+        }
+
+    }
+
+    /**
+     * Defined by Countable interface
+     *
+     * @return int
+     */
+    public function count()
+    {
+        return $this->_count;
+    }
+
+    /**
+     * Defined by Iterator interface
+     *
+     * @return mixed
+     */
+    public function current()
+    {
+        $this->_skipNextIteration = false;
+        return current($this->_data);
+    }
+
+    /**
+     * Defined by Iterator interface
+     *
+     * @return mixed
+     */
+    public function key()
+    {
+        return key($this->_data);
+    }
+
+    /**
+     * Defined by Iterator interface
+     *
+     */
+    public function next()
+    {
+        if ($this->_skipNextIteration) {
+            $this->_skipNextIteration = false;
+            return;
+        }
+        next($this->_data);
+        $this->_index++;
+    }
+
+    /**
+     * Defined by Iterator interface
+     *
+     */
+    public function rewind()
+    {
+        $this->_skipNextIteration = false;
+        reset($this->_data);
+        $this->_index = 0;
+    }
+
+    /**
+     * Defined by Iterator interface
+     *
+     * @return boolean
+     */
+    public function valid()
+    {
+        return $this->_index < $this->_count;
+    }
+
+    /**
+     * Returns the section name(s) loaded.
+     *
+     * @return mixed
+     */
+    public function getSectionName()
+    {
+        if(is_array($this->_loadedSection) && count($this->_loadedSection) == 1) {
+            $this->_loadedSection = $this->_loadedSection[0];
+        }
+        return $this->_loadedSection;
+    }
+
+    /**
+     * Returns true if all sections were loaded
+     *
+     * @return boolean
+     */
+    public function areAllSectionsLoaded()
+    {
+        return $this->_loadedSection === null;
+    }
+
+
+    /**
+     * Merge another Zend_Config with this one. The items
+     * in $merge will override the same named items in
+     * the current config.
+     *
+     * @param Zend_Config $merge
+     * @return Zend_Config
+     */
+    public function merge(Zend_Config $merge)
+    {
+        foreach($merge as $key => $item) {
+            if(array_key_exists($key, $this->_data)) {
+                if($item instanceof Zend_Config && $this->$key instanceof Zend_Config) {
+                    $this->$key = $this->$key->merge(new Zend_Config($item->toArray(), !$this->readOnly()));
+                } else {
+                    $this->$key = $item;
+                }
+            } else {
+                if($item instanceof Zend_Config) {
+                    $this->$key = new Zend_Config($item->toArray(), !$this->readOnly());
+                } else {
+                    $this->$key = $item;
+                }
+            }
+        }
+
+        return $this;
+    }
+
+    /**
+     * Prevent any more modifications being made to this instance. Useful
+     * after merge() has been used to merge multiple Zend_Config objects
+     * into one object which should then not be modified again.
+     *
+     */
+    public function setReadOnly()
+    {
+        $this->_allowModifications = false;
+        foreach ($this->_data as $key => $value) {
+            if ($value instanceof Zend_Config) {
+                $value->setReadOnly();
+            }
+        }
+    }
+    
+    /**
+     * Returns if this Zend_Config object is read only or not.
+     *
+     * @return boolean
+     */
+    public function readOnly()
+    {
+        return !$this->_allowModifications;
+    }
+    
+    /**
+     * Get the current extends
+     *
+     * @return array
+     */
+    public function getExtends()
+    {
+        return $this->_extends;
+    }
+    
+    /**
+     * Set an extend for Zend_Config_Writer
+     *
+     * @param  string $extendingSection
+     * @param  string $extendedSection
+     * @return void
+     */
+    public function setExtend($extendingSection, $extendedSection = null)
+    {
+        if ($extendedSection === null && isset($this->_extends[$extendingSection])) {
+            unset($this->_extends[$extendingSection]);
+        } else if ($extendedSection !== null) {
+            $this->_extends[$extendingSection] = $extendedSection;
+        }
+    }
+    
+    /**
+     * Throws an exception if $extendingSection may not extend $extendedSection,
+     * and tracks the section extension if it is valid.
+     *
+     * @param  string $extendingSection
+     * @param  string $extendedSection
+     * @throws Zend_Config_Exception
+     * @return void
+     */
+    protected function _assertValidExtend($extendingSection, $extendedSection)
+    {
+        // detect circular section inheritance
+        $extendedSectionCurrent = $extendedSection;
+        while (array_key_exists($extendedSectionCurrent, $this->_extends)) {
+            if ($this->_extends[$extendedSectionCurrent] == $extendingSection) {
+                /** @see Zend_Config_Exception */
+                require_once 'Zend/Config/Exception.php';
+                throw new Zend_Config_Exception('Illegal circular inheritance detected');
+            }
+            $extendedSectionCurrent = $this->_extends[$extendedSectionCurrent];
+        }
+        // remember that this section extends another section
+        $this->_extends[$extendingSection] = $extendedSection;
+    }
+
+    /**
+     * Handle any errors from simplexml_load_file or parse_ini_file
+     *
+     * @param integer $errno
+     * @param string $errstr
+     * @param string $errfile
+     * @param integer $errline
+     */
+    protected function _loadFileErrorHandler($errno, $errstr, $errfile, $errline)
+    { 
+        if ($this->_loadFileErrorStr === null) {
+            $this->_loadFileErrorStr = $errstr;
+        } else {
+            $this->_loadFileErrorStr .= (PHP_EOL . $errstr);
+        }
+    }
+
+}
diff --git a/lib/Zend/Config/Exception.php b/lib/Zend/Config/Exception.php
new file mode 100644 (file)
index 0000000..b2f7396
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Config
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Exception.php 10020 2009-08-18 14:34:09Z j.fischer@metaways.de $
+ */
+
+/**
+ * @see Zend_Exception
+ */
+require_once 'Zend/Exception.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Config
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Config_Exception extends Zend_Exception {}
diff --git a/lib/Zend/Config/Ini.php b/lib/Zend/Config/Ini.php
new file mode 100644 (file)
index 0000000..66cc186
--- /dev/null
@@ -0,0 +1,293 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Config
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Ini.php 10020 2009-08-18 14:34:09Z j.fischer@metaways.de $
+ */
+
+
+/**
+ * @see Zend_Config
+ */
+require_once 'Zend/Config.php';
+
+
+/**
+ * @category   Zend
+ * @package    Zend_Config
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Config_Ini extends Zend_Config
+{
+    /**
+     * String that separates nesting levels of configuration data identifiers
+     *
+     * @var string
+     */
+    protected $_nestSeparator = '.';
+
+    /**
+     * String that separates the parent section name
+     *
+     * @var string
+     */
+    protected $_sectionSeparator = ':';
+
+    /**
+     * Wether to skip extends or not
+     *
+     * @var boolean
+     */
+    protected $_skipExtends = false;
+    
+    /**
+     * Loads the section $section from the config file $filename for
+     * access facilitated by nested object properties.
+     *
+     * If the section name contains a ":" then the section name to the right
+     * is loaded and included into the properties. Note that the keys in
+     * this $section will override any keys of the same
+     * name in the sections that have been included via ":".
+     *
+     * If the $section is null, then all sections in the ini file are loaded.
+     *
+     * If any key includes a ".", then this will act as a separator to
+     * create a sub-property.
+     *
+     * example ini file:
+     *      [all]
+     *      db.connection = database
+     *      hostname = live
+     *
+     *      [staging : all]
+     *      hostname = staging
+     *
+     * after calling $data = new Zend_Config_Ini($file, 'staging'); then
+     *      $data->hostname === "staging"
+     *      $data->db->connection === "database"
+     *
+     * The $options parameter may be provided as either a boolean or an array.
+     * If provided as a boolean, this sets the $allowModifications option of
+     * Zend_Config. If provided as an array, there are two configuration
+     * directives that may be set. For example:
+     *
+     * $options = array(
+     *     'allowModifications' => false,
+     *     'nestSeparator'      => '->'
+     *      );
+     *
+     * @param  string        $filename
+     * @param  string|null   $section
+     * @param  boolean|array $options
+     * @throws Zend_Config_Exception
+     * @return void
+     */
+    public function __construct($filename, $section = null, $options = false)
+    {
+        if (empty($filename)) {
+            /**
+             * @see Zend_Config_Exception
+             */
+            require_once 'Zend/Config/Exception.php';
+            throw new Zend_Config_Exception('Filename is not set');
+        }
+
+        $allowModifications = false;
+        if (is_bool($options)) {
+            $allowModifications = $options;
+        } elseif (is_array($options)) {
+            if (isset($options['allowModifications'])) {
+                $allowModifications = (bool) $options['allowModifications'];
+            }
+            if (isset($options['nestSeparator'])) {
+                $this->_nestSeparator = (string) $options['nestSeparator'];
+            }
+            if (isset($options['skipExtends'])) {
+                $this->_skipExtends = (bool) $options['skipExtends'];
+            }
+        }
+
+        $iniArray = $this->_loadIniFile($filename);
+
+        if (null === $section) {
+            // Load entire file
+            $dataArray = array();
+            foreach ($iniArray as $sectionName => $sectionData) {
+                if(!is_array($sectionData)) {
+                    $dataArray = array_merge_recursive($dataArray, $this->_processKey(array(), $sectionName, $sectionData));
+                } else {
+                    $dataArray[$sectionName] = $this->_processSection($iniArray, $sectionName);
+                }
+            }
+            parent::__construct($dataArray, $allowModifications);
+        } else {
+            // Load one or more sections
+            if (!is_array($section)) {
+                $section = array($section);
+            }
+            $dataArray = array();
+            foreach ($section as $sectionName) {
+                if (!isset($iniArray[$sectionName])) {
+                    /**
+                     * @see Zend_Config_Exception
+                     */
+                    require_once 'Zend/Config/Exception.php';
+                    throw new Zend_Config_Exception("Section '$sectionName' cannot be found in $filename");
+                }
+                $dataArray = array_merge($this->_processSection($iniArray, $sectionName), $dataArray);
+
+            }
+            parent::__construct($dataArray, $allowModifications);
+        } 
+
+        $this->_loadedSection = $section;
+    }
+
+    /**
+     * Load the ini file and preprocess the section separator (':' in the
+     * section name (that is used for section extension) so that the resultant
+     * array has the correct section names and the extension information is
+     * stored in a sub-key called ';extends'. We use ';extends' as this can
+     * never be a valid key name in an INI file that has been loaded using
+     * parse_ini_file().
+     *
+     * @param string $filename
+     * @throws Zend_Config_Exception
+     * @return array
+     */
+    protected function _loadIniFile($filename)
+    {
+        set_error_handler(array($this, '_loadFileErrorHandler'));
+        $loaded = parse_ini_file($filename, true); // Warnings and errors are suppressed
+        restore_error_handler();
+        // Check if there was a error while loading file
+        if ($this->_loadFileErrorStr !== null) {
+            /**
+             * @see Zend_Config_Exception
+             */
+            require_once 'Zend/Config/Exception.php';
+            throw new Zend_Config_Exception($this->_loadFileErrorStr);
+        }
+
+        $iniArray = array();
+        foreach ($loaded as $key => $data)
+        {
+            $pieces = explode($this->_sectionSeparator, $key);
+            $thisSection = trim($pieces[0]);
+            switch (count($pieces)) {
+                case 1:
+                    $iniArray[$thisSection] = $data;
+                    break;
+
+                case 2:
+                    $extendedSection = trim($pieces[1]);
+                    $iniArray[$thisSection] = array_merge(array(';extends'=>$extendedSection), $data);
+                    break;
+
+                default:
+                    /**
+                     * @see Zend_Config_Exception
+                     */
+                    require_once 'Zend/Config/Exception.php';
+                    throw new Zend_Config_Exception("Section '$thisSection' may not extend multiple sections in $filename");
+            }
+        }
+
+        return $iniArray;
+    }
+    
+    /**
+     * Process each element in the section and handle the ";extends" inheritance
+     * key. Passes control to _processKey() to handle the nest separator
+     * sub-property syntax that may be used within the key name.
+     *
+     * @param  array  $iniArray
+     * @param  string $section
+     * @param  array  $config
+     * @throws Zend_Config_Exception
+     * @return array
+     */
+    protected function _processSection($iniArray, $section, $config = array())
+    {
+        $thisSection = $iniArray[$section];
+
+        foreach ($thisSection as $key => $value) {
+            if (strtolower($key) == ';extends') {
+                if (isset($iniArray[$value])) {
+                    $this->_assertValidExtend($section, $value);
+                    
+                    if (!$this->_skipExtends) {
+                        $config = $this->_processSection($iniArray, $value, $config);
+                    }
+                } else {
+                    /**
+                     * @see Zend_Config_Exception
+                     */
+                    require_once 'Zend/Config/Exception.php';
+                    throw new Zend_Config_Exception("Parent section '$section' cannot be found");
+                }
+            } else {
+                $config = $this->_processKey($config, $key, $value);
+            }
+        }
+        return $config;
+    }
+
+    /**
+     * Assign the key's value to the property list. Handles the
+     * nest separator for sub-properties.
+     *
+     * @param  array  $config
+     * @param  string $key
+     * @param  string $value
+     * @throws Zend_Config_Exception
+     * @return array
+     */
+    protected function _processKey($config, $key, $value)
+    {
+        if (strpos($key, $this->_nestSeparator) !== false) {
+            $pieces = explode($this->_nestSeparator, $key, 2);
+            if (strlen($pieces[0]) && strlen($pieces[1])) {
+                if (!isset($config[$pieces[0]])) {
+                    if ($pieces[0] === '0' && !empty($config)) {
+                        // convert the current values in $config into an array
+                        $config = array($pieces[0] => $config);
+                    } else {
+                        $config[$pieces[0]] = array();
+                    }
+                } elseif (!is_array($config[$pieces[0]])) {
+                    /**
+                     * @see Zend_Config_Exception
+                     */
+                    require_once 'Zend/Config/Exception.php';
+                    throw new Zend_Config_Exception("Cannot create sub-key for '{$pieces[0]}' as key already exists");
+                }
+                $config[$pieces[0]] = $this->_processKey($config[$pieces[0]], $pieces[1], $value);
+            } else {
+                /**
+                 * @see Zend_Config_Exception
+                 */
+                require_once 'Zend/Config/Exception.php';
+                throw new Zend_Config_Exception("Invalid key '$key'");
+            }
+        } else {
+            $config[$key] = $value;
+        }
+        return $config;
+    }
+}
diff --git a/lib/Zend/Config/Writer.php b/lib/Zend/Config/Writer.php
new file mode 100644 (file)
index 0000000..9480eaa
--- /dev/null
@@ -0,0 +1,101 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Config
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Writer.php 10020 2009-08-18 14:34:09Z j.fischer@metaways.de $
+ */
+
+/**
+ * @category   Zend
+ * @package    Zend_Config
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+abstract class Zend_Config_Writer
+{
+    /**
+     * Option keys to skip when calling setOptions()
+     * 
+     * @var array
+     */
+    protected $_skipOptions = array(
+        'options'
+    );
+    
+    /**
+     * Config object to write
+     *
+     * @var Zend_Config
+     */
+    protected $_config = null;
+
+    /**
+     * Create a new adapter
+     * 
+     * $options can only be passed as array or be omitted 
+     *
+     * @param null|array $options
+     */
+    public function __construct(array $options = null)
+    {
+        if (is_array($options)) {
+            $this->setOptions($options);
+        }
+    }
+    
+    /**
+     * Set options via a Zend_Config instance
+     *
+     * @param  Zend_Config $config
+     * @return Zend_Config_Writer
+     */
+    public function setConfig(Zend_Config $config)
+    {
+        $this->_config = $config;
+        
+        return $this;
+    }
+    
+    /**
+     * Set options via an array
+     *
+     * @param  array $options
+     * @return Zend_Config_Writer
+     */
+    public function setOptions(array $options)
+    {
+        foreach ($options as $key => $value) {
+            if (in_array(strtolower($key), $this->_skipOptions)) {
+                continue;
+            }
+
+            $method = 'set' . ucfirst($key);
+            if (method_exists($this, $method)) {
+                $this->$method($value);
+            }
+        }
+        
+        return $this;
+    }
+    
+    /**
+     * Write a Zend_Config object to it's target
+     *
+     * @return void
+     */
+    abstract public function write();
+}
diff --git a/lib/Zend/Config/Writer/Array.php b/lib/Zend/Config/Writer/Array.php
new file mode 100644 (file)
index 0000000..0f8f8cd
--- /dev/null
@@ -0,0 +1,132 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Config
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Array.php 10020 2009-08-18 14:34:09Z j.fischer@metaways.de $
+ */
+
+/**
+ * @see Zend_Config_Writer
+ */
+require_once 'Zend/Config/Writer.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Config
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Config_Writer_Array extends Zend_Config_Writer
+{
+    /**
+     * Filename to write to
+     *
+     * @var string
+     */
+    protected $_filename = null;
+    
+    /**
+     * Wether to exclusively lock the file or not
+     *
+     * @var boolean
+     */
+    protected $_exclusiveLock = false;
+    
+    /**
+     * Set the target filename
+     *
+     * @param  string $filename
+     * @return Zend_Config_Writer_Array
+     */
+    public function setFilename($filename)
+    {
+        $this->_filename = $filename;
+        
+        return $this;
+    }
+    
+    /**
+     * Set wether to exclusively lock the file or not
+     *
+     * @param  boolean     $exclusiveLock
+     * @return Zend_Config_Writer_Array
+     */
+    public function setExclusiveLock($exclusiveLock)
+    {
+        $this->_exclusiveLock = $exclusiveLock;
+        
+        return $this;
+    }
+    
+    /**
+     * Defined by Zend_Config_Writer
+     *
+     * @param  string      $filename
+     * @param  Zend_Config $config
+     * @param  boolean     $exclusiveLock
+     * @throws Zend_Config_Exception When filename was not set
+     * @throws Zend_Config_Exception When filename is not writable
+     * @return void
+     */
+    public function write($filename = null, Zend_Config $config = null, $exclusiveLock = null)
+    {
+        if ($filename !== null) {
+            $this->setFilename($filename);
+        }
+        
+        if ($config !== null) {
+            $this->setConfig($config);
+        }
+        
+        if ($exclusiveLock !== null) {
+            $this->setExclusiveLock($exclusiveLock);
+        }
+        
+        if ($this->_filename === null) {
+            require_once 'Zend/Config/Exception.php';
+            throw new Zend_Config_Exception('No filename was set');
+        }
+        
+        if ($this->_config === null) {
+            require_once 'Zend/Config/Exception.php';
+            throw new Zend_Config_Exception('No config was set');
+        }
+        
+        $data        = $this->_config->toArray();
+        $sectionName = $this->_config->getSectionName();
+        
+        if (is_string($sectionName)) {
+            $data = array($sectionName => $data);
+        }
+        
+        $arrayString = "<?php\n"
+                     . "return " . var_export($data, true) . ";\n";
+       
+        $flags = 0;
+        
+        if ($this->_exclusiveLock) {
+            $flags |= LOCK_EX;
+        }
+                     
+        $result = @file_put_contents($this->_filename, $arrayString, $flags);
+        
+        if ($result === false) {
+            require_once 'Zend/Config/Exception.php';
+            throw new Zend_Config_Exception('Could not write to file "' . $this->_filename . '"');
+        }
+    }
+}
diff --git a/lib/Zend/Config/Writer/Ini.php b/lib/Zend/Config/Writer/Ini.php
new file mode 100644 (file)
index 0000000..0da2b20
--- /dev/null
@@ -0,0 +1,212 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Config
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Ini.php 10020 2009-08-18 14:34:09Z j.fischer@metaways.de $
+ */
+
+/**
+ * @see Zend_Config_Writer
+ */
+require_once 'Zend/Config/Writer.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Config
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Config_Writer_Ini extends Zend_Config_Writer
+{
+    /**
+     * Filename to write to
+     *
+     * @var string
+     */
+    protected $_filename = null;
+        
+    /**
+     * Wether to exclusively lock the file or not
+     *
+     * @var boolean
+     */
+    protected $_exclusiveLock = false;
+    
+    /**
+     * String that separates nesting levels of configuration data identifiers
+     *
+     * @var string
+     */
+    protected $_nestSeparator = '.';
+    
+    /**
+     * Set the target filename
+     *
+     * @param  string $filename
+     * @return Zend_Config_Writer_Xml
+     */
+    public function setFilename($filename)
+    {
+        $this->_filename = $filename;
+        
+        return $this;
+    }
+    
+    /**
+     * Set wether to exclusively lock the file or not
+     *
+     * @param  boolean     $exclusiveLock
+     * @return Zend_Config_Writer_Array
+     */
+    public function setExclusiveLock($exclusiveLock)
+    {
+        $this->_exclusiveLock = $exclusiveLock;
+        
+        return $this;
+    }
+    
+    /**
+     * Set the nest separator
+     *
+     * @param  string $filename
+     * @return Zend_Config_Writer_Ini
+     */
+    public function setNestSeparator($separator)
+    {
+        $this->_nestSeparator = $separator;
+        
+        return $this;
+    }
+    
+    /**
+     * Defined by Zend_Config_Writer
+     *
+     * @param  string      $filename
+     * @param  Zend_Config $config
+     * @param  boolean     $exclusiveLock
+     * @throws Zend_Config_Exception When filename was not set
+     * @throws Zend_Config_Exception When filename is not writable
+     * @return void
+     */
+    public function write($filename = null, Zend_Config $config = null, $exclusiveLock = null)
+    {
+        if ($filename !== null) {
+            $this->setFilename($filename);
+        }
+        
+        if ($config !== null) {
+            $this->setConfig($config);
+        }
+        
+        if ($exclusiveLock !== null) {
+            $this->setExclusiveLock($exclusiveLock);
+        }
+        
+        if ($this->_filename === null) {
+            require_once 'Zend/Config/Exception.php';
+            throw new Zend_Config_Exception('No filename was set');
+        }
+        
+        if ($this->_config === null) {
+            require_once 'Zend/Config/Exception.php';
+            throw new Zend_Config_Exception('No config was set');
+        }
+        
+        $iniString   = '';
+        $extends     = $this->_config->getExtends();
+        $sectionName = $this->_config->getSectionName();
+        
+        if (is_string($sectionName)) {
+            $iniString .= '[' . $sectionName . ']' . "\n"
+                       .  $this->_addBranch($this->_config)
+                       .  "\n";
+        } else {
+            foreach ($this->_config as $sectionName => $data) {
+                if (!($data instanceof Zend_Config)) {
+                    $iniString .= $sectionName
+                               .  ' = '
+                               .  $this->_prepareValue($data)
+                               .  "\n";
+                } else {
+                    if (isset($extends[$sectionName])) {
+                        $sectionName .= ' : ' . $extends[$sectionName];
+                    }
+                    
+                    $iniString .= '[' . $sectionName . ']' . "\n"
+                               .  $this->_addBranch($data)
+                               .  "\n";
+                }
+            }
+        }
+       
+        $flags = 0;
+        
+        if ($this->_exclusiveLock) {
+            $flags |= LOCK_EX;
+        }
+        
+        $result = @file_put_contents($this->_filename, $iniString, $flags);
+
+        if ($result === false) {
+            require_once 'Zend/Config/Exception.php';
+            throw new Zend_Config_Exception('Could not write to file "' . $this->_filename . '"');
+        }
+    }
+    
+    /**
+     * Add a branch to an INI string recursively
+     *
+     * @param  Zend_Config $config
+     * @return void
+     */
+    protected function _addBranch(Zend_Config $config, $parents = array())
+    {
+        $iniString = '';
+
+        foreach ($config as $key => $value) {
+            $group = array_merge($parents, array($key));
+            
+            if ($value instanceof Zend_Config) {
+                $iniString .= $this->_addBranch($value, $group);
+            } else {
+                $iniString .= implode($this->_nestSeparator, $group)
+                           .  ' = '
+                           .  $this->_prepareValue($value)
+                           .  "\n";
+            }
+        }
+        
+        return $iniString;
+    }
+    
+    /**
+     * Prepare a value for INI
+     *
+     * @param  mixed $value
+     * @return string
+     */
+    protected function _prepareValue($value)
+    {
+        if (is_integer($value) || is_float($value)) {
+            return $value;
+        } elseif (is_bool($value)) {
+            return ($value ? 'true' : 'false');
+        } else {
+            return '"' . addslashes($value) .  '"';
+        }
+    }
+}
diff --git a/lib/Zend/Config/Writer/Xml.php b/lib/Zend/Config/Writer/Xml.php
new file mode 100644 (file)
index 0000000..db2f57a
--- /dev/null
@@ -0,0 +1,204 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Config
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Xml.php 10020 2009-08-18 14:34:09Z j.fischer@metaways.de $
+ */
+
+/**
+ * @see Zend_Config_Writer
+ */
+require_once 'Zend/Config/Writer.php';
+
+/**
+ * @see Zend_Config_Xml
+ */
+require_once 'Zend/Config/Xml.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Config
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Config_Writer_Xml extends Zend_Config_Writer
+{
+    /**
+     * Filename to write to
+     *
+     * @var string
+     */
+    protected $_filename = null;
+    
+    /**
+     * Wether to exclusively lock the file or not
+     *
+     * @var boolean
+     */
+    protected $_exclusiveLock = false;
+    
+    /**
+     * Set the target filename
+     *
+     * @param  string $filename
+     * @return Zend_Config_Writer_Xml
+     */
+    public function setFilename($filename)
+    {
+        $this->_filename = $filename;
+        
+        return $this;
+    }
+    
+    /**
+     * Set wether to exclusively lock the file or not
+     *
+     * @param  boolean     $exclusiveLock
+     * @return Zend_Config_Writer_Array
+     */
+    public function setExclusiveLock($exclusiveLock)
+    {
+        $this->_exclusiveLock = $exclusiveLock;
+        
+        return $this;
+    }
+    
+    /**
+     * Defined by Zend_Config_Writer
+     *
+     * @param  string      $filename
+     * @param  Zend_Config $config
+     * @param  boolean     $exclusiveLock
+     * @throws Zend_Config_Exception When filename was not set
+     * @throws Zend_Config_Exception When filename is not writable
+     * @return void
+     */
+    public function write($filename = null, Zend_Config $config = null, $exclusiveLock = null)
+    {
+        if ($filename !== null) {
+            $this->setFilename($filename);
+        }
+        
+        if ($config !== null) {
+            $this->setConfig($config);
+        }
+        
+        if ($exclusiveLock !== null) {
+            $this->setExclusiveLock($exclusiveLock);
+        }
+        
+        if ($this->_filename === null) {
+            require_once 'Zend/Config/Exception.php';
+            throw new Zend_Config_Exception('No filename was set');
+        }
+        
+        if ($this->_config === null) {
+            require_once 'Zend/Config/Exception.php';
+            throw new Zend_Config_Exception('No config was set');
+        }
+        
+        $xml         = new SimpleXMLElement('<zend-config xmlns:zf="' . Zend_Config_Xml::XML_NAMESPACE . '"/>');
+        $extends     = $this->_config->getExtends();
+        $sectionName = $this->_config->getSectionName();
+        
+        if (is_string($sectionName)) {
+            $child = $xml->addChild($sectionName);
+
+            $this->_addBranch($this->_config, $child, $xml);
+        } else {
+            foreach ($this->_config as $sectionName => $data) {
+                if (!($data instanceof Zend_Config)) {
+                    $xml->addChild($sectionName, (string) $data);
+                } else {
+                    $child = $xml->addChild($sectionName);
+                    
+                    if (isset($extends[$sectionName])) {
+                        $child->addAttribute('zf:extends', $extends[$sectionName], Zend_Config_Xml::XML_NAMESPACE);
+                    }
+        
+                    $this->_addBranch($data, $child, $xml);
+                }
+            }
+        }
+                
+        $dom = dom_import_simplexml($xml)->ownerDocument;
+        $dom->formatOutput = true;
+        
+        $xmlString = $dom->saveXML();
+       
+        $flags = 0;
+        
+        if ($this->_exclusiveLock) {
+            $flags |= LOCK_EX;
+        }
+        
+        $result = @file_put_contents($this->_filename, $xmlString, $flags);
+        
+        if ($result === false) {
+            require_once 'Zend/Config/Exception.php';
+            throw new Zend_Config_Exception('Could not write to file "' . $this->_filename . '"');
+        }
+    }
+    
+    /**
+     * Add a branch to an XML object recursively
+     *
+     * @param  Zend_Config      $config
+     * @param  SimpleXMLElement $xml
+     * @param  SimpleXMLElement $parent
+     * @return void
+     */
+    protected function _addBranch(Zend_Config $config, SimpleXMLElement $xml, SimpleXMLElement $parent)
+    {
+        $branchType = null;
+        
+        foreach ($config as $key => $value) {
+            if ($branchType === null) {
+                if (is_numeric($key)) {
+                    $branchType = 'numeric';
+                    $branchName = $xml->getName();
+                    $xml        = $parent;
+                    
+                    unset($parent->{$branchName});
+                } else {
+                    $branchType = 'string';
+                }
+            } else if ($branchType !== (is_numeric($key) ? 'numeric' : 'string')) {
+                require_once 'Zend/Config/Exception.php';
+                throw new Zend_Config_Exception('Mixing of string and numeric keys is not allowed');                
+            }
+            
+            if ($branchType === 'numeric') {
+                if ($value instanceof Zend_Config) {
+                    $child = $parent->addChild($branchName, (string) $value);
+    
+                    $this->_addBranch($value, $child, $parent);
+                } else {
+                    $parent->addChild($branchName, (string) $value);
+                }
+            } else {            
+                if ($value instanceof Zend_Config) {
+                    $child = $xml->addChild($key);
+    
+                    $this->_addBranch($value, $child, $xml);
+                } else {
+                    $xml->addChild($key, (string) $value);
+                }
+            }
+        }
+    }
+}
diff --git a/lib/Zend/Config/Xml.php b/lib/Zend/Config/Xml.php
new file mode 100644 (file)
index 0000000..0931461
--- /dev/null
@@ -0,0 +1,315 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category  Zend
+ * @package   Zend_Config
+ * @copyright Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license   http://framework.zend.com/license/new-bsd     New BSD License
+ * @version   $Id: Xml.php 10020 2009-08-18 14:34:09Z j.fischer@metaways.de $
+ */
+
+/**
+ * @see Zend_Config
+ */
+require_once 'Zend/Config.php';
+
+/**
+ * XML Adapter for Zend_Config
+ *
+ * @category  Zend
+ * @package   Zend_Config
+ * @copyright Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license   http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Config_Xml extends Zend_Config
+{
+    /**
+     * XML namespace for ZF-related tags and attributes
+     */
+    const XML_NAMESPACE = 'http://framework.zend.com/xml/zend-config-xml/1.0/';
+
+    /**
+     * Wether to skip extends or not
+     *
+     * @var boolean
+     */
+    protected $_skipExtends = false;
+
+    /**
+     * Loads the section $section from the config file (or string $xml for
+     * access facilitated by nested object properties.
+     *
+     * Sections are defined in the XML as children of the root element.
+     *
+     * In order to extend another section, a section defines the "extends"
+     * attribute having a value of the section name from which the extending
+     * section inherits values.
+     *
+     * Note that the keys in $section will override any keys of the same
+     * name in the sections that have been included via "extends".
+     *
+     * @param  string  $xml     XML file or string to process
+     * @param  mixed   $section Section to process
+     * @param  boolean $options Whether modifiacations are allowed at runtime
+     * @throws Zend_Config_Exception When xml is not set or cannot be loaded
+     * @throws Zend_Config_Exception When section $sectionName cannot be found in $xml
+     */
+    public function __construct($xml, $section = null, $options = false)
+    {
+        if (empty($xml)) {
+            require_once 'Zend/Config/Exception.php';
+            throw new Zend_Config_Exception('Filename is not set');
+        }
+
+        $allowModifications = false;
+        if (is_bool($options)) {
+            $allowModifications = $options;
+        } elseif (is_array($options)) {
+            if (isset($options['allowModifications'])) {
+                $allowModifications = (bool) $options['allowModifications'];
+            }
+            if (isset($options['skipExtends'])) {
+                $this->_skipExtends = (bool) $options['skipExtends'];
+            }
+        }
+
+        set_error_handler(array($this, '_loadFileErrorHandler')); // Warnings and errors are suppressed
+        if (strstr($xml, '<?xml')) {
+            $config = simplexml_load_string($xml);
+        } else {
+            $config = simplexml_load_file($xml);
+        }
+
+        restore_error_handler();
+        // Check if there was a error while loading file
+        if ($this->_loadFileErrorStr !== null) {
+            require_once 'Zend/Config/Exception.php';
+            throw new Zend_Config_Exception($this->_loadFileErrorStr);
+        }
+
+        if ($section === null) {
+            $dataArray = array();
+            foreach ($config as $sectionName => $sectionData) {
+                $dataArray[$sectionName] = $this->_processExtends($config, $sectionName);
+            }
+
+            parent::__construct($dataArray, $allowModifications);
+        } else if (is_array($section)) {
+            $dataArray = array();
+            foreach ($section as $sectionName) {
+                if (!isset($config->$sectionName)) {
+                    require_once 'Zend/Config/Exception.php';
+                    throw new Zend_Config_Exception("Section '$sectionName' cannot be found in $xml");
+                }
+
+                $dataArray = array_merge($this->_processExtends($config, $sectionName), $dataArray);
+            }
+
+            parent::__construct($dataArray, $allowModifications);
+        } else {
+            if (!isset($config->$section)) {
+                require_once 'Zend/Config/Exception.php';
+                throw new Zend_Config_Exception("Section '$section' cannot be found in $xml");
+            }
+
+            $dataArray = $this->_processExtends($config, $section);
+            if (!is_array($dataArray)) {
+                // Section in the XML file contains just one top level string
+                $dataArray = array($section => $dataArray);
+            }
+
+            parent::__construct($dataArray, $allowModifications);
+        }
+
+        $this->_loadedSection = $section;
+    }
+
+    /**
+     * Helper function to process each element in the section and handle
+     * the "extends" inheritance attribute.
+     *
+     * @param  SimpleXMLElement $element XML Element to process
+     * @param  string           $section Section to process
+     * @param  array            $config  Configuration which was parsed yet
+     * @throws Zend_Config_Exception When $section cannot be found
+     * @return array
+     */
+    protected function _processExtends(SimpleXMLElement $element, $section, array $config = array())
+    {
+        if (!isset($element->$section)) {
+            require_once 'Zend/Config/Exception.php';
+            throw new Zend_Config_Exception("Section '$section' cannot be found");
+        }
+
+        $thisSection  = $element->$section;
+        $nsAttributes = $thisSection->attributes(self::XML_NAMESPACE);
+
+        if (isset($thisSection['extends']) || isset($nsAttributes['extends'])) {
+            $extendedSection = (string) (isset($nsAttributes['extends']) ? $nsAttributes['extends'] : $thisSection['extends']);
+            $this->_assertValidExtend($section, $extendedSection);
+
+            if (!$this->_skipExtends) {
+                $config = $this->_processExtends($element, $extendedSection, $config);
+            }
+        }
+
+        $config = $this->_arrayMergeRecursive($config, $this->_toArray($thisSection));
+
+        return $config;
+    }
+
+    /**
+     * Returns a string or an associative and possibly multidimensional array from
+     * a SimpleXMLElement.
+     *
+     * @param  SimpleXMLElement $xmlObject Convert a SimpleXMLElement into an array
+     * @return array|string
+     */
+    protected function _toArray(SimpleXMLElement $xmlObject)
+    {
+        $config       = array();
+        $nsAttributes = $xmlObject->attributes(self::XML_NAMESPACE);
+
+        // Search for parent node values
+        if (count($xmlObject->attributes()) > 0) {
+            foreach ($xmlObject->attributes() as $key => $value) {
+                if ($key === 'extends') {
+                    continue;
+                }
+
+                $value = (string) $value;
+
+                if (array_key_exists($key, $config)) {
+                    if (!is_array($config[$key])) {
+                        $config[$key] = array($config[$key]);
+                    }
+
+                    $config[$key][] = $value;
+                } else {
+                    $config[$key] = $value;
+                }
+            }
+        }
+
+        // Search for local 'const' nodes and replace them
+        if (count($xmlObject->children(self::XML_NAMESPACE)) > 0) {
+            if (count($xmlObject->children()) > 0) {
+                require_once 'Zend/Config/Exception.php';
+                throw new Zend_Config_Exception("A node with a 'const' childnode may not have any other children");
+            }
+
+            $dom                 = dom_import_simplexml($xmlObject);
+            $namespaceChildNodes = array();
+
+            // We have to store them in an array, as replacing nodes will
+            // confuse the DOMNodeList later
+            foreach ($dom->childNodes as $node) {
+                if ($node instanceof DOMElement && $node->namespaceURI === self::XML_NAMESPACE) {
+                    $namespaceChildNodes[] = $node;
+                }
+            }
+
+            foreach ($namespaceChildNodes as $node) {
+                switch ($node->localName) {
+                    case 'const':
+                        if (!$node->hasAttributeNS(self::XML_NAMESPACE, 'name')) {
+                            require_once 'Zend/Config/Exception.php';
+                            throw new Zend_Config_Exception("Misssing 'name' attribute in 'const' node");
+                        }
+
+                        $constantName = $node->getAttributeNS(self::XML_NAMESPACE, 'name');
+
+                        if (!defined($constantName)) {
+                            require_once 'Zend/Config/Exception.php';
+                            throw new Zend_Config_Exception("Constant with name '$constantName' was not defined");
+                        }
+
+                        $constantValue = constant($constantName);
+
+                        $dom->replaceChild($dom->ownerDocument->createTextNode($constantValue), $node);
+                        break;
+
+                    default:
+                        require_once 'Zend/Config/Exception.php';
+                        throw new Zend_Config_Exception("Unknown node with name '$node->localName' found");
+                }
+            }
+
+            return (string) simplexml_import_dom($dom);
+        }
+
+        // Search for children
+        if (count($xmlObject->children()) > 0) {
+            foreach ($xmlObject->children() as $key => $value) {
+                if (count($value->children()) > 0 || count($value->children(self::XML_NAMESPACE)) > 0) {
+                    $value = $this->_toArray($value);
+                } else if (count($value->attributes()) > 0) {
+                    $attributes = $value->attributes();
+                    if (isset($attributes['value'])) {
+                        $value = (string) $attributes['value'];
+                    } else {
+                        $value = $this->_toArray($value);
+                    }
+                } else {
+                    $value = (string) $value;
+                }
+
+                if (array_key_exists($key, $config)) {
+                    if (!is_array($config[$key]) || !array_key_exists(0, $config[$key])) {
+                        $config[$key] = array($config[$key]);
+                    }
+
+                    $config[$key][] = $value;
+                } else {
+                    $config[$key] = $value;
+                }
+            }
+        } else if (!isset($xmlObject['extends']) && !isset($nsAttributes['extends']) && (count($config) === 0)) {
+            // Object has no children nor attributes and doesn't use the extends
+            // attribute: it's a string
+            $config = (string) $xmlObject;
+        }
+
+        return $config;
+    }
+
+    /**
+     * Merge two arrays recursively, overwriting keys of the same name
+     * in $firstArray with the value in $secondArray.
+     *
+     * @param  mixed $firstArray  First array
+     * @param  mixed $secondArray Second array to merge into first array
+     * @return array
+     */
+    protected function _arrayMergeRecursive($firstArray, $secondArray)
+    {
+        if (is_array($firstArray) && is_array($secondArray)) {
+            foreach ($secondArray as $key => $value) {
+                if (isset($firstArray[$key])) {
+                    $firstArray[$key] = $this->_arrayMergeRecursive($firstArray[$key], $value);
+                } else {
+                    if($key === 0) {
+                        $firstArray= array(0=>$this->_arrayMergeRecursive($firstArray, $value));
+                    } else {
+                        $firstArray[$key] = $value;
+                    }
+                }
+            }
+        } else {
+            $firstArray = $secondArray;
+        }
+
+        return $firstArray;
+    }
+}
diff --git a/lib/Zend/Db.php b/lib/Zend/Db.php
new file mode 100644 (file)
index 0000000..9bff028
--- /dev/null
@@ -0,0 +1,276 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Db
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Db.php 10020 2009-08-18 14:34:09Z j.fischer@metaways.de $
+ */
+
+
+/**
+ * Class for connecting to SQL databases and performing common operations.
+ *
+ * @category   Zend
+ * @package    Zend_Db
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Db
+{
+
+    /**
+     * Use the PROFILER constant in the config of a Zend_Db_Adapter.
+     */
+    const PROFILER = 'profiler';
+
+    /**
+     * Use the CASE_FOLDING constant in the config of a Zend_Db_Adapter.
+     */
+    const CASE_FOLDING = 'caseFolding';
+
+    /**
+     * Use the AUTO_QUOTE_IDENTIFIERS constant in the config of a Zend_Db_Adapter.
+     */
+    const AUTO_QUOTE_IDENTIFIERS = 'autoQuoteIdentifiers';
+
+    /**
+     * Use the ALLOW_SERIALIZATION constant in the config of a Zend_Db_Adapter.
+     */
+    const ALLOW_SERIALIZATION = 'allowSerialization';
+
+    /**
+     * Use the AUTO_RECONNECT_ON_UNSERIALIZE constant in the config of a Zend_Db_Adapter.
+     */
+    const AUTO_RECONNECT_ON_UNSERIALIZE = 'autoReconnectOnUnserialize';
+
+    /**
+     * Use the INT_TYPE, BIGINT_TYPE, and FLOAT_TYPE with the quote() method.
+     */
+    const INT_TYPE    = 0;
+    const BIGINT_TYPE = 1;
+    const FLOAT_TYPE  = 2;
+
+    /**
+     * PDO constant values discovered by this script result:
+     *
+     * $list = array(
+     *    'PARAM_BOOL', 'PARAM_NULL', 'PARAM_INT', 'PARAM_STR', 'PARAM_LOB',
+     *    'PARAM_STMT', 'PARAM_INPUT_OUTPUT', 'FETCH_LAZY', 'FETCH_ASSOC',
+     *    'FETCH_NUM', 'FETCH_BOTH', 'FETCH_OBJ', 'FETCH_BOUND',
+     *    'FETCH_COLUMN', 'FETCH_CLASS', 'FETCH_INTO', 'FETCH_FUNC',
+     *    'FETCH_GROUP', 'FETCH_UNIQUE', 'FETCH_CLASSTYPE', 'FETCH_SERIALIZE',
+     *    'FETCH_NAMED', 'ATTR_AUTOCOMMIT', 'ATTR_PREFETCH', 'ATTR_TIMEOUT',
+     *    'ATTR_ERRMODE', 'ATTR_SERVER_VERSION', 'ATTR_CLIENT_VERSION',
+     *    'ATTR_SERVER_INFO', 'ATTR_CONNECTION_STATUS', 'ATTR_CASE',
+     *    'ATTR_CURSOR_NAME', 'ATTR_CURSOR', 'ATTR_ORACLE_NULLS',
+     *    'ATTR_PERSISTENT', 'ATTR_STATEMENT_CLASS', 'ATTR_FETCH_TABLE_NAMES',
+     *    'ATTR_FETCH_CATALOG_NAMES', 'ATTR_DRIVER_NAME',
+     *    'ATTR_STRINGIFY_FETCHES', 'ATTR_MAX_COLUMN_LEN', 'ERRMODE_SILENT',
+     *    'ERRMODE_WARNING', 'ERRMODE_EXCEPTION', 'CASE_NATURAL',
+     *    'CASE_LOWER', 'CASE_UPPER', 'NULL_NATURAL', 'NULL_EMPTY_STRING',
+     *    'NULL_TO_STRING', 'ERR_NONE', 'FETCH_ORI_NEXT',
+     *    'FETCH_ORI_PRIOR', 'FETCH_ORI_FIRST', 'FETCH_ORI_LAST',
+     *    'FETCH_ORI_ABS', 'FETCH_ORI_REL', 'CURSOR_FWDONLY', 'CURSOR_SCROLL',
+     *    'ERR_CANT_MAP', 'ERR_SYNTAX', 'ERR_CONSTRAINT', 'ERR_NOT_FOUND',
+     *    'ERR_ALREADY_EXISTS', 'ERR_NOT_IMPLEMENTED', 'ERR_MISMATCH',
+     *    'ERR_TRUNCATED', 'ERR_DISCONNECTED', 'ERR_NO_PERM',
+     * );
+     *
+     * $const = array();
+     * foreach ($list as $name) {
+     *    $const[$name] = constant("PDO::$name");
+     * }
+     * var_export($const);
+     */
+    const ATTR_AUTOCOMMIT = 0;
+    const ATTR_CASE = 8;
+    const ATTR_CLIENT_VERSION = 5;
+    const ATTR_CONNECTION_STATUS = 7;
+    const ATTR_CURSOR = 10;
+    const ATTR_CURSOR_NAME = 9;
+    const ATTR_DRIVER_NAME = 16;
+    const ATTR_ERRMODE = 3;
+    const ATTR_FETCH_CATALOG_NAMES = 15;
+    const ATTR_FETCH_TABLE_NAMES = 14;
+    const ATTR_MAX_COLUMN_LEN = 18;
+    const ATTR_ORACLE_NULLS = 11;
+    const ATTR_PERSISTENT = 12;
+    const ATTR_PREFETCH = 1;
+    const ATTR_SERVER_INFO = 6;
+    const ATTR_SERVER_VERSION = 4;
+    const ATTR_STATEMENT_CLASS = 13;
+    const ATTR_STRINGIFY_FETCHES = 17;
+    const ATTR_TIMEOUT = 2;
+    const CASE_LOWER = 2;
+    const CASE_NATURAL = 0;
+    const CASE_UPPER = 1;
+    const CURSOR_FWDONLY = 0;
+    const CURSOR_SCROLL = 1;
+    const ERR_ALREADY_EXISTS = NULL;
+    const ERR_CANT_MAP = NULL;
+    const ERR_CONSTRAINT = NULL;
+    const ERR_DISCONNECTED = NULL;
+    const ERR_MISMATCH = NULL;
+    const ERR_NO_PERM = NULL;
+    const ERR_NONE = '00000';
+    const ERR_NOT_FOUND = NULL;
+    const ERR_NOT_IMPLEMENTED = NULL;
+    const ERR_SYNTAX = NULL;
+    const ERR_TRUNCATED = NULL;
+    const ERRMODE_EXCEPTION = 2;
+    const ERRMODE_SILENT = 0;
+    const ERRMODE_WARNING = 1;
+    const FETCH_ASSOC = 2;
+    const FETCH_BOTH = 4;
+    const FETCH_BOUND = 6;
+    const FETCH_CLASS = 8;
+    const FETCH_CLASSTYPE = 262144;
+    const FETCH_COLUMN = 7;
+    const FETCH_FUNC = 10;
+    const FETCH_GROUP = 65536;
+    const FETCH_INTO = 9;
+    const FETCH_LAZY = 1;
+    const FETCH_NAMED = 11;
+    const FETCH_NUM = 3;
+    const FETCH_OBJ = 5;
+    const FETCH_ORI_ABS = 4;
+    const FETCH_ORI_FIRST = 2;
+    const FETCH_ORI_LAST = 3;
+    const FETCH_ORI_NEXT = 0;
+    const FETCH_ORI_PRIOR = 1;
+    const FETCH_ORI_REL = 5;
+    const FETCH_SERIALIZE = 524288;
+    const FETCH_UNIQUE = 196608;
+    const NULL_EMPTY_STRING = 1;
+    const NULL_NATURAL = 0;
+    const NULL_TO_STRING = NULL;
+    const PARAM_BOOL = 5;
+    const PARAM_INPUT_OUTPUT = -2147483648;
+    const PARAM_INT = 1;
+    const PARAM_LOB = 3;
+    const PARAM_NULL = 0;
+    const PARAM_STMT = 4;
+    const PARAM_STR = 2;
+
+    /**
+     * Factory for Zend_Db_Adapter_Abstract classes.
+     *
+     * First argument may be a string containing the base of the adapter class
+     * name, e.g. 'Mysqli' corresponds to class Zend_Db_Adapter_Mysqli.  This
+     * is case-insensitive.
+     *
+     * First argument may alternatively be an object of type Zend_Config.
+     * The adapter class base name is read from the 'adapter' property.
+     * The adapter config parameters are read from the 'params' property.
+     *
+     * Second argument is optional and may be an associative array of key-value
+     * pairs.  This is used as the argument to the adapter constructor.
+     *
+     * If the first argument is of type Zend_Config, it is assumed to contain
+     * all parameters, and the second argument is ignored.
+     *
+     * @param  mixed $adapter String name of base adapter class, or Zend_Config object.
+     * @param  mixed $config  OPTIONAL; an array or Zend_Config object with adapter parameters.
+     * @return Zend_Db_Adapter_Abstract
+     * @throws Zend_Db_Exception
+     */
+    public static function factory($adapter, $config = array())
+    {
+        if ($config instanceof Zend_Config) {
+            $config = $config->toArray();
+        }
+
+        /*
+         * Convert Zend_Config argument to plain string
+         * adapter name and separate config object.
+         */
+        if ($adapter instanceof Zend_Config) {
+            if (isset($adapter->params)) {
+                $config = $adapter->params->toArray();
+            }
+            if (isset($adapter->adapter)) {
+                $adapter = (string) $adapter->adapter;
+            } else {
+                $adapter = null;
+            }
+        }
+
+        /*
+         * Verify that adapter parameters are in an array.
+         */
+        if (!is_array($config)) {
+            /**
+             * @see Zend_Db_Exception
+             */
+            require_once 'Zend/Db/Exception.php';
+            throw new Zend_Db_Exception('Adapter parameters must be in an array or a Zend_Config object');
+        }
+
+        /*
+         * Verify that an adapter name has been specified.
+         */
+        if (!is_string($adapter) || empty($adapter)) {
+            /**
+             * @see Zend_Db_Exception
+             */
+            require_once 'Zend/Db/Exception.php';
+            throw new Zend_Db_Exception('Adapter name must be specified in a string');
+        }
+
+        /*
+         * Form full adapter class name
+         */
+        $adapterNamespace = 'Zend_Db_Adapter';
+        if (isset($config['adapterNamespace'])) {
+            if ($config['adapterNamespace'] != '') {
+                $adapterNamespace = $config['adapterNamespace'];
+            }
+            unset($config['adapterNamespace']);
+        }
+        $adapterName = strtolower($adapterNamespace . '_' . $adapter);
+        $adapterName = str_replace(' ', '_', ucwords(str_replace('_', ' ', $adapterName)));
+
+        /*
+         * Load the adapter class.  This throws an exception
+         * if the specified class cannot be loaded.
+         */
+        if (!class_exists($adapterName)) {
+            require_once 'Zend/Loader.php';
+            Zend_Loader::loadClass($adapterName);
+        }
+
+        /*
+         * Create an instance of the adapter class.
+         * Pass the config to the adapter class constructor.
+         */
+        $dbAdapter = new $adapterName($config);
+
+        /*
+         * Verify that the object created is a descendent of the abstract adapter type.
+         */
+        if (! $dbAdapter instanceof Zend_Db_Adapter_Abstract) {
+            /**
+             * @see Zend_Db_Exception
+             */
+            require_once 'Zend/Db/Exception.php';
+            throw new Zend_Db_Exception("Adapter class '$adapterName' does not extend Zend_Db_Adapter_Abstract");
+        }
+
+        return $dbAdapter;
+    }
+
+}
diff --git a/lib/Zend/Db/Adapter/Abstract.php b/lib/Zend/Db/Adapter/Abstract.php
new file mode 100644 (file)
index 0000000..92102a5
--- /dev/null
@@ -0,0 +1,1252 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Db
+ * @subpackage Adapter
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Abstract.php 10020 2009-08-18 14:34:09Z j.fischer@metaways.de $
+ */
+
+
+/**
+ * @see Zend_Db
+ */
+require_once 'Zend/Db.php';
+
+/**
+ * @see Zend_Db_Select
+ */
+require_once 'Zend/Db/Select.php';
+
+/**
+ * Class for connecting to SQL databases and performing common operations.
+ *
+ * @category   Zend
+ * @package    Zend_Db
+ * @subpackage Adapter
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+abstract class Zend_Db_Adapter_Abstract
+{
+
+    /**
+     * User-provided configuration
+     *
+     * @var array
+     */
+    protected $_config = array();
+
+    /**
+     * Fetch mode
+     *
+     * @var integer
+     */
+    protected $_fetchMode = Zend_Db::FETCH_ASSOC;
+
+    /**
+     * Query profiler object, of type Zend_Db_Profiler
+     * or a subclass of that.
+     *
+     * @var Zend_Db_Profiler
+     */
+    protected $_profiler;
+
+    /**
+     * Default class name for a DB statement.
+     *
+     * @var string
+     */
+    protected $_defaultStmtClass = 'Zend_Db_Statement';
+
+    /**
+     * Default class name for the profiler object.
+     *
+     * @var string
+     */
+    protected $_defaultProfilerClass = 'Zend_Db_Profiler';
+
+    /**
+     * Database connection
+     *
+     * @var object|resource|null
+     */
+    protected $_connection = null;
+
+    /**
+     * Specifies the case of column names retrieved in queries
+     * Options
+     * Zend_Db::CASE_NATURAL (default)
+     * Zend_Db::CASE_LOWER
+     * Zend_Db::CASE_UPPER
+     *
+     * @var integer
+     */
+    protected $_caseFolding = Zend_Db::CASE_NATURAL;
+
+    /**
+     * Specifies whether the adapter automatically quotes identifiers.
+     * If true, most SQL generated by Zend_Db classes applies
+     * identifier quoting automatically.
+     * If false, developer must quote identifiers themselves
+     * by calling quoteIdentifier().
+     *
+     * @var bool
+     */
+    protected $_autoQuoteIdentifiers = true;
+
+    /**
+     * Keys are UPPERCASE SQL datatypes or the constants
+     * Zend_Db::INT_TYPE, Zend_Db::BIGINT_TYPE, or Zend_Db::FLOAT_TYPE.
+     *
+     * Values are:
+     * 0 = 32-bit integer
+     * 1 = 64-bit integer
+     * 2 = float or decimal
+     *
+     * @var array Associative array of datatypes to values 0, 1, or 2.
+     */
+    protected $_numericDataTypes = array(
+        Zend_Db::INT_TYPE    => Zend_Db::INT_TYPE,
+        Zend_Db::BIGINT_TYPE => Zend_Db::BIGINT_TYPE,
+        Zend_Db::FLOAT_TYPE  => Zend_Db::FLOAT_TYPE
+    );
+
+    /** Weither or not that object can get serialized
+     *
+     * @var bool
+     */
+    protected $_allowSerialization = true;
+
+    /**
+     * Weither or not the database should be reconnected
+     * to that adapter when waking up
+     *
+     * @var bool
+     */
+    protected $_autoReconnectOnUnserialize = false;
+
+    /**
+     * Constructor.
+     *
+     * $config is an array of key/value pairs or an instance of Zend_Config
+     * containing configuration options.  These options are common to most adapters:
+     *
+     * dbname         => (string) The name of the database to user
+     * username       => (string) Connect to the database as this username.
+     * password       => (string) Password associated with the username.
+     * host           => (string) What host to connect to, defaults to localhost
+     *
+     * Some options are used on a case-by-case basis by adapters:
+     *
+     * port           => (string) The port of the database
+     * persistent     => (boolean) Whether to use a persistent connection or not, defaults to false
+     * protocol       => (string) The network protocol, defaults to TCPIP
+     * caseFolding    => (int) style of case-alteration used for identifiers
+     *
+     * @param  array|Zend_Config $config An array or instance of Zend_Config having configuration data
+     * @throws Zend_Db_Adapter_Exception
+     */
+    public function __construct($config)
+    {
+        /*
+         * Verify that adapter parameters are in an array.
+         */
+        if (!is_array($config)) {
+            /*
+             * Convert Zend_Config argument to a plain array.
+             */
+            if ($config instanceof Zend_Config) {
+                $config = $config->toArray();
+            } else {
+                /**
+                 * @see Zend_Db_Exception
+                 */
+                require_once 'Zend/Db/Exception.php';
+                throw new Zend_Db_Exception('Adapter parameters must be in an array or a Zend_Config object');
+            }
+        }
+
+        $this->_checkRequiredOptions($config);
+
+        $options = array(
+            Zend_Db::CASE_FOLDING           => $this->_caseFolding,
+            Zend_Db::AUTO_QUOTE_IDENTIFIERS => $this->_autoQuoteIdentifiers
+        );
+        $driverOptions = array();
+
+        /*
+         * normalize the config and merge it with the defaults
+         */
+        if (array_key_exists('options', $config)) {
+            // can't use array_merge() because keys might be integers
+            foreach ((array) $config['options'] as $key => $value) {
+                $options[$key] = $value;
+            }
+        }
+        if (array_key_exists('driver_options', $config)) {
+            if (!empty($config['driver_options'])) {
+                // can't use array_merge() because keys might be integers
+                foreach ((array) $config['driver_options'] as $key => $value) {
+                    $driverOptions[$key] = $value;
+                }
+            }
+        }
+
+        if (!isset($config['charset'])) {
+            $config['charset'] = null;
+        }
+        
+        if (!isset($config['persistent'])) {
+            $config['persistent'] = false;
+        }
+
+        $this->_config = array_merge($this->_config, $config);
+        $this->_config['options'] = $options;
+        $this->_config['driver_options'] = $driverOptions;
+
+
+        // obtain the case setting, if there is one
+        if (array_key_exists(Zend_Db::CASE_FOLDING, $options)) {
+            $case = (int) $options[Zend_Db::CASE_FOLDING];
+            switch ($case) {
+                case Zend_Db::CASE_LOWER:
+                case Zend_Db::CASE_UPPER:
+                case Zend_Db::CASE_NATURAL:
+                    $this->_caseFolding = $case;
+                    break;
+                default:
+                    /** @see Zend_Db_Adapter_Exception */
+                    require_once 'Zend/Db/Adapter/Exception.php';
+                    throw new Zend_Db_Adapter_Exception('Case must be one of the following constants: '
+                        . 'Zend_Db::CASE_NATURAL, Zend_Db::CASE_LOWER, Zend_Db::CASE_UPPER');
+            }
+        }
+
+        // obtain quoting property if there is one
+        if (array_key_exists(Zend_Db::AUTO_QUOTE_IDENTIFIERS, $options)) {
+            $this->_autoQuoteIdentifiers = (bool) $options[Zend_Db::AUTO_QUOTE_IDENTIFIERS];
+        }
+
+        // obtain allow serialization property if there is one
+        if (array_key_exists(Zend_Db::ALLOW_SERIALIZATION, $options)) {
+            $this->_allowSerialization = (bool) $options[Zend_Db::ALLOW_SERIALIZATION];
+        }
+
+        // obtain auto reconnect on unserialize property if there is one
+        if (array_key_exists(Zend_Db::AUTO_RECONNECT_ON_UNSERIALIZE, $options)) {
+            $this->_autoReconnectOnUnserialize = (bool) $options[Zend_Db::AUTO_RECONNECT_ON_UNSERIALIZE];
+        }
+
+        // create a profiler object
+        $profiler = false;
+        if (array_key_exists(Zend_Db::PROFILER, $this->_config)) {
+            $profiler = $this->_config[Zend_Db::PROFILER];
+            unset($this->_config[Zend_Db::PROFILER]);
+        }
+        $this->setProfiler($profiler);
+    }
+
+    /**
+     * Check for config options that are mandatory.
+     * Throw exceptions if any are missing.
+     *
+     * @param array $config
+     * @throws Zend_Db_Adapter_Exception
+     */
+    protected function _checkRequiredOptions(array $config)
+    {
+        // we need at least a dbname
+        if (! array_key_exists('dbname', $config)) {
+            /** @see Zend_Db_Adapter_Exception */
+            require_once 'Zend/Db/Adapter/Exception.php';
+            throw new Zend_Db_Adapter_Exception("Configuration array must have a key for 'dbname' that names the database instance");
+        }
+
+        if (! array_key_exists('password', $config)) {
+            /**
+             * @see Zend_Db_Adapter_Exception
+             */
+            require_once 'Zend/Db/Adapter/Exception.php';
+            throw new Zend_Db_Adapter_Exception("Configuration array must have a key for 'password' for login credentials");
+        }
+
+        if (! array_key_exists('username', $config)) {
+            /**
+             * @see Zend_Db_Adapter_Exception
+             */
+            require_once 'Zend/Db/Adapter/Exception.php';
+            throw new Zend_Db_Adapter_Exception("Configuration array must have a key for 'username' for login credentials");
+        }
+    }
+
+    /**
+     * Returns the underlying database connection object or resource.
+     * If not presently connected, this initiates the connection.
+     *
+     * @return object|resource|null
+     */
+    public function getConnection()
+    {
+        $this->_connect();
+        return $this->_connection;
+    }
+
+    /**
+     * Returns the configuration variables in this adapter.
+     *
+     * @return array
+     */
+    public function getConfig()
+    {
+        return $this->_config;
+    }
+
+    /**
+     * Set the adapter's profiler object.
+     *
+     * The argument may be a boolean, an associative array, an instance of
+     * Zend_Db_Profiler, or an instance of Zend_Config.
+     *
+     * A boolean argument sets the profiler to enabled if true, or disabled if
+     * false.  The profiler class is the adapter's default profiler class,
+     * Zend_Db_Profiler.
+     *
+     * An instance of Zend_Db_Profiler sets the adapter's instance to that
+     * object.  The profiler is enabled and disabled separately.
+     *
+     * An associative array argument may contain any of the keys 'enabled',
+     * 'class', and 'instance'. The 'enabled' and 'instance' keys correspond to the
+     * boolean and object types documented above. The 'class' key is used to name a
+     * class to use for a custom profiler. The class must be Zend_Db_Profiler or a
+     * subclass. The class is instantiated with no constructor arguments. The 'class'
+     * option is ignored when the 'instance' option is supplied.
+     *
+     * An object of type Zend_Config may contain the properties 'enabled', 'class', and
+     * 'instance', just as if an associative array had been passed instead.
+     *
+     * @param  Zend_Db_Profiler|Zend_Config|array|boolean $profiler
+     * @return Zend_Db_Adapter_Abstract Provides a fluent interface
+     * @throws Zend_Db_Profiler_Exception if the object instance or class specified
+     *         is not Zend_Db_Profiler or an extension of that class.
+     */
+    public function setProfiler($profiler)
+    {
+        $enabled          = null;
+        $profilerClass    = $this->_defaultProfilerClass;
+        $profilerInstance = null;
+
+        if ($profilerIsObject = is_object($profiler)) {
+            if ($profiler instanceof Zend_Db_Profiler) {
+                $profilerInstance = $profiler;
+            } else if ($profiler instanceof Zend_Config) {
+                $profiler = $profiler->toArray();
+            } else {
+                /**
+                 * @see Zend_Db_Profiler_Exception
+                 */
+                require_once 'Zend/Db/Profiler/Exception.php';
+                throw new Zend_Db_Profiler_Exception('Profiler argument must be an instance of either Zend_Db_Profiler'
+                    . ' or Zend_Config when provided as an object');
+            }
+        }
+
+        if (is_array($profiler)) {
+            if (isset($profiler['enabled'])) {
+                $enabled = (bool) $profiler['enabled'];
+            }
+            if (isset($profiler['class'])) {
+                $profilerClass = $profiler['class'];
+            }
+            if (isset($profiler['instance'])) {
+                $profilerInstance = $profiler['instance'];
+            }
+        } else if (!$profilerIsObject) {
+            $enabled = (bool) $profiler;
+        }
+
+        if ($profilerInstance === null) {
+            if (!class_exists($profilerClass)) {
+                require_once 'Zend/Loader.php';
+                Zend_Loader::loadClass($profilerClass);
+            }
+            $profilerInstance = new $profilerClass();
+        }
+
+        if (!$profilerInstance instanceof Zend_Db_Profiler) {
+            /** @see Zend_Db_Profiler_Exception */
+            require_once 'Zend/Db/Profiler/Exception.php';
+            throw new Zend_Db_Profiler_Exception('Class ' . get_class($profilerInstance) . ' does not extend '
+                . 'Zend_Db_Profiler');
+        }
+
+        if (null !== $enabled) {
+            $profilerInstance->setEnabled($enabled);
+        }
+
+        $this->_profiler = $profilerInstance;
+
+        return $this;
+    }
+
+
+    /**
+     * Returns the profiler for this adapter.
+     *
+     * @return Zend_Db_Profiler
+     */
+    public function getProfiler()
+    {
+        return $this->_profiler;
+    }
+
+    /**
+     * Get the default statement class.
+     *
+     * @return string
+     */
+    public function getStatementClass()
+    {
+        return $this->_defaultStmtClass;
+    }
+
+    /**
+     * Set the default statement class.
+     *
+     * @return Zend_Db_Adapter_Abstract Fluent interface
+     */
+    public function setStatementClass($class)
+    {
+        $this->_defaultStmtClass = $class;
+        return $this;
+    }
+
+    /**
+     * Prepares and executes an SQL statement with bound data.
+     *
+     * @param  mixed  $sql  The SQL statement with placeholders.
+     *                      May be a string or Zend_Db_Select.
+     * @param  mixed  $bind An array of data to bind to the placeholders.
+     * @return Zend_Db_Statement_Interface
+     */
+    public function query($sql, $bind = array())
+    {
+        // connect to the database if needed
+        $this->_connect();
+
+        // is the $sql a Zend_Db_Select object?
+        if ($sql instanceof Zend_Db_Select) {
+            if (empty($bind)) {
+                $bind = $sql->getBind();
+            }
+
+            $sql = $sql->assemble();
+        }
+
+        // make sure $bind to an array;
+        // don't use (array) typecasting because
+        // because $bind may be a Zend_Db_Expr object
+        if (!is_array($bind)) {
+            $bind = array($bind);
+        }
+
+        // prepare and execute the statement with profiling
+        $stmt = $this->prepare($sql);
+        $stmt->execute($bind);
+
+        // return the results embedded in the prepared statement object
+        $stmt->setFetchMode($this->_fetchMode);
+        return $stmt;
+    }
+
+    /**
+     * Leave autocommit mode and begin a transaction.
+     *
+     * @return Zend_Db_Adapter_Abstract
+     */
+    public function beginTransaction()
+    {
+        $this->_connect();
+        $q = $this->_profiler->queryStart('begin', Zend_Db_Profiler::TRANSACTION);
+        $this->_beginTransaction();
+        $this->_profiler->queryEnd($q);
+        return $this;
+    }
+
+    /**
+     * Commit a transaction and return to autocommit mode.
+     *
+     * @return Zend_Db_Adapter_Abstract
+     */
+    public function commit()
+    {
+        $this->_connect();
+        $q = $this->_profiler->queryStart('commit', Zend_Db_Profiler::TRANSACTION);
+        $this->_commit();
+        $this->_profiler->queryEnd($q);
+        return $this;
+    }
+
+    /**
+     * Roll back a transaction and return to autocommit mode.
+     *
+     * @return Zend_Db_Adapter_Abstract
+     */
+    public function rollBack()
+    {
+        $this->_connect();
+        $q = $this->_profiler->queryStart('rollback', Zend_Db_Profiler::TRANSACTION);
+        $this->_rollBack();
+        $this->_profiler->queryEnd($q);
+        return $this;
+    }
+
+    /**
+     * Inserts a table row with specified data.
+     *
+     * @param mixed $table The table to insert data into.
+     * @param array $bind Column-value pairs.
+     * @return int The number of affected rows.
+     */
+    public function insert($table, array $bind)
+    {
+        // extract and quote col names from the array keys
+        $cols = array();
+        $vals = array();
+        foreach ($bind as $col => $val) {
+            $cols[] = $this->quoteIdentifier($col, true);
+            if ($val instanceof Zend_Db_Expr) {
+                $vals[] = $val->__toString();
+                unset($bind[$col]);
+            } else {
+                $vals[] = '?';
+            }
+        }
+
+        // build the statement
+        $sql = "INSERT INTO "
+             . $this->quoteIdentifier($table, true)
+             . ' (' . implode(', ', $cols) . ') '
+             . 'VALUES (' . implode(', ', $vals) . ')';
+
+        // execute the statement and return the number of affected rows
+        $stmt = $this->query($sql, array_values($bind));
+        $result = $stmt->rowCount();
+        return $result;
+    }
+
+    /**
+     * Updates table rows with specified data based on a WHERE clause.
+     *
+     * @param  mixed        $table The table to update.
+     * @param  array        $bind  Column-value pairs.
+     * @param  mixed        $where UPDATE WHERE clause(s).
+     * @return int          The number of affected rows.
+     */
+    public function update($table, array $bind, $where = '')
+    {
+        /**
+         * Build "col = ?" pairs for the statement,
+         * except for Zend_Db_Expr which is treated literally.
+         */
+        $set = array();
+        $i = 0;
+        foreach ($bind as $col => $val) {
+            if ($val instanceof Zend_Db_Expr) {
+                $val = $val->__toString();
+                unset($bind[$col]);
+            } else {
+                if ($this->supportsParameters('positional')) {
+                    $val = '?';
+                } else {
+                    if ($this->supportsParameters('named')) {
+                        unset($bind[$col]);
+                        $bind[':'.$col.$i] = $val;
+                        $val = ':'.$col.$i;
+                        $i++;
+                    } else {
+                        /** @see Zend_Db_Adapter_Exception */
+                        require_once 'Zend/Db/Adapter/Exception.php';
+                        throw new Zend_Db_Adapter_Exception(get_class($this) ." doesn't support positional or named binding");
+                    }
+                }
+            }
+            $set[] = $this->quoteIdentifier($col, true) . ' = ' . $val;
+        }
+
+        $where = $this->_whereExpr($where);
+
+        /**
+         * Build the UPDATE statement
+         */
+        $sql = "UPDATE "
+             . $this->quoteIdentifier($table, true)
+             . ' SET ' . implode(', ', $set)
+             . (($where) ? " WHERE $where" : '');
+
+        /**
+         * Execute the statement and return the number of affected rows
+         */
+        if ($this->supportsParameters('positional')) {
+            $stmt = $this->query($sql, array_values($bind));
+        } else {
+            $stmt = $this->query($sql, $bind);
+        }
+        $result = $stmt->rowCount();
+        return $result;
+    }
+
+    /**
+     * Deletes table rows based on a WHERE clause.
+     *
+     * @param  mixed        $table The table to update.
+     * @param  mixed        $where DELETE WHERE clause(s).
+     * @return int          The number of affected rows.
+     */
+    public function delete($table, $where = '')
+    {
+        $where = $this->_whereExpr($where);
+
+        /**
+         * Build the DELETE statement
+         */
+        $sql = "DELETE FROM "
+             . $this->quoteIdentifier($table, true)
+             . (($where) ? " WHERE $where" : '');
+
+        /**
+         * Execute the statement and return the number of affected rows
+         */
+        $stmt = $this->query($sql);
+        $result = $stmt->rowCount();
+        return $result;
+    }
+
+    /**
+     * Convert an array, string, or Zend_Db_Expr object
+     * into a string to put in a WHERE clause.
+     *
+     * @param mixed $where
+     * @return string
+     */
+    protected function _whereExpr($where)
+    {
+        if (empty($where)) {
+            return $where;
+        }
+        if (!is_array($where)) {
+            $where = array($where);
+        }
+        foreach ($where as $cond => &$term) {
+            // is $cond an int? (i.e. Not a condition)
+            if (is_int($cond)) {
+                // $term is the full condition
+                if ($term instanceof Zend_Db_Expr) {
+                    $term = $term->__toString();
+                }
+            } else {
+                // $cond is the condition with placeholder,
+                // and $term is quoted into the condition
+                $term = $this->quoteInto($cond, $term);
+            }
+            $term = '(' . $term . ')';
+        }
+
+        $where = implode(' AND ', $where);
+        return $where;
+    }
+
+    /**
+     * Creates and returns a new Zend_Db_Select object for this adapter.
+     *
+     * @return Zend_Db_Select
+     */
+    public function select()
+    {
+        return new Zend_Db_Select($this);
+    }
+
+    /**
+     * Get the fetch mode.
+     *
+     * @return int
+     */
+    public function getFetchMode()
+    {
+        return $this->_fetchMode;
+    }
+
+    /**
+     * Fetches all SQL result rows as a sequential array.
+     * Uses the current fetchMode for the adapter.
+     *
+     * @param string|Zend_Db_Select $sql  An SQL SELECT statement.
+     * @param mixed                 $bind Data to bind into SELECT placeholders.
+     * @param mixed                 $fetchMode Override current fetch mode.
+     * @return array
+     */
+    public function fetchAll($sql, $bind = array(), $fetchMode = null)
+    {
+        if ($fetchMode === null) {
+            $fetchMode = $this->_fetchMode;
+        }
+        $stmt = $this->query($sql, $bind);
+        $result = $stmt->fetchAll($fetchMode);
+        return $result;
+    }
+
+    /**
+     * Fetches the first row of the SQL result.
+     * Uses the current fetchMode for the adapter.
+     *
+     * @param string|Zend_Db_Select $sql An SQL SELECT statement.
+     * @param mixed $bind Data to bind into SELECT placeholders.
+     * @param mixed                 $fetchMode Override current fetch mode.
+     * @return array
+     */
+    public function fetchRow($sql, $bind = array(), $fetchMode = null)
+    {
+        if ($fetchMode === null) {
+            $fetchMode = $this->_fetchMode;
+        }
+        $stmt = $this->query($sql, $bind);
+        $result = $stmt->fetch($fetchMode);
+        return $result;
+    }
+
+    /**
+     * Fetches all SQL result rows as an associative array.
+     *
+     * The first column is the key, the entire row array is the
+     * value.  You should construct the query to be sure that
+     * the first column contains unique values, or else
+     * rows with duplicate values in the first column will
+     * overwrite previous data.
+     *
+     * @param string|Zend_Db_Select $sql An SQL SELECT statement.
+     * @param mixed $bind Data to bind into SELECT placeholders.
+     * @return string
+     */
+    public function fetchAssoc($sql, $bind = array())
+    {
+        $stmt = $this->query($sql, $bind);
+        $data = array();
+        while ($row = $stmt->fetch(Zend_Db::FETCH_ASSOC)) {
+            $tmp = array_values(array_slice($row, 0, 1));
+            $data[$tmp[0]] = $row;
+        }
+        return $data;
+    }
+
+    /**
+     * Fetches the first column of all SQL result rows as an array.
+     *
+     * The first column in each row is used as the array key.
+     *
+     * @param string|Zend_Db_Select $sql An SQL SELECT statement.
+     * @param mixed $bind Data to bind into SELECT placeholders.
+     * @return array
+     */
+    public function fetchCol($sql, $bind = array())
+    {
+        $stmt = $this->query($sql, $bind);
+        $result = $stmt->fetchAll(Zend_Db::FETCH_COLUMN, 0);
+        return $result;
+    }
+
+    /**
+     * Fetches all SQL result rows as an array of key-value pairs.
+     *
+     * The first column is the key, the second column is the
+     * value.
+     *
+     * @param string|Zend_Db_Select $sql An SQL SELECT statement.
+     * @param mixed $bind Data to bind into SELECT placeholders.
+     * @return string
+     */
+    public function fetchPairs($sql, $bind = array())
+    {
+        $stmt = $this->query($sql, $bind);
+        $data = array();
+        while ($row = $stmt->fetch(Zend_Db::FETCH_NUM)) {
+            $data[$row[0]] = $row[1];
+        }
+        return $data;
+    }
+
+    /**
+     * Fetches the first column of the first row of the SQL result.
+     *
+     * @param string|Zend_Db_Select $sql An SQL SELECT statement.
+     * @param mixed $bind Data to bind into SELECT placeholders.
+     * @return string
+     */
+    public function fetchOne($sql, $bind = array())
+    {
+        $stmt = $this->query($sql, $bind);
+        $result = $stmt->fetchColumn(0);
+        return $result;
+    }
+
+    /**
+     * Quote a raw string.
+     *
+     * @param string $value     Raw string
+     * @return string           Quoted string
+     */
+    protected function _quote($value)
+    {
+        if (is_int($value)) {
+            return $value;
+        } elseif (is_float($value)) {
+            return sprintf('%F', $value);
+        }
+        return "'" . addcslashes($value, "\000\n\r\\'\"\032") . "'";
+    }
+
+    /**
+     * Safely quotes a value for an SQL statement.
+     *
+     * If an array is passed as the value, the array values are quoted
+     * and then returned as a comma-separated string.
+     *
+     * @param mixed $value The value to quote.
+     * @param mixed $type  OPTIONAL the SQL datatype name, or constant, or null.
+     * @return mixed An SQL-safe quoted value (or string of separated values).
+     */
+    public function quote($value, $type = null)
+    {
+        $this->_connect();
+
+        if ($value instanceof Zend_Db_Select) {
+            return '(' . $value->assemble() . ')';
+        }
+
+        if ($value instanceof Zend_Db_Expr) {
+            return $value->__toString();
+        }
+
+        if (is_array($value)) {
+            foreach ($value as &$val) {
+                $val = $this->quote($val, $type);
+            }
+            return implode(', ', $value);
+        }
+
+        if ($type !== null && array_key_exists($type = strtoupper($type), $this->_numericDataTypes)) {
+            $quotedValue = '0';
+            switch ($this->_numericDataTypes[$type]) {
+                case Zend_Db::INT_TYPE: // 32-bit integer
+                    $quotedValue = (string) intval($value);
+                    break;
+                case Zend_Db::BIGINT_TYPE: // 64-bit integer
+                    // ANSI SQL-style hex literals (e.g. x'[\dA-F]+')
+                    // are not supported here, because these are string
+                    // literals, not numeric literals.
+                    if (preg_match('/^(
+                          [+-]?                  # optional sign
+                          (?:
+                            0[Xx][\da-fA-F]+     # ODBC-style hexadecimal
+                            |\d+                 # decimal or octal, or MySQL ZEROFILL decimal
+                            (?:[eE][+-]?\d+)?    # optional exponent on decimals or octals
+                          )
+                        )/x',
+                        (string) $value, $matches)) {
+                        $quotedValue = $matches[1];
+                    }
+                    break;
+                case Zend_Db::FLOAT_TYPE: // float or decimal
+                    $quotedValue = sprintf('%F', $value);
+            }
+            return $quotedValue;
+        }
+
+        return $this->_quote($value);
+    }
+
+    /**
+     * Quotes a value and places into a piece of text at a placeholder.
+     *
+     * The placeholder is a question-mark; all placeholders will be replaced
+     * with the quoted value.   For example:
+     *
+     * <code>
+     * $text = "WHERE date < ?";
+     * $date = "2005-01-02";
+     * $safe = $sql->quoteInto($text, $date);
+     * // $safe = "WHERE date < '2005-01-02'"
+     * </code>
+     *
+     * @param string  $text  The text with a placeholder.
+     * @param mixed   $value The value to quote.
+     * @param string  $type  OPTIONAL SQL datatype
+     * @param integer $count OPTIONAL count of placeholders to replace
+     * @return string An SQL-safe quoted value placed into the original text.
+     */
+    public function quoteInto($text, $value, $type = null, $count = null)
+    {
+        if ($count === null) {
+            return str_replace('?', $this->quote($value, $type), $text);
+        } else {
+            while ($count > 0) {
+                if (strpos($text, '?') != false) {
+                    $text = substr_replace($text, $this->quote($value, $type), strpos($text, '?'), 1);
+                }
+                --$count;
+            }
+            return $text;
+        }
+    }
+
+    /**
+     * Quotes an identifier.
+     *
+     * Accepts a string representing a qualified indentifier. For Example:
+     * <code>
+     * $adapter->quoteIdentifier('myschema.mytable')
+     * </code>
+     * Returns: "myschema"."mytable"
+     *
+     * Or, an array of one or more identifiers that may form a qualified identifier:
+     * <code>
+     * $adapter->quoteIdentifier(array('myschema','my.table'))
+     * </code>
+     * Returns: "myschema"."my.table"
+     *
+     * The actual quote character surrounding the identifiers may vary depending on
+     * the adapter.
+     *
+     * @param string|array|Zend_Db_Expr $ident The identifier.
+     * @param boolean $auto If true, heed the AUTO_QUOTE_IDENTIFIERS config option.
+     * @return string The quoted identifier.
+     */
+    public function quoteIdentifier($ident, $auto=false)
+    {
+        return $this->_quoteIdentifierAs($ident, null, $auto);
+    }
+
+    /**
+     * Quote a column identifier and alias.
+     *
+     * @param string|array|Zend_Db_Expr $ident The identifier or expression.
+     * @param string $alias An alias for the column.
+     * @param boolean $auto If true, heed the AUTO_QUOTE_IDENTIFIERS config option.
+     * @return string The quoted identifier and alias.
+     */
+    public function quoteColumnAs($ident, $alias, $auto=false)
+    {
+        return $this->_quoteIdentifierAs($ident, $alias, $auto);
+    }
+
+    /**
+     * Quote a table identifier and alias.
+     *
+     * @param string|array|Zend_Db_Expr $ident The identifier or expression.
+     * @param string $alias An alias for the table.
+     * @param boolean $auto If true, heed the AUTO_QUOTE_IDENTIFIERS config option.
+     * @return string The quoted identifier and alias.
+     */
+    public function quoteTableAs($ident, $alias = null, $auto = false)
+    {
+        return $this->_quoteIdentifierAs($ident, $alias, $auto);
+    }
+
+    /**
+     * Quote an identifier and an optional alias.
+     *
+     * @param string|array|Zend_Db_Expr $ident The identifier or expression.
+     * @param string $alias An optional alias.
+     * @param boolean $auto If true, heed the AUTO_QUOTE_IDENTIFIERS config option.
+     * @param string $as The string to add between the identifier/expression and the alias.
+     * @return string The quoted identifier and alias.
+     */
+    protected function _quoteIdentifierAs($ident, $alias = null, $auto = false, $as = ' AS ')
+    {
+        if ($ident instanceof Zend_Db_Expr) {
+            $quoted = $ident->__toString();
+        } elseif ($ident instanceof Zend_Db_Select) {
+            $quoted = '(' . $ident->assemble() . ')';
+        } else {
+            if (is_string($ident)) {
+                $ident = explode('.', $ident);
+            }
+            if (is_array($ident)) {
+                $segments = array();
+                foreach ($ident as $segment) {
+                    if ($segment instanceof Zend_Db_Expr) {
+                        $segments[] = $segment->__toString();
+                    } else {
+                        $segments[] = $this->_quoteIdentifier($segment, $auto);
+                    }
+                }
+                if ($alias !== null && end($ident) == $alias) {
+                    $alias = null;
+                }
+                $quoted = implode('.', $segments);
+            } else {
+                $quoted = $this->_quoteIdentifier($ident, $auto);
+            }
+        }
+        if ($alias !== null) {
+            $quoted .= $as . $this->_quoteIdentifier($alias, $auto);
+        }
+        return $quoted;
+    }
+
+    /**
+     * Quote an identifier.
+     *
+     * @param  string $value The identifier or expression.
+     * @param boolean $auto If true, heed the AUTO_QUOTE_IDENTIFIERS config option.
+     * @return string        The quoted identifier and alias.
+     */
+    protected function _quoteIdentifier($value, $auto=false)
+    {
+        if ($auto === false || $this->_autoQuoteIdentifiers === true) {
+            $q = $this->getQuoteIdentifierSymbol();
+            return ($q . str_replace("$q", "$q$q", $value) . $q);
+        }
+        return $value;
+    }
+
+    /**
+     * Returns the symbol the adapter uses for delimited identifiers.
+     *
+     * @return string
+     */
+    public function getQuoteIdentifierSymbol()
+    {
+        return '"';
+    }
+
+    /**
+     * Return the most recent value from the specified sequence in the database.
+     * This is supported only on RDBMS brands that support sequences
+     * (e.g. Oracle, PostgreSQL, DB2).  Other RDBMS brands return null.
+     *
+     * @param string $sequenceName
+     * @return string
+     */
+    public function lastSequenceId($sequenceName)
+    {
+        return null;
+    }
+
+    /**
+     * Generate a new value from the specified sequence in the database, and return it.
+     * This is supported only on RDBMS brands that support sequences
+     * (e.g. Oracle, PostgreSQL, DB2).  Other RDBMS brands return null.
+     *
+     * @param string $sequenceName
+     * @return string
+     */
+    public function nextSequenceId($sequenceName)
+    {
+        return null;
+    }
+
+    /**
+     * Helper method to change the case of the strings used
+     * when returning result sets in FETCH_ASSOC and FETCH_BOTH
+     * modes.
+     *
+     * This is not intended to be used by application code,
+     * but the method must be public so the Statement class
+     * can invoke it.
+     *
+     * @param string $key
+     * @return string
+     */
+    public function foldCase($key)
+    {
+        switch ($this->_caseFolding) {
+            case Zend_Db::CASE_LOWER:
+                $value = strtolower((string) $key);
+                break;
+            case Zend_Db::CASE_UPPER:
+                $value = strtoupper((string) $key);
+                break;
+            case Zend_Db::CASE_NATURAL:
+            default:
+                $value = (string) $key;
+        }
+        return $value;
+    }
+
+    /**
+     * called when object is getting serialized
+     * This disconnects the DB object that cant be serialized
+     *
+     * @throws Zend_Db_Adapter_Exception
+     * @return array
+     */
+    public function __sleep()
+    {
+        if ($this->_allowSerialization == false) {
+            /** @see Zend_Db_Adapter_Exception */
+            require_once 'Zend/Db/Adapter/Exception.php';
+            throw new Zend_Db_Adapter_Exception(get_class($this) ." is not allowed to be serialized");
+        }
+        $this->_connection = false;
+        return array_keys(array_diff_key(get_object_vars($this), array('_connection'=>false)));
+    }
+
+    /**
+     * called when object is getting unserialized
+     *
+     * @return void
+     */
+    public function __wakeup()
+    {
+        if ($this->_autoReconnectOnUnserialize == true) {
+            $this->getConnection();
+        }
+    }
+
+    /**
+     * Abstract Methods
+     */
+
+    /**
+     * Returns a list of the tables in the database.
+     *
+     * @return array
+     */
+    abstract public function listTables();
+
+    /**
+     * Returns the column descriptions for a table.
+     *
+     * The return value is an associative array keyed by the column name,
+     * as returned by the RDBMS.
+     *
+     * The value of each array element is an associative array
+     * with the following keys:
+     *
+     * SCHEMA_NAME => string; name of database or schema
+     * TABLE_NAME  => string;
+     * COLUMN_NAME => string; column name
+     * COLUMN_POSITION => number; ordinal position of column in table
+     * DATA_TYPE   => string; SQL datatype name of column
+     * DEFAULT     => string; default expression of column, null if none
+     * NULLABLE    => boolean; true if column can have nulls
+     * LENGTH      => number; length of CHAR/VARCHAR
+     * SCALE       => number; scale of NUMERIC/DECIMAL
+     * PRECISION   => number; precision of NUMERIC/DECIMAL
+     * UNSIGNED    => boolean; unsigned property of an integer type
+     * PRIMARY     => boolean; true if column is part of the primary key
+     * PRIMARY_POSITION => integer; position of column in primary key
+     *
+     * @param string $tableName
+     * @param string $schemaName OPTIONAL
+     * @return array
+     */
+    abstract public function describeTable($tableName, $schemaName = null);
+
+    /**
+     * Creates a connection to the database.
+     *
+     * @return void
+     */
+    abstract protected function _connect();
+
+    /**
+     * Test if a connection is active
+     *
+     * @return boolean
+     */
+    abstract public function isConnected();
+
+    /**
+     * Force the connection to close.
+     *
+     * @return void
+     */
+    abstract public function closeConnection();
+
+    /**
+     * Prepare a statement and return a PDOStatement-like object.
+     *
+     * @param string|Zend_Db_Select $sql SQL query
+     * @return Zend_Db_Statement|PDOStatement
+     */
+    abstract public function prepare($sql);
+
+    /**
+     * Gets the last ID generated automatically by an IDENTITY/AUTOINCREMENT column.
+     *
+     * As a convention, on RDBMS brands that support sequences
+     * (e.g. Oracle, PostgreSQL, DB2), this method forms the name of a sequence
+     * from the arguments and returns the last id generated by that sequence.
+     * On RDBMS brands that support IDENTITY/AUTOINCREMENT columns, this method
+     * returns the last value generated for such a column, and the table name
+     * argument is disregarded.
+     *
+     * @param string $tableName   OPTIONAL Name of table.
+     * @param string $primaryKey  OPTIONAL Name of primary key column.
+     * @return string
+     */
+    abstract public function lastInsertId($tableName = null, $primaryKey = null);
+
+    /**
+     * Begin a transaction.
+     */
+    abstract protected function _beginTransaction();
+
+    /**
+     * Commit a transaction.
+     */
+    abstract protected function _commit();
+
+    /**
+     * Roll-back a transaction.
+     */
+    abstract protected function _rollBack();
+
+    /**
+     * Set the fetch mode.
+     *
+     * @param integer $mode
+     * @return void
+     * @throws Zend_Db_Adapter_Exception
+     */
+    abstract public function setFetchMode($mode);
+
+    /**
+     * Adds an adapter-specific LIMIT clause to the SELECT statement.
+     *
+     * @param mixed $sql
+     * @param integer $count
+     * @param integer $offset
+     * @return string
+     */
+    abstract public function limit($sql, $count, $offset = 0);
+
+    /**
+     * Check if the adapter supports real SQL parameters.
+     *
+     * @param string $type 'positional' or 'named'
+     * @return bool
+     */
+    abstract public function supportsParameters($type);
+
+    /**
+     * Retrieve server version in PHP style
+     *
+     * @return string
+     */
+    abstract public function getServerVersion();
+}
diff --git a/lib/Zend/Db/Adapter/Db2.php b/lib/Zend/Db/Adapter/Db2.php
new file mode 100644 (file)
index 0000000..1ba4446
--- /dev/null
@@ -0,0 +1,832 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Db
+ * @subpackage Adapter
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Db2.php 10020 2009-08-18 14:34:09Z j.fischer@metaways.de $
+ *
+ */
+
+/**
+ * @see Zend_Db
+ */
+require_once 'Zend/Db.php';
+
+/**
+ * @see Zend_Db_Adapter_Abstract
+ */
+require_once 'Zend/Db/Adapter/Abstract.php';
+
+/**
+ * @see Zend_Db_Statement_Db2
+ */
+require_once 'Zend/Db/Statement/Db2.php';
+
+
+/**
+ * @package    Zend_Db
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+class Zend_Db_Adapter_Db2 extends Zend_Db_Adapter_Abstract
+{
+    /**
+     * User-provided configuration.
+     *
+     * Basic keys are:
+     *
+     * username   => (string)  Connect to the database as this username.
+     * password   => (string)  Password associated with the username.
+     * host       => (string)  What host to connect to (default 127.0.0.1)
+     * dbname     => (string)  The name of the database to user
+     * protocol   => (string)  Protocol to use, defaults to "TCPIP"
+     * port       => (integer) Port number to use for TCP/IP if protocol is "TCPIP"
+     * persistent => (boolean) Set TRUE to use a persistent connection (db2_pconnect)
+     * os         => (string)  This should be set to 'i5' if the db is on an os400/i5
+     * schema     => (string)  The default schema the connection should use
+     *
+     * @var array
+     */
+    protected $_config = array(
+        'dbname'       => null,
+        'username'     => null,
+        'password'     => null,
+        'host'         => 'localhost',
+        'port'         => '50000',
+        'protocol'     => 'TCPIP',
+        'persistent'   => false,
+        'os'           => null,
+        'schema'       => null
+    );
+
+    /**
+     * Execution mode
+     *
+     * @var int execution flag (DB2_AUTOCOMMIT_ON or DB2_AUTOCOMMIT_OFF)
+     */
+    protected $_execute_mode = DB2_AUTOCOMMIT_ON;
+
+    /**
+     * Default class name for a DB statement.
+     *
+     * @var string
+     */
+    protected $_defaultStmtClass = 'Zend_Db_Statement_Db2';
+    protected $_isI5 = false;
+
+    /**
+     * Keys are UPPERCASE SQL datatypes or the constants
+     * Zend_Db::INT_TYPE, Zend_Db::BIGINT_TYPE, or Zend_Db::FLOAT_TYPE.
+     *
+     * Values are:
+     * 0 = 32-bit integer
+     * 1 = 64-bit integer
+     * 2 = float or decimal
+     *
+     * @var array Associative array of datatypes to values 0, 1, or 2.
+     */
+    protected $_numericDataTypes = array(
+        Zend_Db::INT_TYPE    => Zend_Db::INT_TYPE,
+        Zend_Db::BIGINT_TYPE => Zend_Db::BIGINT_TYPE,
+        Zend_Db::FLOAT_TYPE  => Zend_Db::FLOAT_TYPE,
+        'INTEGER'            => Zend_Db::INT_TYPE,
+        'SMALLINT'           => Zend_Db::INT_TYPE,
+        'BIGINT'             => Zend_Db::BIGINT_TYPE,
+        'DECIMAL'            => Zend_Db::FLOAT_TYPE,
+        'NUMERIC'            => Zend_Db::FLOAT_TYPE
+    );
+
+    /**
+     * Creates a connection resource.
+     *
+     * @return void
+     */
+    protected function _connect()
+    {
+        if (is_resource($this->_connection)) {
+            // connection already exists
+            return;
+        }
+
+        if (!extension_loaded('ibm_db2')) {
+            /**
+             * @see Zend_Db_Adapter_Db2_Exception
+             */
+            require_once 'Zend/Db/Adapter/Db2/Exception.php';
+            throw new Zend_Db_Adapter_Db2_Exception('The IBM DB2 extension is required for this adapter but the extension is not loaded');
+        }
+
+        $this->_determineI5();
+        if ($this->_config['persistent']) {
+            // use persistent connection
+            $conn_func_name = 'db2_pconnect';
+        } else {
+            // use "normal" connection
+            $conn_func_name = 'db2_connect';
+        }
+
+        if (!isset($this->_config['driver_options']['autocommit'])) {
+            // set execution mode
+            $this->_config['driver_options']['autocommit'] = &$this->_execute_mode;
+        }
+
+        if (isset($this->_config['options'][Zend_Db::CASE_FOLDING])) {
+            $caseAttrMap = array(
+                Zend_Db::CASE_NATURAL => DB2_CASE_NATURAL,
+                Zend_Db::CASE_UPPER   => DB2_CASE_UPPER,
+                Zend_Db::CASE_LOWER   => DB2_CASE_LOWER
+            );
+            $this->_config['driver_options']['DB2_ATTR_CASE'] = $caseAttrMap[$this->_config['options'][Zend_Db::CASE_FOLDING]];
+        }
+
+        if ($this->_config['host'] !== 'localhost' && !$this->_isI5) {
+            // if the host isn't localhost, use extended connection params
+            $dbname = 'DRIVER={IBM DB2 ODBC DRIVER}' .
+                     ';DATABASE=' . $this->_config['dbname'] .
+                     ';HOSTNAME=' . $this->_config['host'] .
+                     ';PORT='     . $this->_config['port'] .
+                     ';PROTOCOL=' . $this->_config['protocol'] .
+                     ';UID='      . $this->_config['username'] .
+                     ';PWD='      . $this->_config['password'] .';';
+            $this->_connection = $conn_func_name(
+                $dbname,
+                null,
+                null,
+                $this->_config['driver_options']
+            );
+        } else {
+            // host is localhost, so use standard connection params
+            $this->_connection = $conn_func_name(
+                $this->_config['dbname'],
+                $this->_config['username'],
+                $this->_config['password'],
+                $this->_config['driver_options']
+            );
+        }
+
+        // check the connection
+        if (!$this->_connection) {
+            /**
+             * @see Zend_Db_Adapter_Db2_Exception
+             */
+            require_once 'Zend/Db/Adapter/Db2/Exception.php';
+            throw new Zend_Db_Adapter_Db2_Exception(db2_conn_errormsg(), db2_conn_error());
+        }
+    }
+
+    /**
+     * Test if a connection is active
+     *
+     * @return boolean
+     */
+    public function isConnected()
+    {
+        return ((bool) (is_resource($this->_connection)
+                     && get_resource_type($this->_connection) == 'DB2 Connection'));
+    }
+
+    /**
+     * Force the connection to close.
+     *
+     * @return void
+     */
+    public function closeConnection()
+    {
+        if ($this->isConnected()) {
+            db2_close($this->_connection);
+        }
+        $this->_connection = null;
+    }
+
+    /**
+     * Returns an SQL statement for preparation.
+     *
+     * @param string $sql The SQL statement with placeholders.
+     * @return Zend_Db_Statement_Db2
+     */
+    public function prepare($sql)
+    {
+        $this->_connect();
+        $stmtClass = $this->_defaultStmtClass;
+        if (!class_exists($stmtClass)) {
+            require_once 'Zend/Loader.php';
+            Zend_Loader::loadClass($stmtClass);
+        }
+        $stmt = new $stmtClass($this, $sql);
+        $stmt->setFetchMode($this->_fetchMode);
+        return $stmt;
+    }
+
+    /**
+     * Gets the execution mode
+     *
+     * @return int the execution mode (DB2_AUTOCOMMIT_ON or DB2_AUTOCOMMIT_OFF)
+     */
+    public function _getExecuteMode()
+    {
+        return $this->_execute_mode;
+    }
+
+    /**
+     * @param integer $mode
+     * @return void
+     */
+    public function _setExecuteMode($mode)
+    {
+        switch ($mode) {
+            case DB2_AUTOCOMMIT_OFF:
+            case DB2_AUTOCOMMIT_ON:
+                $this->_execute_mode = $mode;
+                db2_autocommit($this->_connection, $mode);
+                break;
+            default:
+                /**
+                 * @see Zend_Db_Adapter_Db2_Exception
+                 */
+                require_once 'Zend/Db/Adapter/Db2/Exception.php';
+                throw new Zend_Db_Adapter_Db2_Exception("execution mode not supported");
+                break;
+        }
+    }
+
+    /**
+     * Quote a raw string.
+     *
+     * @param string $value     Raw string
+     * @return string           Quoted string
+     */
+    protected function _quote($value)
+    {
+        if (is_int($value) || is_float($value)) {
+            return $value;
+        }
+        /**
+         * Use db2_escape_string() if it is present in the IBM DB2 extension.
+         * But some supported versions of PHP do not include this function,
+         * so fall back to default quoting in the parent class.
+         */
+        if (function_exists('db2_escape_string')) {
+            return "'" . db2_escape_string($value) . "'";
+        }
+        return parent::_quote($value);
+    }
+
+    /**
+     * @return string
+     */
+    public function getQuoteIdentifierSymbol()
+    {
+        $this->_connect();
+        $info = db2_server_info($this->_connection);
+        if ($info) {
+            $identQuote = $info->IDENTIFIER_QUOTE_CHAR;
+        } else {
+            // db2_server_info() does not return result on some i5 OS version
+            if ($this->_isI5) {
+                $identQuote ="'";
+            }
+        }
+        return $identQuote;
+    }
+
+    /**
+     * Returns a list of the tables in the database.
+     * @param string $schema OPTIONAL
+     * @return array
+     */
+    public function listTables($schema = null)
+    {
+        $this->_connect();
+
+        if ($schema === null && $this->_config['schema'] != null) {
+            $schema = $this->_config['schema'];
+        }
+
+        $tables = array();
+
+        if (!$this->_isI5) {
+            if ($schema) {
+                $stmt = db2_tables($this->_connection, null, $schema);
+            } else {
+                $stmt = db2_tables($this->_connection);
+            }
+            while ($row = db2_fetch_assoc($stmt)) {
+                $tables[] = $row['TABLE_NAME'];
+            }
+        } else {
+            $tables = $this->_i5listTables($schema);
+        }
+
+        return $tables;
+    }
+
+
+    /**
+     * Returns the column descriptions for a table.
+     *
+     * The return value is an associative array keyed by the column name,
+     * as returned by the RDBMS.
+     *
+     * The value of each array element is an associative array
+     * with the following keys:
+     *
+     * SCHEMA_NAME      => string; name of database or schema
+     * TABLE_NAME       => string;
+     * COLUMN_NAME      => string; column name
+     * COLUMN_POSITION  => number; ordinal position of column in table
+     * DATA_TYPE        => string; SQL datatype name of column
+     * DEFAULT          => string; default expression of column, null if none
+     * NULLABLE         => boolean; true if column can have nulls
+     * LENGTH           => number; length of CHAR/VARCHAR
+     * SCALE            => number; scale of NUMERIC/DECIMAL
+     * PRECISION        => number; precision of NUMERIC/DECIMAL
+     * UNSIGNED         => boolean; unsigned property of an integer type
+     *                     DB2 not supports UNSIGNED integer.
+     * PRIMARY          => boolean; true if column is part of the primary key
+     * PRIMARY_POSITION => integer; position of column in primary key
+     * IDENTITY         => integer; true if column is auto-generated with unique values
+     *
+     * @param string $tableName
+     * @param string $schemaName OPTIONAL
+     * @return array
+     */
+    public function describeTable($tableName, $schemaName = null)
+    {
+        // Ensure the connection is made so that _isI5 is set
+        $this->_connect();
+
+        if ($schemaName === null && $this->_config['schema'] != null) {
+            $schemaName = $this->_config['schema'];
+        }
+        
+        if (!$this->_isI5) {
+
+            $sql = "SELECT DISTINCT c.tabschema, c.tabname, c.colname, c.colno,
+                c.typename, c.default, c.nulls, c.length, c.scale,
+                c.identity, tc.type AS tabconsttype, k.colseq
+                FROM syscat.columns c
+                LEFT JOIN (syscat.keycoluse k JOIN syscat.tabconst tc
+                ON (k.tabschema = tc.tabschema
+                    AND k.tabname = tc.tabname
+                    AND tc.type = 'P'))
+                ON (c.tabschema = k.tabschema
+                    AND c.tabname = k.tabname
+                    AND c.colname = k.colname)
+                WHERE "
+                . $this->quoteInto('UPPER(c.tabname) = UPPER(?)', $tableName);
+
+            if ($schemaName) {
+               $sql .= $this->quoteInto(' AND UPPER(c.tabschema) = UPPER(?)', $schemaName);
+            }
+
+            $sql .= " ORDER BY c.colno";
+
+        } else {
+
+            // DB2 On I5 specific query
+            $sql = "SELECT DISTINCT C.TABLE_SCHEMA, C.TABLE_NAME, C.COLUMN_NAME, C.ORDINAL_POSITION,
+                C.DATA_TYPE, C.COLUMN_DEFAULT, C.NULLS ,C.LENGTH, C.SCALE, LEFT(C.IDENTITY,1),
+                LEFT(tc.TYPE, 1) AS tabconsttype, k.COLSEQ
+                FROM QSYS2.SYSCOLUMNS C     
+                LEFT JOIN (QSYS2.syskeycst k JOIN QSYS2.SYSCST tc
+                    ON (k.TABLE_SCHEMA = tc.TABLE_SCHEMA
+                      AND k.TABLE_NAME = tc.TABLE_NAME
+                      AND LEFT(tc.type,1) = 'P'))                  
+                    ON (C.TABLE_SCHEMA = k.TABLE_SCHEMA
+                       AND C.TABLE_NAME = k.TABLE_NAME
+                       AND C.COLUMN_NAME = k.COLUMN_NAME)                          
+                WHERE "
+                 . $this->quoteInto('UPPER(C.TABLE_NAME) = UPPER(?)', $tableName);
+
+            if ($schemaName) {
+                $sql .= $this->quoteInto(' AND UPPER(C.TABLE_SCHEMA) = UPPER(?)', $schemaName);
+            }
+
+            $sql .= " ORDER BY C.ORDINAL_POSITION FOR FETCH ONLY";
+        }
+
+        $desc = array();
+        $stmt = $this->query($sql);
+
+        /**
+         * To avoid case issues, fetch using FETCH_NUM
+         */
+        $result = $stmt->fetchAll(Zend_Db::FETCH_NUM);
+
+        /**
+         * The ordering of columns is defined by the query so we can map
+         * to variables to improve readability
+         */
+        $tabschema      = 0;
+        $tabname        = 1;
+        $colname        = 2;
+        $colno          = 3;
+        $typename       = 4;
+        $default        = 5;
+        $nulls          = 6;
+        $length         = 7;
+        $scale          = 8;
+        $identityCol    = 9;
+        $tabconstType   = 10;
+        $colseq         = 11;
+
+        foreach ($result as $key => $row) {
+            list ($primary, $primaryPosition, $identity) = array(false, null, false);
+            if ($row[$tabconstType] == 'P') {
+                $primary = true;
+                $primaryPosition = $row[$colseq];
+            }
+            /**
+             * In IBM DB2, an column can be IDENTITY
+             * even if it is not part of the PRIMARY KEY.
+             */
+            if ($row[$identityCol] == 'Y') {
+                $identity = true;
+            }
+
+            // only colname needs to be case adjusted
+            $desc[$this->foldCase($row[$colname])] = array(
+                'SCHEMA_NAME'      => $this->foldCase($row[$tabschema]),
+                'TABLE_NAME'       => $this->foldCase($row[$tabname]),
+                'COLUMN_NAME'      => $this->foldCase($row[$colname]),
+                'COLUMN_POSITION'  => (!$this->_isI5) ? $row[$colno]+1 : $row[$colno],
+                'DATA_TYPE'        => $row[$typename],
+                'DEFAULT'          => $row[$default],
+                'NULLABLE'         => (bool) ($row[$nulls] == 'Y'),
+                'LENGTH'           => $row[$length],
+                'SCALE'            => $row[$scale],
+                'PRECISION'        => ($row[$typename] == 'DECIMAL' ? $row[$length] : 0),
+                'UNSIGNED'         => false,
+                'PRIMARY'          => $primary,
+                'PRIMARY_POSITION' => $primaryPosition,
+                'IDENTITY'         => $identity
+            );
+        }
+
+        return $desc;
+    }
+
+    /**
+     * Return the most recent value from the specified sequence in the database.
+     * This is supported only on RDBMS brands that support sequences
+     * (e.g. Oracle, PostgreSQL, DB2).  Other RDBMS brands return null.
+     *
+     * @param string $sequenceName
+     * @return string
+     */
+    public function lastSequenceId($sequenceName)
+    {
+        $this->_connect();
+
+        if (!$this->_isI5) {
+            $quotedSequenceName = $this->quoteIdentifier($sequenceName, true);
+            $sql = 'SELECT PREVVAL FOR ' . $quotedSequenceName . ' AS VAL FROM SYSIBM.SYSDUMMY1';
+        } else {
+            $quotedSequenceName = $sequenceName;
+            $sql = 'SELECT PREVVAL FOR ' . $this->quoteIdentifier($sequenceName, true) . ' AS VAL FROM QSYS2.QSQPTABL';
+        }
+
+        $value = $this->fetchOne($sql);
+        return (string) $value;
+    }
+
+    /**
+     * Generate a new value from the specified sequence in the database, and return it.
+     * This is supported only on RDBMS brands that support sequences
+     * (e.g. Oracle, PostgreSQL, DB2).  Other RDBMS brands return null.
+     *
+     * @param string $sequenceName
+     * @return string
+     */
+    public function nextSequenceId($sequenceName)
+    {
+        $this->_connect();
+        $sql = 'SELECT NEXTVAL FOR '.$this->quoteIdentifier($sequenceName, true).' AS VAL FROM SYSIBM.SYSDUMMY1';
+        $value = $this->fetchOne($sql);
+        return (string) $value;
+    }
+
+    /**
+     * Gets the last ID generated automatically by an IDENTITY/AUTOINCREMENT column.
+     *
+     * As a convention, on RDBMS brands that support sequences
+     * (e.g. Oracle, PostgreSQL, DB2), this method forms the name of a sequence
+     * from the arguments and returns the last id generated by that sequence.
+     * On RDBMS brands that support IDENTITY/AUTOINCREMENT columns, this method
+     * returns the last value generated for such a column, and the table name
+     * argument is disregarded.
+     *
+     * The IDENTITY_VAL_LOCAL() function gives the last generated identity value
+     * in the current process, even if it was for a GENERATED column.
+     *
+     * @param string $tableName OPTIONAL
+     * @param string $primaryKey OPTIONAL
+     * @param string $idType OPTIONAL used for i5 platform to define sequence/idenity unique value
+     * @return string
+     */
+
+    public function lastInsertId($tableName = null, $primaryKey = null, $idType = null)
+    {
+        $this->_connect();
+
+        if ($this->_isI5) {
+            return (string) $this->_i5LastInsertId($tableName, $idType);
+        }
+
+        if ($tableName !== null) {
+            $sequenceName = $tableName;
+            if ($primaryKey) {
+                $sequenceName .= "_$primaryKey";
+            }
+            $sequenceName .= '_seq';
+            return $this->lastSequenceId($sequenceName);
+        }
+
+        $sql = 'SELECT IDENTITY_VAL_LOCAL() AS VAL FROM SYSIBM.SYSDUMMY1';
+        $value = $this->fetchOne($sql);
+        return (string) $value;
+    }
+
+    /**
+     * Begin a transaction.
+     *
+     * @return void
+     */
+    protected function _beginTransaction()
+    {
+        $this->_setExecuteMode(DB2_AUTOCOMMIT_OFF);
+    }
+
+    /**
+     * Commit a transaction.
+     *
+     * @return void
+     */
+    protected function _commit()
+    {
+        if (!db2_commit($this->_connection)) {
+            /**
+             * @see Zend_Db_Adapter_Db2_Exception
+             */
+            require_once 'Zend/Db/Adapter/Db2/Exception.php';
+            throw new Zend_Db_Adapter_Db2_Exception(
+                db2_conn_errormsg($this->_connection),
+                db2_conn_error($this->_connection));
+        }
+
+        $this->_setExecuteMode(DB2_AUTOCOMMIT_ON);
+    }
+
+    /**
+     * Rollback a transaction.
+     *
+     * @return void
+     */
+    protected function _rollBack()
+    {
+        if (!db2_rollback($this->_connection)) {
+            /**
+             * @see Zend_Db_Adapter_Db2_Exception
+             */
+            require_once 'Zend/Db/Adapter/Db2/Exception.php';
+            throw new Zend_Db_Adapter_Db2_Exception(
+                db2_conn_errormsg($this->_connection),
+                db2_conn_error($this->_connection));
+        }
+        $this->_setExecuteMode(DB2_AUTOCOMMIT_ON);
+    }
+
+    /**
+     * Set the fetch mode.
+     *
+     * @param integer $mode
+     * @return void
+     * @throws Zend_Db_Adapter_Db2_Exception
+     */
+    public function setFetchMode($mode)
+    {
+        switch ($mode) {
+            case Zend_Db::FETCH_NUM:   // seq array
+            case Zend_Db::FETCH_ASSOC: // assoc array
+            case Zend_Db::FETCH_BOTH:  // seq+assoc array
+            case Zend_Db::FETCH_OBJ:   // object
+                $this->_fetchMode = $mode;
+                break;
+            case Zend_Db::FETCH_BOUND:   // bound to PHP variable
+                /**
+                 * @see Zend_Db_Adapter_Db2_Exception
+                 */
+                require_once 'Zend/Db/Adapter/Db2/Exception.php';
+                throw new Zend_Db_Adapter_Db2_Exception('FETCH_BOUND is not supported yet');
+                break;
+            default:
+                /**
+                 * @see Zend_Db_Adapter_Db2_Exception
+                 */
+                require_once 'Zend/Db/Adapter/Db2/Exception.php';
+                throw new Zend_Db_Adapter_Db2_Exception("Invalid fetch mode '$mode' specified");
+                break;
+        }
+    }
+
+    /**
+     * Adds an adapter-specific LIMIT clause to the SELECT statement.
+     *
+     * @param string $sql
+     * @param integer $count
+     * @param integer $offset OPTIONAL
+     * @return string
+     */
+    public function limit($sql, $count, $offset = 0)
+    {
+        $count = intval($count);
+        if ($count <= 0) {
+            /**
+             * @see Zend_Db_Adapter_Db2_Exception
+             */
+            require_once 'Zend/Db/Adapter/Db2/Exception.php';
+            throw new Zend_Db_Adapter_Db2_Exception("LIMIT argument count=$count is not valid");
+        }
+
+        $offset = intval($offset);
+        if ($offset < 0) {
+            /**
+             * @see Zend_Db_Adapter_Db2_Exception
+             */
+            require_once 'Zend/Db/Adapter/Db2/Exception.php';
+            throw new Zend_Db_Adapter_Db2_Exception("LIMIT argument offset=$offset is not valid");
+        }
+
+        if ($offset == 0) {
+            $limit_sql = $sql . " FETCH FIRST $count ROWS ONLY";
+            return $limit_sql;
+        }
+
+        /**
+         * DB2 does not implement the LIMIT clause as some RDBMS do.
+         * We have to simulate it with subqueries and ROWNUM.
+         * Unfortunately because we use the column wildcard "*",
+         * this puts an extra column into the query result set.
+         */
+        $limit_sql = "SELECT z2.*
+            FROM (
+                SELECT ROW_NUMBER() OVER() AS \"ZEND_DB_ROWNUM\", z1.*
+                FROM (
+                    " . $sql . "
+                ) z1
+            ) z2
+            WHERE z2.zend_db_rownum BETWEEN " . ($offset+1) . " AND " . ($offset+$count);
+        return $limit_sql;
+    }
+
+    /**
+     * Check if the adapter supports real SQL parameters.
+     *
+     * @param string $type 'positional' or 'named'
+     * @return bool
+     */
+    public function supportsParameters($type)
+    {
+        if ($type == 'positional') {
+            return true;
+        }
+
+        // if its 'named' or anything else
+        return false;
+    }
+
+    /**
+     * Retrieve server version in PHP style
+     *
+     * @return string
+     */
+    public function getServerVersion()
+    {
+        $this->_connect();
+        $server_info = db2_server_info($this->_connection);
+        if ($server_info !== false) {
+            $version = $server_info->DBMS_VER;
+            if ($this->_isI5) {
+                $version = (int) substr($version, 0, 2) . '.' . (int) substr($version, 2, 2) . '.' . (int) substr($version, 4);
+            }
+            return $version;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Return whether or not this is running on i5
+     *
+     * @return bool
+     */
+    public function isI5()
+    {
+        if ($this->_isI5 === null) {
+            $this->_determineI5();
+        }
+
+        return (bool) $this->_isI5;
+    }
+
+    /**
+     * Check the connection parameters according to verify
+     * type of used OS
+     *
+     *  @return void
+     */
+    protected function _determineI5()
+    {
+        // first us the compiled flag.
+        $this->_isI5 = (php_uname('s') == 'OS400') ? true : false;
+
+        // if this is set, then us it
+        if (isset($this->_config['os'])){
+            if (strtolower($this->_config['os']) === 'i5') {
+                $this->_isI5 = true;
+            } else {
+                // any other value passed in, its null
+                $this->_isI5 = false;
+            }
+        }
+
+    }
+
+    /**
+     * Db2 On I5 specific method
+     *
+     * Returns a list of the tables in the database .
+     * Used only for DB2/400.
+     *
+     * @return array
+     */
+    protected function _i5listTables($schema = null)
+    {
+        //list of i5 libraries.
+        $tables = array();
+        if ($schema) {
+            $tablesStatement = db2_tables($this->_connection, null, $schema);
+            while ($rowTables = db2_fetch_assoc($tablesStatement) ) {
+                if ($rowTables['TABLE_NAME'] !== null) {
+                    $tables[] = $rowTables['TABLE_NAME'];
+                }
+            }
+        } else {
+            $schemaStatement = db2_tables($this->_connection);
+            while ($schema = db2_fetch_assoc($schemaStatement)) {
+                if ($schema['TABLE_SCHEM'] !== null) {
+                    // list of the tables which belongs to the selected library
+                    $tablesStatement = db2_tables($this->_connection, NULL, $schema['TABLE_SCHEM']);
+                    if (is_resource($tablesStatement)) {
+                        while ($rowTables = db2_fetch_assoc($tablesStatement) ) {
+                            if ($rowTables['TABLE_NAME'] !== null) {
+                                $tables[] = $rowTables['TABLE_NAME'];
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        return $tables;
+    }
+
+    protected function _i5LastInsertId($objectName = null, $idType = null)
+    {
+
+        if ($objectName === null) {
+            $sql = 'SELECT IDENTITY_VAL_LOCAL() AS VAL FROM QSYS2.QSQPTABL';
+            $value = $this->fetchOne($sql);
+            return $value;
+        }
+
+        if (strtoupper($idType) === 'S'){
+            //check i5_lib option
+            $sequenceName = $objectName;
+            return $this->lastSequenceId($sequenceName);
+        }
+
+            //returns last identity value for the specified table
+        //if (strtoupper($idType) === 'I') {
+        $tableName = $objectName;
+        return $this->fetchOne('SELECT IDENTITY_VAL_LOCAL() from ' . $this->quoteIdentifier($tableName));
+    }
+
+}
+
+
diff --git a/lib/Zend/Db/Adapter/Db2/Exception.php b/lib/Zend/Db/Adapter/Db2/Exception.php
new file mode 100644 (file)
index 0000000..0287a10
--- /dev/null
@@ -0,0 +1,45 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Db
+ * @subpackage Adapter
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Exception.php 10020 2009-08-18 14:34:09Z j.fischer@metaways.de $
+ */
+
+/**
+ * Zend_Db_Adapter_Exception
+ */
+require_once 'Zend/Db/Adapter/Exception.php';
+
+/**
+ * Zend_Db_Adapter_Db2_Exception
+ *
+ * @package    Zend_Db
+ * @subpackage Adapter
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Db_Adapter_Db2_Exception extends Zend_Db_Adapter_Exception
+{
+   protected $code = '00000';
+   protected $message = 'unknown exception';
+
+   function __construct($msg = 'unknown exception', $state = '00000') {
+       $this->message = $msg;
+       $this->code = $state;
+   }
+}
diff --git a/lib/Zend/Db/Adapter/Exception.php b/lib/Zend/Db/Adapter/Exception.php
new file mode 100644 (file)
index 0000000..69b2d02
--- /dev/null
@@ -0,0 +1,53 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Db
+ * @subpackage Adapter
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Exception.php 10020 2009-08-18 14:34:09Z j.fischer@metaways.de $
+ */
+
+/**
+ * Zend_Db_Exception
+ */
+require_once 'Zend/Db/Exception.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Db
+ * @subpackage Adapter
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Db_Adapter_Exception extends Zend_Db_Exception
+{
+    protected $_chainedException = null;
+
+    public function __construct($message = null, Exception $e = null)
+    {
+        if ($e) {
+            $this->_chainedException = $e;
+            $this->code = $e->getCode();
+        }
+        parent::__construct($message);
+    }
+
+    public function getChainedException()
+    {
+        return $this->_chainedException;
+    }
+
+}
diff --git a/lib/Zend/Db/Adapter/Mysqli.php b/lib/Zend/Db/Adapter/Mysqli.php
new file mode 100644 (file)
index 0000000..e592c99
--- /dev/null
@@ -0,0 +1,549 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Db
+ * @subpackage Adapter
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Mysqli.php 10020 2009-08-18 14:34:09Z j.fischer@metaways.de $
+ */
+
+
+/**
+ * @see Zend_Db_Adapter_Abstract
+ */
+require_once 'Zend/Db/Adapter/Abstract.php';
+
+/**
+ * @see Zend_Db_Profiler
+ */
+require_once 'Zend/Db/Profiler.php';
+
+/**
+ * @see Zend_Db_Select
+ */
+require_once 'Zend/Db/Select.php';
+
+/**
+ * @see Zend_Db_Statement_Mysqli
+ */
+require_once 'Zend/Db/Statement/Mysqli.php';
+
+
+/**
+ * @category   Zend
+ * @package    Zend_Db
+ * @subpackage Adapter
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Db_Adapter_Mysqli extends Zend_Db_Adapter_Abstract
+{
+
+    /**
+     * Keys are UPPERCASE SQL datatypes or the constants
+     * Zend_Db::INT_TYPE, Zend_Db::BIGINT_TYPE, or Zend_Db::FLOAT_TYPE.
+     *
+     * Values are:
+     * 0 = 32-bit integer
+     * 1 = 64-bit integer
+     * 2 = float or decimal
+     *
+     * @var array Associative array of datatypes to values 0, 1, or 2.
+     */
+    protected $_numericDataTypes = array(
+        Zend_Db::INT_TYPE    => Zend_Db::INT_TYPE,
+        Zend_Db::BIGINT_TYPE => Zend_Db::BIGINT_TYPE,
+        Zend_Db::FLOAT_TYPE  => Zend_Db::FLOAT_TYPE,
+        'INT'                => Zend_Db::INT_TYPE,
+        'INTEGER'            => Zend_Db::INT_TYPE,
+        'MEDIUMINT'          => Zend_Db::INT_TYPE,
+        'SMALLINT'           => Zend_Db::INT_TYPE,
+        'TINYINT'            => Zend_Db::INT_TYPE,
+        'BIGINT'             => Zend_Db::BIGINT_TYPE,
+        'SERIAL'             => Zend_Db::BIGINT_TYPE,
+        'DEC'                => Zend_Db::FLOAT_TYPE,
+        'DECIMAL'            => Zend_Db::FLOAT_TYPE,
+        'DOUBLE'             => Zend_Db::FLOAT_TYPE,
+        'DOUBLE PRECISION'   => Zend_Db::FLOAT_TYPE,
+        'FIXED'              => Zend_Db::FLOAT_TYPE,
+        'FLOAT'              => Zend_Db::FLOAT_TYPE
+    );
+
+    /**
+     * @var Zend_Db_Statement_Mysqli
+     */
+    protected $_stmt = null;
+
+    /**
+     * Default class name for a DB statement.
+     *
+     * @var string
+     */
+    protected $_defaultStmtClass = 'Zend_Db_Statement_Mysqli';
+
+    /**
+     * Quote a raw string.
+     *
+     * @param mixed $value Raw string
+     *
+     * @return string           Quoted string
+     */
+    protected function _quote($value)
+    {
+        if (is_int($value) || is_float($value)) {
+            return $value;
+        }
+        $this->_connect();
+        return "'" . $this->_connection->real_escape_string($value) . "'";
+    }
+
+    /**
+     * Returns the symbol the adapter uses for delimiting identifiers.
+     *
+     * @return string
+     */
+    public function getQuoteIdentifierSymbol()
+    {
+        return "`";
+    }
+
+    /**
+     * Returns a list of the tables in the database.
+     *
+     * @return array
+     */
+    public function listTables()
+    {
+        $result = array();
+        // Use mysqli extension API, because SHOW doesn't work
+        // well as a prepared statement on MySQL 4.1.
+        $sql = 'SHOW TABLES';
+        if ($queryResult = $this->getConnection()->query($sql)) {
+            while ($row = $queryResult->fetch_row()) {
+                $result[] = $row[0];
+            }
+            $queryResult->close();
+        } else {
+            /**
+             * @see Zend_Db_Adapter_Mysqli_Exception
+             */
+            require_once 'Zend/Db/Adapter/Mysqli/Exception.php';
+            throw new Zend_Db_Adapter_Mysqli_Exception($this->getConnection()->error);
+        }
+        return $result;
+    }
+
+    /**
+     * Returns the column descriptions for a table.
+     *
+     * The return value is an associative array keyed by the column name,
+     * as returned by the RDBMS.
+     *
+     * The value of each array element is an associative array
+     * with the following keys:
+     *
+     * SCHEMA_NAME      => string; name of database or schema
+     * TABLE_NAME       => string;
+     * COLUMN_NAME      => string; column name
+     * COLUMN_POSITION  => number; ordinal position of column in table
+     * DATA_TYPE        => string; SQL datatype name of column
+     * DEFAULT          => string; default expression of column, null if none
+     * NULLABLE         => boolean; true if column can have nulls
+     * LENGTH           => number; length of CHAR/VARCHAR
+     * SCALE            => number; scale of NUMERIC/DECIMAL
+     * PRECISION        => number; precision of NUMERIC/DECIMAL
+     * UNSIGNED         => boolean; unsigned property of an integer type
+     * PRIMARY          => boolean; true if column is part of the primary key
+     * PRIMARY_POSITION => integer; position of column in primary key
+     * IDENTITY         => integer; true if column is auto-generated with unique values
+     *
+     * @param string $tableName
+     * @param string $schemaName OPTIONAL
+     * @return array
+     */
+    public function describeTable($tableName, $schemaName = null)
+    {
+        /**
+         * @todo  use INFORMATION_SCHEMA someday when
+         * MySQL's implementation isn't too slow.
+         */
+
+        if ($schemaName) {
+            $sql = 'DESCRIBE ' . $this->quoteIdentifier("$schemaName.$tableName", true);
+        } else {
+            $sql = 'DESCRIBE ' . $this->quoteIdentifier($tableName, true);
+        }
+
+        /**
+         * Use mysqli extension API, because DESCRIBE doesn't work
+         * well as a prepared statement on MySQL 4.1.
+         */
+        if ($queryResult = $this->getConnection()->query($sql)) {
+            while ($row = $queryResult->fetch_assoc()) {
+                $result[] = $row;
+            }
+            $queryResult->close();
+        } else {
+            /**
+             * @see Zend_Db_Adapter_Mysqli_Exception
+             */
+            require_once 'Zend/Db/Adapter/Mysqli/Exception.php';
+            throw new Zend_Db_Adapter_Mysqli_Exception($this->getConnection()->error);
+        }
+
+        $desc = array();
+
+        $row_defaults = array(
+            'Length'          => null,
+            'Scale'           => null,
+            'Precision'       => null,
+            'Unsigned'        => null,
+            'Primary'         => false,
+            'PrimaryPosition' => null,
+            'Identity'        => false
+        );
+        $i = 1;
+        $p = 1;
+        foreach ($result as $key => $row) {
+            $row = array_merge($row_defaults, $row);
+            if (preg_match('/unsigned/', $row['Type'])) {
+                $row['Unsigned'] = true;
+            }
+            if (preg_match('/^((?:var)?char)\((\d+)\)/', $row['Type'], $matches)) {
+                $row['Type'] = $matches[1];
+                $row['Length'] = $matches[2];
+            } else if (preg_match('/^decimal\((\d+),(\d+)\)/', $row['Type'], $matches)) {
+                $row['Type'] = 'decimal';
+                $row['Precision'] = $matches[1];
+                $row['Scale'] = $matches[2];
+            } else if (preg_match('/^float\((\d+),(\d+)\)/', $row['Type'], $matches)) {
+                $row['Type'] = 'float';
+                $row['Precision'] = $matches[1];
+                $row['Scale'] = $matches[2];
+            } else if (preg_match('/^((?:big|medium|small|tiny)?int)\((\d+)\)/', $row['Type'], $matches)) {
+                $row['Type'] = $matches[1];
+                /**
+                 * The optional argument of a MySQL int type is not precision
+                 * or length; it is only a hint for display width.
+                 */
+            }
+            if (strtoupper($row['Key']) == 'PRI') {
+                $row['Primary'] = true;
+                $row['PrimaryPosition'] = $p;
+                if ($row['Extra'] == 'auto_increment') {
+                    $row['Identity'] = true;
+                } else {
+                    $row['Identity'] = false;
+                }
+                ++$p;
+            }
+            $desc[$this->foldCase($row['Field'])] = array(
+                'SCHEMA_NAME'      => null, // @todo
+                'TABLE_NAME'       => $this->foldCase($tableName),
+                'COLUMN_NAME'      => $this->foldCase($row['Field']),
+                'COLUMN_POSITION'  => $i,
+                'DATA_TYPE'        => $row['Type'],
+                'DEFAULT'          => $row['Default'],
+                'NULLABLE'         => (bool) ($row['Null'] == 'YES'),
+                'LENGTH'           => $row['Length'],
+                'SCALE'            => $row['Scale'],
+                'PRECISION'        => $row['Precision'],
+                'UNSIGNED'         => $row['Unsigned'],
+                'PRIMARY'          => $row['Primary'],
+                'PRIMARY_POSITION' => $row['PrimaryPosition'],
+                'IDENTITY'         => $row['Identity']
+            );
+            ++$i;
+        }
+        return $desc;
+    }
+
+    /**
+     * Creates a connection to the database.
+     *
+     * @return void
+     * @throws Zend_Db_Adapter_Mysqli_Exception
+     */
+    protected function _connect()
+    {
+        if ($this->_connection) {
+            return;
+        }
+
+        if (!extension_loaded('mysqli')) {
+            /**
+             * @see Zend_Db_Adapter_Mysqli_Exception
+             */
+            require_once 'Zend/Db/Adapter/Mysqli/Exception.php';
+            throw new Zend_Db_Adapter_Mysqli_Exception('The Mysqli extension is required for this adapter but the extension is not loaded');
+        }
+
+        if (isset($this->_config['port'])) {
+            $port = (integer) $this->_config['port'];
+        } else {
+            $port = null;
+        }
+
+        $this->_connection = mysqli_init();
+
+        if(!empty($this->_config['driver_options'])) {
+            foreach($this->_config['driver_options'] as $option=>$value) {
+                if(is_string($option)) {
+                    // Suppress warnings here
+                    // Ignore it if it's not a valid constant
+                    $option = @constant(strtoupper($option));
+                    if($option === null)
+                        continue;
+                }
+                mysqli_options($this->_connection, $option, $value);
+            }
+        }
+
+        // Suppress connection warnings here.
+        // Throw an exception instead.
+        $_isConnected = @mysqli_real_connect(
+            $this->_connection,
+            $this->_config['host'],
+            $this->_config['username'],
+            $this->_config['password'],
+            $this->_config['dbname'],
+            $port
+        );
+
+        if ($_isConnected === false || mysqli_connect_errno()) {
+
+            $this->closeConnection();
+            /**
+             * @see Zend_Db_Adapter_Mysqli_Exception
+             */
+            require_once 'Zend/Db/Adapter/Mysqli/Exception.php';
+            throw new Zend_Db_Adapter_Mysqli_Exception(mysqli_connect_error());
+        }
+
+        if (!empty($this->_config['charset'])) {
+            mysqli_set_charset($this->_connection, $this->_config['charset']);
+        }
+    }
+
+    /**
+     * Test if a connection is active
+     *
+     * @return boolean
+     */
+    public function isConnected()
+    {
+        return ((bool) ($this->_connection instanceof mysqli));
+    }
+
+    /**
+     * Force the connection to close.
+     *
+     * @return void
+     */
+    public function closeConnection()
+    {
+        if ($this->isConnected()) {
+            $this->_connection->close();
+        }
+        $this->_connection = null;
+    }
+
+    /**
+     * Prepare a statement and return a PDOStatement-like object.
+     *
+     * @param  string  $sql  SQL query
+     * @return Zend_Db_Statement_Mysqli
+     */
+    public function prepare($sql)
+    {
+        $this->_connect();
+        if ($this->_stmt) {
+            $this->_stmt->close();
+        }
+        $stmtClass = $this->_defaultStmtClass;
+        if (!class_exists($stmtClass)) {
+            require_once 'Zend/Loader.php';
+            Zend_Loader::loadClass($stmtClass);
+        }
+        $stmt = new $stmtClass($this, $sql);
+        if ($stmt === false) {
+            return false;
+        }
+        $stmt->setFetchMode($this->_fetchMode);
+        $this->_stmt = $stmt;
+        return $stmt;
+    }
+
+    /**
+     * Gets the last ID generated automatically by an IDENTITY/AUTOINCREMENT column.
+     *
+     * As a convention, on RDBMS brands that support sequences
+     * (e.g. Oracle, PostgreSQL, DB2), this method forms the name of a sequence
+     * from the arguments and returns the last id generated by that sequence.
+     * On RDBMS brands that support IDENTITY/AUTOINCREMENT columns, this method
+     * returns the last value generated for such a column, and the table name
+     * argument is disregarded.
+     *
+     * MySQL does not support sequences, so $tableName and $primaryKey are ignored.
+     *
+     * @param string $tableName   OPTIONAL Name of table.
+     * @param string $primaryKey  OPTIONAL Name of primary key column.
+     * @return string
+     * @todo Return value should be int?
+     */
+    public function lastInsertId($tableName = null, $primaryKey = null)
+    {
+        $mysqli = $this->_connection;
+        return (string) $mysqli->insert_id;
+    }
+
+    /**
+     * Begin a transaction.
+     *
+     * @return void
+     */
+    protected function _beginTransaction()
+    {
+        $this->_connect();
+        $this->_connection->autocommit(false);
+    }
+
+    /**
+     * Commit a transaction.
+     *
+     * @return void
+     */
+    protected function _commit()
+    {
+        $this->_connect();
+        $this->_connection->commit();
+        $this->_connection->autocommit(true);
+    }
+
+    /**
+     * Roll-back a transaction.
+     *
+     * @return void
+     */
+    protected function _rollBack()
+    {
+        $this->_connect();
+        $this->_connection->rollback();
+        $this->_connection->autocommit(true);
+    }
+
+    /**
+     * Set the fetch mode.
+     *
+     * @param int $mode
+     * @return void
+     * @throws Zend_Db_Adapter_Mysqli_Exception
+     */
+    public function setFetchMode($mode)
+    {
+        switch ($mode) {
+            case Zend_Db::FETCH_LAZY:
+            case Zend_Db::FETCH_ASSOC:
+            case Zend_Db::FETCH_NUM:
+            case Zend_Db::FETCH_BOTH:
+            case Zend_Db::FETCH_NAMED:
+            case Zend_Db::FETCH_OBJ:
+                $this->_fetchMode = $mode;
+                break;
+            case Zend_Db::FETCH_BOUND: // bound to PHP variable
+                /**
+                 * @see Zend_Db_Adapter_Mysqli_Exception
+                 */
+                require_once 'Zend/Db/Adapter/Mysqli/Exception.php';
+                throw new Zend_Db_Adapter_Mysqli_Exception('FETCH_BOUND is not supported yet');
+                break;
+            default:
+                /**
+                 * @see Zend_Db_Adapter_Mysqli_Exception
+                 */
+                require_once 'Zend/Db/Adapter/Mysqli/Exception.php';
+                throw new Zend_Db_Adapter_Mysqli_Exception("Invalid fetch mode '$mode' specified");
+        }
+    }
+
+    /**
+     * Adds an adapter-specific LIMIT clause to the SELECT statement.
+     *
+     * @param string $sql
+     * @param int $count
+     * @param int $offset OPTIONAL
+     * @return string
+     */
+    public function limit($sql, $count, $offset = 0)
+    {
+        $count = intval($count);
+        if ($count <= 0) {
+            /**
+             * @see Zend_Db_Adapter_Mysqli_Exception
+             */
+            require_once 'Zend/Db/Adapter/Mysqli/Exception.php';
+            throw new Zend_Db_Adapter_Mysqli_Exception("LIMIT argument count=$count is not valid");
+        }
+
+        $offset = intval($offset);
+        if ($offset < 0) {
+            /**
+             * @see Zend_Db_Adapter_Mysqli_Exception
+             */
+            require_once 'Zend/Db/Adapter/Mysqli/Exception.php';
+            throw new Zend_Db_Adapter_Mysqli_Exception("LIMIT argument offset=$offset is not valid");
+        }
+
+        $sql .= " LIMIT $count";
+        if ($offset > 0) {
+            $sql .= " OFFSET $offset";
+        }
+
+        return $sql;
+    }
+
+    /**
+     * Check if the adapter supports real SQL parameters.
+     *
+     * @param string $type 'positional' or 'named'
+     * @return bool
+     */
+    public function supportsParameters($type)
+    {
+        switch ($type) {
+            case 'positional':
+                return true;
+            case 'named':
+            default:
+                return false;
+        }
+    }
+
+    /**
+     * Retrieve server version in PHP style
+     *
+     *@return string
+     */
+    public function getServerVersion()
+    {
+        $this->_connect();
+        $version = $this->_connection->server_version;
+        $major = (int) ($version / 10000);
+        $minor = (int) ($version % 10000 / 100);
+        $revision = (int) ($version % 100);
+        return $major . '.' . $minor . '.' . $revision;
+    }
+}
diff --git a/lib/Zend/Db/Adapter/Mysqli/Exception.php b/lib/Zend/Db/Adapter/Mysqli/Exception.php
new file mode 100644 (file)
index 0000000..1b83c22
--- /dev/null
@@ -0,0 +1,40 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Db
+ * @subpackage Adapter
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Exception.php 10020 2009-08-18 14:34:09Z j.fischer@metaways.de $
+ *
+ */
+
+/**
+ * Zend
+ */
+require_once 'Zend/Db/Adapter/Exception.php';
+
+/**
+ * Zend_Db_Adapter_Mysqli_Exception
+ *
+ * @category   Zend
+ * @package    Zend_Db
+ * @subpackage Adapter
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Db_Adapter_Mysqli_Exception extends Zend_Db_Adapter_Exception
+{
+}
diff --git a/lib/Zend/Db/Adapter/Oracle.php b/lib/Zend/Db/Adapter/Oracle.php
new file mode 100644 (file)
index 0000000..0a9573e
--- /dev/null
@@ -0,0 +1,680 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Db
+ * @subpackage Adapter
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Oracle.php 10020 2009-08-18 14:34:09Z j.fischer@metaways.de $
+ */
+
+/**
+ * @see Zend_Db_Adapter_Abstract
+ */
+require_once 'Zend/Db/Adapter/Abstract.php';
+
+/**
+ * @see Zend_Db_Statement_Oracle
+ */
+require_once 'Zend/Db/Statement/Oracle.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Db
+ * @subpackage Adapter
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Db_Adapter_Oracle extends Zend_Db_Adapter_Abstract
+{
+    /**
+     * User-provided configuration.
+     *
+     * Basic keys are:
+     *
+     * username => (string) Connect to the database as this username.
+     * password => (string) Password associated with the username.
+     * dbname   => Either the name of the local Oracle instance, or the
+     *             name of the entry in tnsnames.ora to which you want to connect.
+     * persistent => (boolean) Set TRUE to use a persistent connection
+     * @var array
+     */
+    protected $_config = array(
+        'dbname'       => null,
+        'username'     => null,
+        'password'     => null,
+        'persistent'   => false
+    );
+
+    /**
+     * Keys are UPPERCASE SQL datatypes or the constants
+     * Zend_Db::INT_TYPE, Zend_Db::BIGINT_TYPE, or Zend_Db::FLOAT_TYPE.
+     *
+     * Values are:
+     * 0 = 32-bit integer
+     * 1 = 64-bit integer
+     * 2 = float or decimal
+     *
+     * @var array Associative array of datatypes to values 0, 1, or 2.
+     */
+    protected $_numericDataTypes = array(
+        Zend_Db::INT_TYPE    => Zend_Db::INT_TYPE,
+        Zend_Db::BIGINT_TYPE => Zend_Db::BIGINT_TYPE,
+        Zend_Db::FLOAT_TYPE  => Zend_Db::FLOAT_TYPE,
+        'BINARY_DOUBLE'      => Zend_Db::FLOAT_TYPE,
+        'BINARY_FLOAT'       => Zend_Db::FLOAT_TYPE,
+        'NUMBER'             => Zend_Db::FLOAT_TYPE,
+    );
+
+    /**
+     * @var integer
+     */
+    protected $_execute_mode = OCI_COMMIT_ON_SUCCESS;
+
+    /**
+     * Default class name for a DB statement.
+     *
+     * @var string
+     */
+    protected $_defaultStmtClass = 'Zend_Db_Statement_Oracle';
+
+    /**
+     * Check if LOB field are returned as string
+     * instead of OCI-Lob object
+     *
+     * @var boolean
+     */
+    protected $_lobAsString = null;
+
+    /**
+     * Creates a connection resource.
+     *
+     * @return void
+     * @throws Zend_Db_Adapter_Oracle_Exception
+     */
+    protected function _connect()
+    {
+        if (is_resource($this->_connection)) {
+            // connection already exists
+            return;
+        }
+
+        if (!extension_loaded('oci8')) {
+            /**
+             * @see Zend_Db_Adapter_Oracle_Exception
+             */
+            require_once 'Zend/Db/Adapter/Oracle/Exception.php';
+            throw new Zend_Db_Adapter_Oracle_Exception('The OCI8 extension is required for this adapter but the extension is not loaded');
+        }
+
+        $connectionFuncName = ($this->_config['persistent'] == true) ? 'oci_pconnect' : 'oci_connect';
+        
+        $this->_connection = @$connectionFuncName(
+                $this->_config['username'],
+                $this->_config['password'],
+                $this->_config['dbname'],
+                $this->_config['charset']);
+
+        // check the connection
+        if (!$this->_connection) {
+            /**
+             * @see Zend_Db_Adapter_Oracle_Exception
+             */
+            require_once 'Zend/Db/Adapter/Oracle/Exception.php';
+            throw new Zend_Db_Adapter_Oracle_Exception(oci_error());
+        }
+    }
+
+    /**
+     * Test if a connection is active
+     *
+     * @return boolean
+     */
+    public function isConnected()
+    {
+        return ((bool) (is_resource($this->_connection)</