diff --git a/lib/kolab_sync_backend_common.php b/lib/kolab_sync_backend_common.php index 018ee15..06e0fbb 100644 --- a/lib/kolab_sync_backend_common.php +++ b/lib/kolab_sync_backend_common.php @@ -1,277 +1,279 @@ | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU Affero General Public License as published | | by the Free Software Foundation, either version 3 of the License, or | | (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | You should have received a copy of the GNU Affero General Public License | | along with this program. If not, see | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Parent backend class for kolab backends */ class kolab_sync_backend_common implements Syncroton_Backend_IBackend { /** * Table name * * @var string */ protected $table_name; /** * Model interface name * * @var string */ protected $interface_name; /** * Backend interface name * * @var string */ protected $class_name; /** * SQL Database engine * * @var rcube_db */ protected $db; /** * Internal cache (in-memory) * * @var array */ protected $cache = array(); /** * Constructor */ function __construct() { $this->db = rcube::get_instance()->get_dbh(); if (empty($this->class_name)) { $this->class_name = str_replace('Model_I', 'Model_', $this->interface_name); } } /** * Creates new Syncroton object in database * * @param Syncroton_Model_* $object Object * - * @throws InvalidArgumentException * @return Syncroton_Model_* Object + * @throws InvalidArgumentException|Syncroton_Exception_DeadlockDetected|Exception */ public function create($object) { if (! $object instanceof $this->interface_name) { throw new InvalidArgumentException('$object must be instance of ' . $this->interface_name); } $data = $this->object_to_array($object); $cols = array(); $data['id'] = $object->id = sha1(mt_rand(). microtime()); foreach (array_keys($data) as $key) { $cols[] = $this->db->quote_identifier($key); } $result = $this->db->query('INSERT INTO `' . $this->table_name . '`' . ' (' . implode(', ', $cols) . ')' . ' VALUES(' . implode(', ', array_fill(0, count($cols), '?')) . ')', array_values($data) ); if ($err = $this->db->is_error($result)) { - //Deadlock detected + $err = "Failed to save instance of {$this->interface_name}: {$err}"; if ($this->db->error_info()[0] == '40001') { - throw new Syncroton_Exception_DeadlockDetected('Failed to save instance of ' . $this->interface_name . ": " . $err); + throw new Syncroton_Exception_DeadlockDetected($err); } else { - throw new Exception('Failed to save instance of ' . $this->interface_name . ": " . $err); + throw new Exception($err); } } return $object; } /** * Returns Syncroton data object * * @param string $id * @throws Syncroton_Exception_NotFound * @return Syncroton_Model_* */ public function get($id) { $id = $id instanceof $this->interface_name ? $id->id : $id; if ($id) { $select = $this->db->query('SELECT * FROM `' . $this->table_name . '` WHERE `id` = ?', array($id)); $data = $this->db->fetch_assoc($select); } if (empty($data)) { throw new Syncroton_Exception_NotFound('Object not found'); } return $this->get_object($data); } /** * Deletes Syncroton data object * * @param string|Syncroton_Model_* $id Object or identifier * * @return bool True on success, False on failure + * @throws Syncroton_Exception_DeadlockDetected|Exception */ public function delete($id) { $id = $id instanceof $this->interface_name ? $id->id : $id; if (!$id) { return false; } $result = $this->db->query('DELETE FROM `' . $this->table_name .'` WHERE `id` = ?', array($id)); if ($err = $this->db->is_error($result)) { - //Deadlock detected + $err = "Failed to delete instance of {$this->interface_name}: {$err}"; if ($this->db->error_info()[0] == '40001') { - throw new Syncroton_Exception_DeadlockDetected('Failed to delete instance of ' . $this->interface_name . ": " . $err); + throw new Syncroton_Exception_DeadlockDetected($err); } else { - throw new Exception('Failed to delete instance of ' . $this->interface_name . ": " . $err); + throw new Exception($rr); } } return (bool) $this->db->affected_rows($result); } /** * Updates Syncroton data object * * @param Syncroton_Model_* $object * - * @throws InvalidArgumentException * @return Syncroton_Model_* Object + * @throws InvalidArgumentException|Syncroton_Exception_DeadlockDetected|Exception */ public function update($object) { if (! $object instanceof $this->interface_name) { throw new InvalidArgumentException('$object must be instanace of ' . $this->interface_name); } $data = $this->object_to_array($object); $set = array(); foreach (array_keys($data) as $key) { $set[] = $this->db->quote_identifier($key) . ' = ?'; } $result = $this->db->query('UPDATE `' . $this->table_name . '` SET ' . implode(', ', $set) . ' WHERE `id` = ' . $this->db->quote($object->id), array_values($data)); + if ($err = $this->db->is_error($result)) { - //Deadlock detected + $err = "Failed to update instance of {$this->interface_name}: {$err}"; if ($this->db->error_info()[0] == '40001') { - throw new Syncroton_Exception_DeadlockDetected('Failed to update instance of ' . $this->interface_name . ": " . $err); + throw new Syncroton_Exception_DeadlockDetected($err); } else { - throw new Exception('Failed to update instance of ' . $this->interface_name . ": " . $err); + throw new Exception($err); } } return $object; } /** * Returns list of user accounts * * @param Syncroton_Model_Device $device The current device * * @return array List of Syncroton_Model_Account objects */ public function userAccounts($device) { // this method is overwritten by kolab_sync_backend class } /** * Convert array into model object */ protected function get_object($data) { foreach ($data as $key => $value) { unset($data[$key]); if (!empty($value) && preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/', $value)) { // 2012-08-12 07:43:26 $value = new DateTime($value, new DateTimeZone('utc')); } $data[$this->to_camelcase($key, false)] = $value; } return new $this->class_name($data); } /** * Converts model object into array */ protected function object_to_array($object) { $data = array(); foreach ($object as $key => $value) { if ($value instanceof DateTime) { $value = $value->format('Y-m-d H:i:s'); } elseif (is_object($value) && isset($value->id)) { $value = $value->id; } $data[$this->from_camelcase($key)] = $value; } return $data; } /** * Convert property name from camel-case to lower-case-with-underscore */ protected function from_camelcase($string) { $string = lcfirst($string); return preg_replace_callback('/([A-Z])/', function ($string) { return '_' . strtolower($string[0]); }, $string); } /** * Convert property name from lower-case-with-underscore to camel-case */ protected function to_camelcase($string, $ucFirst = true) { if ($ucFirst) { $string = ucfirst($string); } return preg_replace_callback('/_([a-z])/', function ($string) { return strtoupper($string[1]); }, $string); } } diff --git a/lib/kolab_sync_backend_content.php b/lib/kolab_sync_backend_content.php index 83298af..36432cb 100644 --- a/lib/kolab_sync_backend_content.php +++ b/lib/kolab_sync_backend_content.php @@ -1,135 +1,138 @@ | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU Affero General Public License as published | | by the Free Software Foundation, either version 3 of the License, or | | (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | You should have received a copy of the GNU Affero General Public License | | along with this program. If not, see | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Kolab backend class for content storage */ class kolab_sync_backend_content extends kolab_sync_backend_common implements Syncroton_Backend_IContent { protected $table_name = 'syncroton_content'; protected $interface_name = 'Syncroton_Model_IContent'; /** * mark state as deleted. The state gets removed finally, * when the synckey gets validated during next sync. * * @param Syncroton_Model_IContent|string $id */ public function delete($id) { $id = $id instanceof Syncroton_Model_IContent ? $id->id : $id; $result = $this->db->query("UPDATE `{$this->table_name}` SET `is_deleted` = 1 WHERE `id` = ?", array($id)); if ($result = (bool) $this->db->affected_rows($result)) { unset($this->cache['content_folderstate']); } return $result; } /** * @param Syncroton_Model_IDevice|string $_deviceId * @param Syncroton_Model_IFolder|string $_folderId * @param string $_contentId + * * @return Syncroton_Model_IContent */ public function getContentState($_deviceId, $_folderId, $_contentId) { $deviceId = $_deviceId instanceof Syncroton_Model_IDevice ? $_deviceId->id : $_deviceId; $folderId = $_folderId instanceof Syncroton_Model_IFolder ? $_folderId->id : $_folderId; $where[] = $this->db->quote_identifier('device_id') . ' = ' . $this->db->quote($deviceId); $where[] = $this->db->quote_identifier('folder_id') . ' = ' . $this->db->quote($folderId); $where[] = $this->db->quote_identifier('contentid') . ' = ' . $this->db->quote($_contentId); $where[] = $this->db->quote_identifier('is_deleted') . ' = 0'; $select = $this->db->query("SELECT * FROM `{$this->table_name}` WHERE " . implode(' AND ', $where)); $state = $this->db->fetch_assoc($select); if (empty($state)) { throw new Syncroton_Exception_NotFound('Content not found'); } return $this->get_object($state); } /** * get array of ids which got send to the client for a given class * * @param Syncroton_Model_IDevice|string $_deviceId * @param Syncroton_Model_IFolder|string $_folderId + * @param int $syncKey + * * @return array */ public function getFolderState($_deviceId, $_folderId, $syncKey = null) { $deviceId = $_deviceId instanceof Syncroton_Model_IDevice ? $_deviceId->id : $_deviceId; $folderId = $_folderId instanceof Syncroton_Model_IFolder ? $_folderId->id : $_folderId; - $cachekey = $deviceId . ':' . $folderId; + $cachekey = $deviceId . ':' . $folderId . ':' . ($syncKey ?: '*'); // in Sync request we call this function twice in case when // folder state changed - use cache to skip at least one SELECT query if (isset($this->cache['content_folderstate'][$cachekey])) { return $this->cache['content_folderstate'][$cachekey]; } $where[] = $this->db->quote_identifier('device_id') . ' = ' . $this->db->quote($deviceId); $where[] = $this->db->quote_identifier('folder_id') . ' = ' . $this->db->quote($folderId); + $where[] = $this->db->quote_identifier('is_deleted') . ' = 0'; if ($syncKey) { $where[] = $this->db->quote_identifier('creation_synckey') . ' < ' . $this->db->quote($syncKey + 1); } - $where[] = $this->db->quote_identifier('is_deleted') . ' = 0'; $select = $this->db->query("SELECT `contentid` FROM `{$this->table_name}` WHERE " . implode(' AND ', $where)); $result = array(); while ($state = $this->db->fetch_assoc($select)) { $result[] = $state['contentid']; } return $this->cache['content_folderstate'][$cachekey] = $result; } /** * reset list of stored id * * @param Syncroton_Model_IDevice|string $_deviceId * @param Syncroton_Model_IFolder|string $_folderId */ public function resetState($_deviceId, $_folderId) { $deviceId = $_deviceId instanceof Syncroton_Model_IDevice ? $_deviceId->id : $_deviceId; $folderId = $_folderId instanceof Syncroton_Model_IFolder ? $_folderId->id : $_folderId; $cachekey = $deviceId . ':' . $folderId; unset($this->cache['content_folderstate'][$cachekey]); $where[] = $this->db->quote_identifier('device_id') . ' = ' . $this->db->quote($deviceId); $where[] = $this->db->quote_identifier('folder_id') . ' = ' . $this->db->quote($folderId); $this->db->query("DELETE FROM `{$this->table_name}` WHERE " . implode(' AND ', $where)); } }