config->xuanxuan->global->version) ? $this->config->xuanxuan->global->version : '1.0'; } /** * Upgrade xuanxuan. * * @param string $fromVersion * @access public * @return void */ public function upgradeXuanxuan($fromVersion) { switch($fromVersion) { case '1.0' : $this->execSQL($this->getUpgradeFile('xuanxuan1.0')); case '1.1.0' : case '1.1.1' : $this->execSQL($this->getUpgradeFile('xuanxuan1.1.1')); case '1.3.0' : $this->execSQL($this->getUpgradeFile('xuanxuan1.3.0')); case '1.4.0' : $this->execSQL($this->getUpgradeFile('xuanxuan1.4.0')); $this->processMessageStatus(); case '1.5.0' : case '1.6.0' : $this->execSQL($this->getUpgradeFile('xuanxuan1.6.0')); case '2.0.0' : $this->execSQL($this->getUpgradeFile('xuanxuan2.0.0')); case '2.1.0' : $this->execSQL($this->getUpgradeFile('xuanxuan2.1.0')); case '2.2.0' : $this->processUserStatus(); $this->processXuanxuanKey(); case '2.3.0' : $this->execSQL($this->getUpgradeFile('xuanxuan2.3.0')); case '2.4.0' : $this->execSQL($this->getUpgradeFile('xuanxuan2.4.0')); $this->changeMessageStatusTable(); case '2.5.0' : $this->execSQL($this->getUpgradeFile('xuanxuan2.5.0')); case '2.5.1' : case '2.5.2' : case '2.5.3' : case '2.5.4' : case '2.5.5' : case '2.5.6' : case '2.5.7' : $this->execSQL($this->getUpgradeFile('xuanxuan2.5.7')); case '3.0.0-beta.1' : $this->execSQL($this->getUpgradeFile('xuanxuan3.0.0-beta.1')); case '3.0 beta2' : case '3.0-beta3' : $this->execSQL($this->getUpgradeFile('xuanxuan3.0-beta3')); case '3.0.beta4' : case '3.0' : case '3.1' : case '3.1.1' : $this->execSQL($this->getUpgradeFile('xuanxuan3.1.1')); case '3.2' : case '3.2.1' : case '3.2.2' : case '3.2.3' : $this->execSQL($this->getUpgradeFile('xuanxuan3.2.3')); case '3.3' : $this->execSQL($this->getUpgradeFile('xuanxuan3.3')); $this->updateLastMessage(); $this->updateUserDevice(); case '4.0.beta1' : case '4.0.beta2' : $this->execSQL($this->getUpgradeFile('xuanxuan4.0.beta2')); case '4.0.beta3' : $this->execSQL($this->getUpgradeFile('xuanxuan4.0.beta3')); $this->dropOrderFromMessagePartitions(); case '4.0' : $this->execSQL($this->getUpgradeFile('xuanxuan4.0')); case '4.1.beta' : case '4.1' : case '4.2' : $this->execSQL($this->getUpgradeFile('xuanxuan4.2')); case '4.3' : case '4.4' : $this->execSQL($this->getUpgradeFile('xuanxuan4.4')); case '4.4.1' : case '4.5.beta1' : case '4.5' : case '4.6' : $this->execSQL($this->getUpgradeFile('xuanxuan4.6')); case '4.7' : case '5.0' : case '5.1' : $this->execSQL($this->getUpgradeFile('xuanxuan5.1')); case '5.2' : case '5.3' : case '5.3.1' : case '5.3.2' : case '5.4' : case '5.5' : $this->execSQL($this->getUpgradeFile('xuanxuan5.5')); case '5.6' : $this->execSQL($this->getUpgradeFile('xuanxuan5.6')); $this->addMessageIndexColumns(); $this->reindexMessages(); $this->populateLastReadMessageIndex(); case '6.0.beta' : $this->fixChatsWithoutLastRead(); case '6.0' : $this->transferDeletedUserGroups(); case '6.0.1' : $this->execSQL($this->getUpgradeFile('xuanxuan6.0.1')); case '6.1' : case '6.2' : case '6.3' : $this->setOwnedByForGroups(); $this->recoverCreatedDates(); $this->setPartitionedMessageIndex(); case '6.4' : $this->execSQL($this->getUpgradeFile('xuanxuan6.4')); case '6.5' : $this->execSQL($this->getUpgradeFile('xuanxuan6.5')); $this->setMuteForHiddenGroups(); $this->notifyGroupHiddenUsers(); case '6.6' : $this->execSQL($this->getUpgradeFile('xuanxuan6.6')); case '7.0' : case '7.1' : $this->execSQL($this->getUpgradeFile('xuanxuan7.1')); case '7.2.beta' : $this->execSQL($this->getUpgradeFile('xuanxuan7.2.beta')); case '7.2' : default : $this->loadModel('setting')->setItem('system.xuanxuan.global.version', isset($this->config->product) && $this->config->product == 'xxb' ? $this->config->version : $this->config->xuanxuan->version); } } /** * Change status of table user to clientStatus. * * @access public * @return bool */ public function processUserStatus() { $hasStatus = false; $hasClientStatus = false; $fields = $this->dbh->query('DESC ' . TABLE_USER)->fetchAll(); foreach($fields as $field) { if($field->Field == 'status') $hasStatus = true; if($field->Field == 'clientStatus') $hasClientStatus = true; } if($hasStatus && !$hasClientStatus) { try { $this->dbh->exec('ALTER TABLE ' . TABLE_USER . "CHANGE `status` `clientStatus` enum('online', 'away', 'busy', 'offline') NOT NULL DEFAULT 'offline'"); } catch (PDOException $e) { return false; } } return true; } /** * Process message status. * * @access public * @return bool */ public function processMessageStatus() { $table = $this->dbh->query("SHOW TABLES LIKE '{$this->config->db->prefix}im_usermessage'")->fetch(); if(empty($table)) return false; $userMessages = array(); $messagesList = $this->dao->select('*')->from($this->config->db->prefix . 'im_usermessage')->fetchAll(); foreach($messagesList as $messages) { $user = $messages->user; $messages = json_decode($messages->message); foreach($messages as $message) { if(isset($userMessages[$user][$message->gid])) continue; $data = new stdClass(); $data->user = $user; $data->gid = $message->gid; $data->status = 'waiting'; $this->dao->insert(TABLE_IM_MESSAGESTATUS)->data($data)->exec(); $userMessages[$user][$message->gid] = $message->gid; } } return !dao::isError(); } /** * Process key of xuanxuan. * * @access public * @return bool */ public function processXuanxuanKey() { $this->loadModel('setting')->setItem('system.common.xuanxuan.key', $this->config->xuanxuan->key); $this->setting->deleteItems('owner=system&module=xuanxuan&key=key'); return !dao::isError(); } /** * Fix the history and Change messagestatus table. * @return bool */ public function changeMessageStatusTable() { $prefix = $this->config->db->prefix; $errorTable = $this->dbh->query("show tables like '{$prefix}im_messsagestatus'")->fetch(); if(!empty($errorTable)) $this->dbh->query("RENAME TABLE `{$prefix}im_messsagestatus` TO `{$prefix}im_messagestatus`"); $needUpdate = false; $fields = $this->dbh->query('DESC ' . TABLE_IM_MESSAGESTATUS)->fetchAll(); foreach($fields as $field) { if($field->Field == 'gid') { $needUpdate = true; break; } } if(!$needUpdate) return false; $gids = $this->dao->select('gid')->from(TABLE_IM_MESSAGESTATUS)->where('status')->ne('sent')->fetchPairs('gid'); if(!empty($gids)) { $messages = $this->dao->select('gid, id')->from(TABLE_IM_MESSAGE)->where('gid')->in($gids)->fetchPairs(); foreach($messages as $gid => $message) { $this->dao->update(TABLE_IM_MESSAGESTATUS)->set('message')->eq($message)->where('gid')->eq($gid)->exec(); } } $this->dao->delete()->from(TABLE_IM_MESSAGESTATUS)->where('status')->eq('sent')->exec(); $userIndex = $this->dbh->query("show index from " . TABLE_IM_MESSAGESTATUS . " where Key_name = 'user'")->fetch(); if($userIndex) $this->dbh->exec('ALTER TABLE ' . TABLE_IM_MESSAGESTATUS . ' DROP INDEX `user`;'); $this->dbh->exec('ALTER TABLE ' . TABLE_IM_MESSAGESTATUS . ' DROP `gid`;'); $this->dbh->exec('ALTER TABLE ' . TABLE_IM_MESSAGESTATUS . ' ADD UNIQUE INDEX `user` (`user`, `message`);'); return !dao::isError(); } /** * Update lastMessage of im_chat table. * * @access public * @return bool */ public function updateLastMessage() { $prefix = $this->config->db->prefix; $lastMessageData = array(); $lastMessages = $this->dbh->query("SELECT MAX({$prefix}im_message.id),{$prefix}im_chat.id FROM {$prefix}im_message LEFT JOIN {$prefix}im_chat ON {$prefix}im_message.cgid = {$prefix}im_chat.gid GROUP BY {$prefix}im_chat.gid;")->fetchAll(); if(empty($lastMessages)) return true; foreach($lastMessages as $lastMessage) { $cid = $lastMessage->id; $mid = $lastMessage->{"MAX({$prefix}im_message.id)"}; if(empty($cid) || empty($mid)) continue; $lastMessageData[] = "($cid,$mid)"; } if(empty($lastMessageData)) return true; $query = "INSERT INTO `{$prefix}im_chat` (`id`,`lastMessage`) VALUES " . join(',', $lastMessageData) . " ON DUPLICATE KEY UPDATE `lastMessage`=VALUES(`lastMessage`)"; $this->dbh->query($query); return !dao::isError(); } /** * Move data into im_userdevice table. * * @access public * @return bool */ public function updateUserDevice() { $prefix = $this->config->db->prefix; $userDeviceData = array(); $userDevices = $this->dbh->query("SELECT " . TABLE_CONFIG . ".`value`," . TABLE_USER . ".`id` FROM " . TABLE_CONFIG . " LEFT JOIN " . TABLE_USER . " ON " . TABLE_CONFIG . ".`owner` = " . TABLE_USER . ".`account` WHERE " . TABLE_CONFIG . ".`section` = 'lastLogin' AND " . TABLE_CONFIG . ".`key` = 'desktop' GROUP BY " . TABLE_CONFIG . ".`owner`;")->fetchAll(); if(empty($userDevices)) return true; foreach($userDevices as $userDevice) { $user = $userDevice->id; $lastLogin = $userDevice->value; if(empty($user) || empty($lastLogin)) continue; $userDeviceData[] = "($user,'$lastLogin','$lastLogin')"; } if(empty($userDeviceData)) return true; $query = "INSERT INTO `{$prefix}im_userdevice` (`user`,`lastLogin`,`lastLogout`) VALUES " . join(',', $userDeviceData); $this->dbh->query($query); $this->dbh->query("DELETE FROM " . TABLE_CONFIG . " WHERE `section` = 'lastLogin';"); return !dao::isError(); } /** * Drop order column from all message partition tables. * * @access public * @return bool */ public function dropOrderFromMessagePartitions() { $tables = $this->dbh->query("SHOW TABLES LIKE '{$this->config->db->prefix}im_message\_%'")->fetchAll(); $tables = array_filter(array_map(function($table) { $tableName = current(array_values((array)$table)); if(!preg_match("/{$this->config->db->prefix}im_message_[a-z]+/", $tableName)) return $tableName; }, $tables)); if(empty($tables)) return true; $query = ''; foreach($tables as $table) { $query .= "ALTER TABLE `$table` DROP COLUMN `order`;"; } $this->dbh->query($query); return !dao::isError(); } /** * Add `index` column to all message partition tables. * * @access public * @return bool */ public function addMessageIndexColumns() { $prefix = $this->config->db->prefix; $tables = $this->dbh->query("SHOW TABLES LIKE '{$prefix}im_message\_%'")->fetchAll(); $tables = array_filter(array_map(function($table) use ($prefix) { $tableName = current(array_values((array)$table)); if(!preg_match("/{$prefix}im_message_[a-z]+/", $tableName)) return $tableName; }, $tables )); if(empty($tables)) return true; $query = ''; foreach($tables as $table) $query .= "ALTER TABLE `$table` ADD `index` int(11) unsigned DEFAULT 0 AFTER `date`;"; $this->dbh->query($query); return !dao::isError(); } /** * Re-index messages. * * @access public * @return bool */ public function reindexMessages() { /** @var array[] $chatTablePairs Associations of chats and partition tables, without main table. */ $chatTablePairs = array(); ini_set('memory_limit', -1); set_time_limit(0); /* Fetch chat and message partition table associations. */ $chatTableData = $this->dao->select('gid,tableName')->from(TABLE_IM_CHAT_MESSAGE_INDEX)->orderBy('id_asc')->fetchAll(); foreach($chatTableData as $chatTable) { if(isset($chatTablePairs[$chatTable->gid])) { $chatTablePairs[$chatTable->gid][] = $chatTable->tableName; continue; } $chatTablePairs[$chatTable->gid] = array($chatTable->tableName); } /* Append all non-partitioned chats. */ $allChats = $this->dao->select('gid')->from(TABLE_IM_CHAT)->fetchPairs(); $nonPartitionedChats = array_diff(array_values($allChats), array_keys($chatTablePairs)); foreach($nonPartitionedChats as $chat) $chatTablePairs[$chat] = array(); /* Do index. */ foreach($chatTablePairs as $chat => $tables) { $result = $this->doIndex($chat, $tables); if(!$result) return false; } return true; } /** * Index messages of chat in partition tables and main table. * * @param string $chat * @param array $tables * @return bool */ public function doIndex($chat, $tables) { $messageIndex = 0; $tables[] = str_replace('`', '', TABLE_IM_MESSAGE); foreach($tables as $table) { $idIndices = array(); $ids = $this->dao->select('id')->from("`$table`")->where('cgid')->eq($chat)->fetchAll('id'); $ids = array_keys($ids); if(empty($ids)) continue; for($index = 1; $index <= count($ids); $index++) $idIndices[$ids[$index - 1]] = $index + $messageIndex; $queryData = array(); foreach($idIndices as $id => $index) $queryData[] = "WHEN $id THEN $index"; $query = "UPDATE `$table` SET `index` = (CASE `id` " . join(' ', $queryData) . " END) WHERE `id` IN(" . join(',', $ids) . ");"; $this->dao->query($query); $messageIndex = max(array_values($idIndices)); } $this->dao->update(TABLE_IM_CHAT)->set('lastMessageIndex')->eq($messageIndex)->where('gid')->eq($chat)->exec(); return !dao::isError(); } /** * Set lastReadMessageIndex into table im_chatuser. * * @access public * @return bool */ public function populateLastReadMessageIndex() { $lastReadMessages = $this->dao->select('lastReadMessage')->from(TABLE_IM_CHATUSER)->where('lastReadMessage')->ne(0)->fetchAll('lastReadMessage'); $lastReadMessages = array_keys($lastReadMessages); if(empty($lastReadMessages)) return true; ini_set('memory_limit', -1); set_time_limit(0); $messages = $this->loadModel('im')->messageGetList('', $lastReadMessages, null, '', '', false); if(empty($messages)) return; $queryData = array(); foreach($messages as $message) $queryData[] = "WHEN {$message->id} THEN {$message->index}"; $query = "UPDATE " . TABLE_IM_CHATUSER . " SET `lastReadMessageIndex` = (CASE `lastReadMessage` " . join(' ', $queryData) . " END) WHERE `id` IN(" . join(',', $lastReadMessages) . ");"; $this->dao->query($query); return !dao::isError(); } /** * Set index range for chats in chat partition index table. * * @access public * @return bool */ public function setPartitionedMessageIndex() { ini_set('memory_limit', -1); set_time_limit(0); /* Fetch chat and message partition table associations with message range. */ $chatTableData = $this->dao->select('gid, tableName, start, end')->from(TABLE_IM_CHAT_MESSAGE_INDEX) ->where('startIndex')->eq(0) ->orWhere('endIndex')->eq(0) ->orderBy('id_asc') ->fetchAll(); /* Sort ranges by table. */ $tableRanges = array(); foreach($chatTableData as $chatTable) { if(isset($tableRanges[$chatTable->tableName])) { $tableRanges[$chatTable->tableName]->start[] = $chatTable->start; $tableRanges[$chatTable->tableName]->end[] = $chatTable->end; continue; } $tableRanges[$chatTable->tableName] = (object)array('start' => array($chatTable->start), 'end' => array($chatTable->end)); } /* Query corresponding message indice. */ foreach($tableRanges as $tableName => $tableRange) { $ids = array_merge($tableRange->start, $tableRange->end); $ids = array_unique($ids); $indexPairs = $this->dao->select('id, `index`')->from("`$tableName`") ->where('id')->in($ids) ->fetchPairs('id'); $tableRanges[$tableName]->indexPairs = $indexPairs; } /* Set startIndice and endIndice. */ foreach($tableRanges as $tableRange) { $queryData = array(); foreach($tableRange->start as $id) $queryData[] = "WHEN $id THEN {$tableRange->indexPairs[$id]}"; $query = "UPDATE " . TABLE_IM_CHAT_MESSAGE_INDEX . " SET `startIndex` = (CASE `start` " . join(' ', $queryData) . " END) WHERE `start` IN(" . join(',', $tableRange->start) . ");"; $this->dao->query($query); $queryData = array(); foreach($tableRange->end as $id) $queryData[] = "WHEN $id THEN {$tableRange->indexPairs[$id]}"; $query = "UPDATE " . TABLE_IM_CHAT_MESSAGE_INDEX . " SET `endIndex` = (CASE `end` " . join(' ', $queryData) . " END) WHERE `end` IN(" . join(',', $tableRange->end) . ");"; $this->dao->query($query); } } /** * Fix chats without lastReadMessage. * * @access public * @return bool */ public function fixChatsWithoutLastRead() { $zeroLastReadChats = $this->dao->select('cgid')->from(TABLE_IM_CHATUSER)->where('lastReadMessage')->eq(0)->fetchAll('cgid'); $zeroLastReadChats = array_keys($zeroLastReadChats); if(empty($zeroLastReadChats)) return true; ini_set('memory_limit', -1); set_time_limit(0); $lastMessages = $this->dao->select('MAX(`index`), cgid')->from(TABLE_IM_MESSAGE)->where('cgid')->in($zeroLastReadChats)->groupBy('cgid')->fetchAll('cgid'); if(empty($lastMessages)) return true; $maxIndex = 'MAX(`index`)'; $queryData = array(); foreach($lastMessages as $cgid => $lastMessage) $queryData[] = "WHEN '{$cgid}' THEN {$lastMessage->$maxIndex}"; $query = "UPDATE " . TABLE_IM_CHATUSER . " SET `lastReadMessageIndex` = (CASE `cgid` " . join(' ', $queryData) . " END) WHERE `cgid` IN('" . join("','", array_keys($lastMessages)) . "');"; $this->dao->query($query); return !dao::isError(); } /* *Transfer chat groups if users were deleted * * @access public * @return bool */ public function transferDeletedUserGroups() { $deletedUsers = $this->dao->select('id') ->from(TABLE_USER) ->where('deleted')->eq('1') ->fetchAll(); if(!empty($deletedUsers)) { $this->loadModel('im'); foreach($deletedUsers as $user) { $this->im->chatTransferAllFromUser($user->id); } } return !dao::isError(); } /** * Set ownedBy for group chats without it. * * @access public * @return bool */ public function setOwnedByForGroups() { $this->dao->update(TABLE_IM_CHAT)->set('ownedBy = createdBy')->where('ownedBy')->eq('')->exec(); return !dao::isError(); } /** * Recover created date for chats. * * @access public * @return bool */ public function recoverCreatedDates() { $chats = $this->dao->select('gid, id')->from(TABLE_IM_CHAT) ->where('createdDate')->eq('0000-00-00 00:00:00') ->fetchPairs('gid'); $createdDateData = array(); /* Try query earliest message date indexed. */ $indexedMinDates = $this->dao->select('gid, MIN(startDate)')->from(TABLE_IM_CHAT_MESSAGE_INDEX) ->where('gid')->in(array_keys($chats)) ->groupBy('gid') ->fetchPairs('gid'); /* Then try query earliest message date non-indexed from master table. */ $queryChats = array_diff(array_keys($chats), array_keys($indexedMinDates)); $minDates = $this->dao->select('cgid, MIN(date)')->from(TABLE_IM_MESSAGE) ->where('cgid')->in($queryChats) ->groupBy('cgid') ->fetchPairs('cgid'); $knownMinDates = array_merge($indexedMinDates, $minDates); $remainingChats = $chats; foreach($chats as $cgid => $cid) { if(isset($knownMinDates[$cgid])) { $createdDateData[$cid] = $knownMinDates[$cgid]; unset($remainingChats[$cgid]); } } /* Use other dates for chats without messages. */ $chatDates = $this->dao->select('id, gid, editedDate, lastActiveTime, dismissDate')->from(TABLE_IM_CHAT) ->where('gid')->in(array_keys($remainingChats)) ->fetchAll('gid'); $chatDates = array_map(function($chatDate) { $dates = array_filter(array($chatDate->editedDate, $chatDate->lastActiveTime, $chatDate->dismissDate), function($date) { return $date != '0000-00-00 00:00:00'; }); $minDate = min($dates); return $minDate; }, $chatDates); $knownMinDates = array_merge($knownMinDates, $chatDates); $queryData = array(); foreach($knownMinDates as $gid => $date) $queryData[] = "WHEN {$chats[$gid]} THEN '{$date}'"; $query = "UPDATE " . TABLE_IM_CHAT . " SET `createdDate` = (CASE `id` " . join(' ', $queryData) . " END) WHERE `id` IN(" . join(",", array_values($chats)) . ");"; $this->dao->query($query); return !dao::isError(); } /** * Set mute and freeze for hidden groups. * * @access public * @return bool */ public function setMuteForHiddenGroups() { $this->dao->update(TABLE_IM_CHATUSER)->set('hide')->eq('0')->set('mute')->eq('1')->set('freeze')->eq('1')->where('hide')->eq('1')->andWhere('quit')->eq('0000-00-00 00:00:00')->exec(); return !dao::isError(); } /** * Notify users who have hide the group. * * @access public * @return bool */ public function notifyGroupHiddenUsers() { $noticeUsers = $this->dao->select('user')->from(TABLE_IM_CHATUSER)->where('hide')->eq('1')->andWhere('quit')->eq('0000-00-00 00:00:00')->groupBy('user')->fetchAll(); $sender = new stdClass(); $sender->avatar = commonModel::getSysURL() . '/www/favicon.ico'; $sender->id = 'upgradeArchive'; $sender->realname = $this->lang->upgrade->archiveChangeNoticeTitle; $this->loadModel('im')->messageCreateNotify(array_keys($noticeUsers), '', '', $this->lang->upgrade->archiveChangeNoticeContent, 'text', '', array(), $sender); return !dao::isError(); }