diff --git a/lib/kolab_sync_backend.php b/lib/kolab_sync_backend.php index 89f83bb..d3b0f7b 100644 --- a/lib/kolab_sync_backend.php +++ b/lib/kolab_sync_backend.php @@ -1,1046 +1,1059 @@ | | | | 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 | +--------------------------------------------------------------------------+ */ class kolab_sync_backend { /** * Singleton instace of kolab_sync_backend * * @var kolab_sync_backend */ static protected $instance; protected $storage; protected $folder_meta; protected $folder_uids; protected $root_meta; static protected $types = array( 1 => '', 2 => 'mail.inbox', 3 => 'mail.drafts', 4 => 'mail.wastebasket', 5 => 'mail.sentitems', 6 => 'mail.outbox', 7 => 'task.default', 8 => 'event.default', 9 => 'contact.default', 10 => 'note.default', 11 => 'journal.default', 12 => 'mail', 13 => 'event', 14 => 'contact', 15 => 'task', 16 => 'journal', 17 => 'note', ); static protected $classes = array( Syncroton_Data_Factory::CLASS_CALENDAR => 'event', Syncroton_Data_Factory::CLASS_CONTACTS => 'contact', Syncroton_Data_Factory::CLASS_EMAIL => 'mail', Syncroton_Data_Factory::CLASS_NOTES => 'note', Syncroton_Data_Factory::CLASS_TASKS => 'task', ); const ROOT_MAILBOX = 'INBOX'; // const ROOT_MAILBOX = ''; const ASYNC_KEY = '/private/vendor/kolab/activesync'; const UID_KEY = '/shared/vendor/cmu/cyrus-imapd/uniqueid'; /** * This implements the 'singleton' design pattern * * @return kolab_sync_backend The one and only instance */ static function get_instance() { if (!self::$instance) { self::$instance = new kolab_sync_backend; self::$instance->startup(); // init AFTER object was linked with self::$instance } return self::$instance; } /** * Class initialization */ public function startup() { $this->storage = rcube::get_instance()->get_storage(); // @TODO: reset cache? if we do this for every request the cache would be useless // There's no session here //$this->storage->clear_cache('mailboxes.', true); // set additional header used by libkolab $this->storage->set_options(array( // @TODO: there can be Roundcube plugins defining additional headers, // we maybe would need to add them here 'fetch_headers' => 'X-KOLAB-TYPE X-KOLAB-MIME-VERSION', 'skip_deleted' => true, 'threading' => false, )); // Disable paging $this->storage->set_pagesize(999999); } /** * List known devices * * @return array Device list as hash array */ public function devices_list() { if ($this->root_meta === null) { // @TODO: consider server annotation instead of INBOX if ($meta = $this->storage->get_metadata(self::ROOT_MAILBOX, self::ASYNC_KEY)) { $this->root_meta = $this->unserialize_metadata($meta[self::ROOT_MAILBOX][self::ASYNC_KEY]); } else { $this->root_meta = array(); } } if (!empty($this->root_meta['DEVICE']) && is_array($this->root_meta['DEVICE'])) { return $this->root_meta['DEVICE']; } return array(); } /** * Get list of folders available for sync * * @param string $deviceid Device identifier * @param string $type Folder type * @param bool $flat_mode Enables flat-list mode * * @return array|bool List of mailbox folders, False on backend failure */ public function folders_list($deviceid, $type, $flat_mode = false) { // get all folders of specified type $folders = kolab_storage::list_folders('', '*', $type, false, $typedata); // get folders activesync config $folderdata = $this->folder_meta(); if (!is_array($folders) || !is_array($folderdata)) { return false; } $folders_list = array(); // check if folders are "subscribed" for activesync foreach ($folderdata as $folder => $meta) { if (empty($meta['FOLDER']) || empty($meta['FOLDER'][$deviceid]) || empty($meta['FOLDER'][$deviceid]['S']) ) { continue; } // force numeric folder name to be a string (T1283) $folder = (string) $folder; if (!empty($type) && !in_array($folder, $folders)) { continue; } // Activesync folder identifier (serverId) $folder_type = $typedata[$folder] ?: 'mail'; $folder_id = self::folder_id($folder, $folder_type); $folders_list[$folder_id] = $this->folder_data($folder, $folder_type); } if ($flat_mode) { $folders_list = $this->folders_list_flat($folders_list, $type, $typedata); } return $folders_list; } /** * Converts list of folders to a "flat" list */ private function folders_list_flat($folders, $type, $typedata) { $delim = $this->storage->get_hierarchy_delimiter(); foreach ($folders as $idx => $folder) { if ($folder['parentId']) { // for non-mail folders we make the list completely flat if ($type != 'mail') { $display_name = kolab_storage::object_name($folder['imap_name']); $display_name = html_entity_decode($display_name, ENT_COMPAT, RCUBE_CHARSET); $folders[$idx]['parentId'] = 0; $folders[$idx]['displayName'] = $display_name; } // for mail folders we modify only folders with non-existing parents else if (!isset($folders[$folder['parentId']])) { $items = explode($delim, $folder['imap_name']); $parent = 0; // find existing parent while (count($items) > 0) { array_pop($items); $parent_name = implode($items, $delim); $parent_type = $typedata[$parent_name] ?: 'mail'; $parent_id = self::folder_id($parent_name, $parent_type); if (isset($folders[$parent_id])) { $parent = $parent_id; break; } } if (!$parent) { $display_name = kolab_storage::object_name($folder['imap_name']); $display_name = html_entity_decode($display_name, ENT_COMPAT, RCUBE_CHARSET); } else { $parent_name = $folders[$parent_id]['imap_name']; $display_name = substr($folder['imap_name'], strlen($parent_name)+1); $display_name = rcube_charset::convert($display_name, 'UTF7-IMAP'); $display_name = str_replace($delim, ' ยป ', $display_name); } $folders[$idx]['parentId'] = $parent; $folders[$idx]['displayName'] = $display_name; } } } return $folders; } /** * Getter for folder metadata * * @return array|bool Hash array with meta data for each folder, False on backend failure */ public function folder_meta() { if (!isset($this->folder_meta)) { // get folders activesync config $folderdata = $this->storage->get_metadata("*", self::ASYNC_KEY); if (!is_array($folderdata)) { return $this->folder_meta = false; } $this->folder_meta = array(); foreach ($folderdata as $folder => $meta) { if ($asyncdata = $meta[self::ASYNC_KEY]) { if ($metadata = $this->unserialize_metadata($asyncdata)) { $this->folder_meta[$folder] = $metadata; } } } } return $this->folder_meta; } /** * Creates folder and subscribes to the device * * @param string $name Folder name (UTF7-IMAP) * @param int $type Folder (ActiveSync) type * @param string $deviceid Device identifier * * @return bool True on success, False on failure */ public function folder_create($name, $type, $deviceid) { if ($this->storage->folder_exists($name)) { $created = true; } else { $type = self::type_activesync2kolab($type); $created = kolab_storage::folder_create($name, $type, true); } if ($created) { // Set ActiveSync subscription flag $this->folder_set($name, $deviceid, 1); return true; } return false; } /** * Renames a folder * * @param string $old_name Old folder name (UTF7-IMAP) * @param string $new_name New folder name (UTF7-IMAP) * @param int $type Folder (ActiveSync) type * * @return bool True on success, False on failure */ public function folder_rename($old_name, $new_name, $type) { $this->folder_meta = null; $type = self::type_activesync2kolab($type); // don't use kolab_storage for moving mail folders if (preg_match('/^mail/', $type)) { return $this->storage->rename_folder($old_name, $new_name); } else { return kolab_storage::folder_rename($old_name, $new_name); } } /** * Deletes folder * * @param string $name Folder name (UTF7-IMAP) * @param string $deviceid Device identifier * */ public function folder_delete($name, $deviceid) { unset($this->folder_meta[$name]); return kolab_storage::folder_delete($name); } /** * Sets ActiveSync subscription flag on a folder * * @param string $name Folder name (UTF7-IMAP) * @param string $deviceid Device identifier * @param int $flag Flag value (0|1|2) */ public function folder_set($name, $deviceid, $flag) { if (empty($deviceid)) { return false; } // get folders activesync config $metadata = $this->folder_meta(); if (!is_array($metadata)) { return false; } $metadata = $metadata[$name]; if ($flag) { if (empty($metadata)) { $metadata = array(); } if (empty($metadata['FOLDER'])) { $metadata['FOLDER'] = array(); } if (empty($metadata['FOLDER'][$deviceid])) { $metadata['FOLDER'][$deviceid] = array(); } // Z-Push uses: // 1 - synchronize, no alarms // 2 - synchronize with alarms $metadata['FOLDER'][$deviceid]['S'] = $flag; } if (!$flag) { unset($metadata['FOLDER'][$deviceid]['S']); if (empty($metadata['FOLDER'][$deviceid])) { unset($metadata['FOLDER'][$deviceid]); } if (empty($metadata['FOLDER'])) { unset($metadata['FOLDER']); } if (empty($metadata)) { $metadata = null; } } // Return if nothing's been changed if (!self::data_array_diff($this->folder_meta[$name], $metadata)) { return true; } $this->folder_meta[$name] = $metadata; return $this->storage->set_metadata($name, array( self::ASYNC_KEY => $this->serialize_metadata($metadata))); } public function device_get($id) { $devices_list = $this->devices_list(); $result = $devices_list[$id]; return $result; } /** * Registers new device on server * * @param array $device Device data * @param string $id Device ID * * @return bool True on success, False on failure */ public function device_create($device, $id) { // Fill local cache $this->devices_list(); // Some devices create dummy devices with name "validate" (#1109) // This device entry is used in two initial requests, but later // the device registers a real name. We can remove this dummy entry // on new device creation $this->device_delete('validate'); // Old Kolab_ZPush device parameters // MODE: -1 | 0 | 1 (not set | flatmode | foldermode) // TYPE: device type string // ALIAS: user-friendly device name // Syncroton (kolab_sync_backend_device) uses // ID: internal identifier in syncroton database // TYPE: device type string // ALIAS: user-friendly device name $metadata = $this->root_meta; $metadata['DEVICE'][$id] = $device; $metadata = array(self::ASYNC_KEY => $this->serialize_metadata($metadata)); $result = $this->storage->set_metadata(self::ROOT_MAILBOX, $metadata); if ($result) { // Update local cache $this->root_meta['DEVICE'][$id] = $device; // subscribe default set of folders $this->device_init_subscriptions($id); } return $result; } /** * Device update. * * @param array $device Device data * @param string $id Device ID * * @return bool True on success, False on failure */ public function device_update($device, $id) { $devices_list = $this->devices_list(); $old_device = $devices_list[$id]; if (!$old_device) { return false; } // Do nothing if nothing is changed if (!self::data_array_diff($old_device, $device)) { return true; } $device = array_merge($old_device, $device); $metadata = $this->root_meta; $metadata['DEVICE'][$id] = $device; $metadata = array(self::ASYNC_KEY => $this->serialize_metadata($metadata)); $result = $this->storage->set_metadata(self::ROOT_MAILBOX, $metadata); if ($result) { // Update local cache $this->root_meta['DEVICE'][$id] = $device; } return $result; } /** * Device delete. * * @param string $id Device ID * * @return bool True on success, False on failure */ public function device_delete($id) { $device = $this->device_get($id); if (!$device) { return false; } unset($this->root_meta['DEVICE'][$id], $this->root_meta['FOLDER'][$id]); if (empty($this->root_meta['DEVICE'])) { unset($this->root_meta['DEVICE']); } if (empty($this->root_meta['FOLDER'])) { unset($this->root_meta['FOLDER']); } $metadata = $this->serialize_metadata($this->root_meta); $metadata = array(self::ASYNC_KEY => $metadata); // update meta data $result = $this->storage->set_metadata(self::ROOT_MAILBOX, $metadata); if ($result) { // remove device annotation for every folder foreach ($this->folder_meta() as $folder => $meta) { // skip root folder (already handled above) if ($folder == self::ROOT_MAILBOX) continue; if (!empty($meta['FOLDER']) && isset($meta['FOLDER'][$id])) { unset($meta['FOLDER'][$id]); if (empty($meta['FOLDER'])) { unset($this->folder_meta[$folder]['FOLDER']); unset($meta['FOLDER']); } if (empty($meta)) { unset($this->folder_meta[$folder]); $meta = null; } $metadata = array(self::ASYNC_KEY => $this->serialize_metadata($meta)); $res = $this->storage->set_metadata($folder, $metadata); if ($res && $meta) { $this->folder_meta[$folder] = $meta; } } } } return $result; } /** * Subscribe default set of folders on device registration */ private function device_init_subscriptions($deviceid) { // INBOX always exists $this->folder_set('INBOX', $deviceid, 1); $supported_types = array( 'mail.drafts', 'mail.wastebasket', 'mail.sentitems', 'mail.outbox', 'event.default', 'contact.default', 'note.default', 'task.default', 'event', 'contact', 'note', 'task', 'event.confidential', 'event.private', 'task.confidential', 'task.private', ); // This default set can be extended by adding following values: $modes = array( 'SUB_PERSONAL' => 1, // all subscribed folders in personal namespace 'ALL_PERSONAL' => 2, // all folders in personal namespace 'SUB_OTHER' => 4, // all subscribed folders in other users namespace 'ALL_OTHER' => 8, // all folders in other users namespace 'SUB_SHARED' => 16, // all subscribed folders in shared namespace 'ALL_SHARED' => 32, // all folders in shared namespace ); $rcube = rcube::get_instance(); $config = $rcube->config; $mode = (int) $config->get('activesync_init_subscriptions'); $folders = array(); // Subscribe to default folders $foldertypes = kolab_storage::folders_typedata(); if (!empty($foldertypes)) { $_foldertypes = array_intersect($foldertypes, $supported_types); // get default folders foreach ($_foldertypes as $folder => $type) { // only personal folders if ($this->storage->folder_namespace($folder) == 'personal') { $flag = preg_match('/^(event|task)/', $type) ? 2 : 1; $this->folder_set($folder, $deviceid, $flag); $folders[] = $folder; } } } // we're in default mode, exit if (!$mode) { return; } // below we support additionally all mail folders $supported_types[] = 'mail'; $supported_types[] = 'mail.junkemail'; // get configured special folders $special_folders = array(); $map = array( 'drafts' => 'mail.drafts', 'junk' => 'mail.junkemail', 'sent' => 'mail.sentitems', 'trash' => 'mail.wastebasket', ); foreach ($map as $folder => $type) { if ($folder = $config->get($folder . '_mbox')) { $special_folders[$folder] = $type; } } // get folders list(s) if (($mode & $modes['ALL_PERSONAL']) || ($mode & $modes['ALL_OTHER']) || ($mode & $modes['ALL_SHARED'])) { $all_folders = $this->storage->list_folders(); if (($mode & $modes['SUB_PERSONAL']) || ($mode & $modes['SUB_OTHER']) || ($mode & $modes['SUB_SHARED'])) { $subscribed_folders = $this->storage->list_folders_subscribed(); } } else { $all_folders = $this->storage->list_folders_subscribed(); } foreach ($all_folders as $folder) { // folder already subscribed if (in_array($folder, $folders)) { continue; } $type = $foldertypes[$folder] ?: 'mail'; if ($type == 'mail' && isset($special_folders[$folder])) { $type = $special_folders[$folder]; } if (!in_array($type, $supported_types)) { continue; } $ns = strtoupper($this->storage->folder_namespace($folder)); // subscribe the folder according to configured mode // and folder namespace/subscription status if (($mode & $modes["ALL_$ns"]) || (($mode & $modes["SUB_$ns"]) && (!isset($subscribed_folders) || in_array($folder, $subscribed_folders))) ) { $flag = preg_match('/^(event|task)/', $type) ? 2 : 1; $this->folder_set($folder, $deviceid, $flag); } } } /** * Helper method to decode saved IMAP metadata */ private function unserialize_metadata($str) { if (!empty($str)) { // Support old Z-Push annotation format if ($str[0] != '{') { $str = base64_decode($str); } $data = json_decode($str, true); return $data; } return null; } /** * Helper method to encode IMAP metadata for saving */ private function serialize_metadata($data) { if (!empty($data) && is_array($data)) { $data = json_encode($data); // $data = base64_encode($data); return $data; } return null; } /** * Returns Kolab folder type for specified ActiveSync type ID */ public static function type_activesync2kolab($type) { if (!empty(self::$types[$type])) { return self::$types[$type]; } return ''; } /** * Returns ActiveSync folder type for specified Kolab type */ public static function type_kolab2activesync($type) { $type = preg_replace('/\.(confidential|private)$/i', '', $type); if ($key = array_search($type, self::$types)) { return $key; } return key(self::$types); } /** * Returns Kolab folder type for specified ActiveSync class name */ public static function class_activesync2kolab($class) { if (!empty(self::$classes[$class])) { return self::$classes[$class]; } return ''; } /** * Returns folder data in Syncroton format */ private function folder_data($folder, $type) { // Folder name parameters $delim = $this->storage->get_hierarchy_delimiter(); $items = explode($delim, $folder); $name = array_pop($items); // Folder UID $folder_id = $this->folder_id($folder, $type); // Folder type - $type = self::type_kolab2activesync($type); - // fix type, if there's no type annotation it's detected as UNKNOWN - // we'll use 'mail' (12) or 'mail.inbox' (2) - if ($type == 1) { - $type = $folder == 'INBOX' ? 2 : 12; + if (strcasecmp($folder, 'INBOX') === 0) { + // INBOX is always inbox, prevent from issues related with a change of + // folder type annotation (it can be initially unset). + $type = 2; + } + else { + $type = self::type_kolab2activesync($type); + + // fix type, if there's no type annotation it's detected as UNKNOWN we'll use 'mail' (12) + if ($type == 1) { + $type = 12; + } } // Syncroton folder data array return array( 'serverId' => $folder_id, 'parentId' => count($items) ? self::folder_id(implode($delim, $items)) : 0, 'displayName' => rcube_charset::convert($name, 'UTF7-IMAP', kolab_sync::CHARSET), 'type' => $type, // for internal use 'imap_name' => $folder, ); } /** * Builds folder ID based on folder name */ public function folder_id($name, $type = null) { // ActiveSync expects folder identifiers to be max.64 characters // So we can't use just folder name $name = (string) $name; if ($name === '') { return null; } if (isset($this->folder_uids[$name])) { return $this->folder_uids[$name]; } /* @TODO: For now uniqueid annotation doesn't work, we will create UIDs by ourselves. There's one inconvenience of this solution: folder name/type change would be handled in ActiveSync as delete + create. // get folders unique identifier $folderdata = $this->storage->get_metadata($name, self::UID_KEY); if ($folderdata && !empty($folderdata[$name])) { $uid = $folderdata[$name][self::UID_KEY]; return $this->folder_uids[$name] = $uid; } */ - - if ($type === null) { - $type = kolab_storage::folder_type($name); + if (strcasecmp($name, 'INBOX') === 0) { + // INBOX is always inbox, prevent from issues related with a change of + // folder type annotation (it can be initially unset). + $type = 'mail.inbox'; } + else { + if ($type === null) { + $type = kolab_storage::folder_type($name); + } - $type = preg_replace('/\.(confidential|private)$/i', '', $type); + $type = preg_replace('/\.(confidential|private)$/i', '', $type); + } // Add type to folder UID hash, so type change can be detected by Syncroton $uid = $name . '!!' . $type; $uid = md5($uid); return $this->folder_uids[$name] = $uid; } /** * Returns IMAP folder name * * @param string $id Folder identifier * @param string $deviceid Device dentifier * * @return string Folder name (UTF7-IMAP) */ public function folder_id2name($id, $deviceid) { // check in cache first if (!empty($this->folder_uids)) { if (($name = array_search($id, $this->folder_uids)) !== false) { return $name; } } /* @TODO: see folder_id() // get folders unique identifier $folderdata = $this->storage->get_metadata('*', self::UID_KEY); foreach ((array)$folderdata as $folder => $data) { if (!empty($data[self::UID_KEY])) { $uid = $data[self::UID_KEY]; $this->folder_uids[$folder] = $uid; if ($uid == $id) { $name = $folder; } } } */ // get all folders of specified type $folderdata = $this->folder_meta(); if (!is_array($folderdata) || $id === null) { return null; } // check if folders are "subscribed" for activesync foreach ($folderdata as $folder => $meta) { if (empty($meta['FOLDER']) || empty($meta['FOLDER'][$deviceid]) || empty($meta['FOLDER'][$deviceid]['S']) ) { continue; } if ($uid = self::folder_id($folder)) { $this->folder_uids[$folder] = $uid; } if ($uid === $id) { $name = $folder; } } return $name; } /** */ public function modseq_set($deviceid, $folderid, $synctime, $data) { $synctime = $synctime->format('Y-m-d H:i:s'); $rcube = rcube::get_instance(); $db = $rcube->get_dbh(); $old_data = $this->modseq[$folderid][$synctime]; if (empty($old_data)) { $this->modseq[$folderid][$synctime] = $data; $data = json_encode($data); $db->set_option('ignore_key_errors', true); $db->query("INSERT INTO `syncroton_modseq` (`device_id`, `folder_id`, `synctime`, `data`)" ." VALUES (?, ?, ?, ?)", $deviceid, $folderid, $synctime, $data); $db->set_option('ignore_key_errors', false); } } public function modseq_get($deviceid, $folderid, $synctime) { $synctime = $synctime->format('Y-m-d H:i:s'); if (empty($this->modseq[$folderid][$synctime])) { $this->modseq[$folderid] = array(); $rcube = rcube::get_instance(); $db = $rcube->get_dbh(); $db->limitquery("SELECT `data`, `synctime` FROM `syncroton_modseq`" ." WHERE `device_id` = ? AND `folder_id` = ? AND `synctime` <= ?" ." ORDER BY `synctime` DESC", 0, 1, $deviceid, $folderid, $synctime); if ($row = $db->fetch_assoc()) { $synctime = $row['synctime']; // @TODO: make sure synctime from sql is in "Y-m-d H:i:s" format $this->modseq[$folderid][$synctime] = json_decode($row['data'], true); } // Cleanup: remove all records except the current one $db->query("DELETE FROM `syncroton_modseq`" ." WHERE `device_id` = ? AND `folder_id` = ? AND `synctime` <> ?", $deviceid, $folderid, $synctime); } return @$this->modseq[$folderid][$synctime]; } /** * Set state of relation objects at specified point in time */ public function relations_state_set($deviceid, $folderid, $synctime, $relations) { $synctime = $synctime->format('Y-m-d H:i:s'); $rcube = rcube::get_instance(); $db = $rcube->get_dbh(); $old_data = $this->relations[$folderid][$synctime]; if (empty($old_data)) { $this->relations[$folderid][$synctime] = $relations; $data = rcube_charset::clean(json_encode($relations)); $db->set_option('ignore_key_errors', true); $db->query("INSERT INTO `syncroton_relations_state`" ." (`device_id`, `folder_id`, `synctime`, `data`)" ." VALUES (?, ?, ?, ?)", $deviceid, $folderid, $synctime, $data); $db->set_option('ignore_key_errors', false); } } /** * Get state of relation objects at specified point in time */ public function relations_state_get($deviceid, $folderid, $synctime) { $synctime = $synctime->format('Y-m-d H:i:s'); if (empty($this->relations[$folderid][$synctime])) { $this->relations[$folderid] = array(); $rcube = rcube::get_instance(); $db = $rcube->get_dbh(); $db->limitquery("SELECT `data`, `synctime` FROM `syncroton_relations_state`" ." WHERE `device_id` = ? AND `folder_id` = ? AND `synctime` <= ?" ." ORDER BY `synctime` DESC", 0, 1, $deviceid, $folderid, $synctime); if ($row = $db->fetch_assoc()) { $synctime = $row['synctime']; // @TODO: make sure synctime from sql is in "Y-m-d H:i:s" format $this->relations[$folderid][$synctime] = json_decode($row['data'], true); } // Cleanup: remove all records except the current one $db->query("DELETE FROM `syncroton_relations_state`" ." WHERE `device_id` = ? AND `folder_id` = ? AND `synctime` <> ?", $deviceid, $folderid, $synctime); } return @$this->relations[$folderid][$synctime]; } /** * Return last storage error */ public static function last_error() { return kolab_storage::$last_error; } /** * Compares two arrays * * @param array $array1 * @param array $array2 * * @return bool True if arrays differs, False otherwise */ private static function data_array_diff($array1, $array2) { if (!is_array($array1) || !is_array($array2)) { return $array1 != $array2; } if (count($array1) != count($array2)) { return true; } foreach ($array1 as $key => $val) { if (!array_key_exists($key, $array2)) { return true; } if ($val !== $array2[$key]) { return true; } } return false; } }