* @package im * @license ZOSL (http://zpl.pub/page/zoslv1.html) * @version $Id$ * @Link http://xuan.im */ class imModel extends model { /** * Sub-model list, these models are stored at im/model/. * * @access public */ public $models = array('chat', 'message', 'user', 'conference', 'bot'); /** * __construct loads and inits sub-models. * * @access public * @return void */ public function __construct() { parent::__construct(); if((isset($_SERVER['RR_RELAY']) || isset($_SERVER['RR_MODE'])) && !commonModel::isLicensedMethod('im', 'roadrunner')) die; $modelPath = dirname(__FILE__) . DS . "model" . DS; foreach($this->models as $model) { helper::import($modelPath . "$model.php"); $className = "im$model"; $this->$model = new $className($this->appName, $this); } } /** * Get apiScheme info. * * @param string $key * @access public * @return mixed */ public function getApiScheme($key = '') { $schemeFile = $this->app->getExtensionRoot() . 'xuan/im/apischeme.json'; $scheme = json_decode(trim(file_get_contents($schemeFile)), true); if(!$key) return $scheme; return zget($scheme, $key, ''); } /** * Send formatted output. * * @param array|object $output Example: array('result' => 'success', 'data' => $messages, 'users' => $userIdList, ...); * @param string $schemeName * @access public * @return void */ public function sendOutput($output, $schemeName = 'RAW') { $output = $this->formatOutput($output, $schemeName); return $this->app->output($this->app->encrypt($output)); } /** * Send formatted output group. * * @param array|object $outputs Example: array('result' => 'success', 'data' => $messages, 'users' => $userIdList, ...); * @access public * @return void */ public function sendOutputGroup($outputs) { $formatted = array(); foreach($outputs as $output) { if(!isset($firstOutput)) $firstOutput = $output; if(!isset($output->method)) { $stack = debug_backtrace(false, 2); $output->method = isset($stack[1]) ? $stack[1]['function'] : ''; } $formatted[] = $this->formatOutput($output, strtolower($output->method) . 'Response', $returnRaw = true); } $userID = isset($firstOutput->userID) ? $firstOutput->userID : 0; $method = isset($firstOutput->method) ? $firstOutput->method : ''; $result = isset($firstOutput->result) ? $firstOutput->result : 'success'; $finalOutput = $this->appendResponseHeader($formatted, $userID, $firstOutput->users, $method, $result); return $this->app->output($this->app->encrypt($finalOutput)); } /** * Format output data. * * @param array|object $data Example: array('result' => 'success', 'data' => $messages, 'users' => $userIdList, ...); * @param string $map * @param bool $returnRaw * @param bool $prependName * @access public * @return object|string */ public function formatOutput($data, $map = 'RAW', $returnRaw = false, $prependName = true) { if(!empty($this->app->debug)) $this->app->log("format output($map): " . json_encode($data)); $output = new stdclass(); foreach($data as $key => $value) { if($key == 'users' && !is_array($value)) $value = array((int) $value); $output->$key = $value; } $output->device = zget($this->app->input, 'device', 'desktop'); $output->userID = zget($data, 'userID', '0'); $output->result = zget($data, 'result', 'success'); $output->method = zget($data, 'method', $this->app->getMethodName()); $output->rid = zget($this->app->input, 'rid'); if($map != 'RAW') { $maps = zget($this->config->maps, $map, array()); /* Using requestPack as fallback maps */ if(empty($maps)) $maps = zget($this->config->maps, 'responsePack'); $data = self::encodeOutput($output, $maps); } else { $map = strtolower($output->method) . 'Response'; } $data = $prependName ? array($map, $data) : $data; if(!empty($this->app->debug)) $this->app->log("encoded output($map): " . json_encode($data)); if($returnRaw) return $data; $users = isset($output->users) ? $output->users : array(); return $this->appendResponseHeader($data, $output->userID, $users, $output->method, $output->result); } /** * Encode output. * * @param object $output * @param object|string|boolean $map * @static * @access public * @return array|object */ public static function encodeOutput($output, $map) { if(empty($map)) return $output; $output = (array) $output; /* If map is not final map array, decode with map's dataType setting.*/ if(isset($map['name']) and isset($map['dataType'])) $map = $map['dataType']; if(isset($map['name']) and !isset($map['dataType'])) $map = array($map); $data = array(); foreach($map as $key => $prop) { $indexName = $prop['name']; if($prop['type'] == 'basic') { if(isset($prop['options'])) { $options = array_flip($prop['options']); $data[$key] = zget($options, zget($output, $indexName, '')); } else { $data[$key] = zget($output, $indexName, ''); } continue; } if($prop['type'] == 'object') { if(isset($output[$indexName])) $data[$key] = self::encodeOutput($output[$indexName], $prop['dataType']); continue; } if($prop['type'] == 'list') { $tmpOutput = array(); if(isset($output[$indexName])) { foreach($output[$indexName] as $item) { $tmpOutput[] = self::encodeOutput($item, $prop['dataType']); } } $data[$key] = $tmpOutput; } } return $data; } /** * Batch encode output. * * @param array $array * @param bool|object|string $map * @static * @access public * @return array */ public static function batchEncodeOutput($array, $map) { $data = array(); if(empty($array)) return $data; foreach($array as $output) $data[] = self::encodeOutput($output, $map); return $data; } /** * Append header for xxd to response. * * @param array $output * @param string $from current user id * @param string|int|array $to id list of users to notify : 123,2,3,4,76,423 * @param string $method * @param string $result * @access public * @return string */ public function appendResponseHeader($output, $from, $to = 0, $method = '', $result = 'success') { if(empty($to)) $to = $this->app->input['userID']; elseif(is_array($to)) $to = implode(',', $to); if(!$method) $method = strtolower($this->app->getMethodName()); $device = $this->app->input['device']; $lang = $this->app->input['lang']; if(!isset($from) || empty($from)) $from = $this->app->input['userID']; $response = "{$to}\n"; $response .= "{$from}\n"; $response .= "$method\n"; $response .= "success\n"; $response .= "{$device}\n"; $response .= "{$lang}\n"; if(is_array($output) && !is_string($output[0])) { foreach($output as $op) $response .= json_encode($op) . "\n"; $response = trim($response); } else { $response .= json_encode($output); } return $response; } /** * Get output data of user list. * * @param array $identities * @param int $userID * @param bool $returnRaw * @access public * @return object */ public function getUserListOutput($identities, $userID, $returnRaw = false) { $output = new stdclass(); $users = $this->userGetList($status = '', $identities, $idAsKey = false); if(dao::isError()) { $output->result = 'fail'; $output->message = 'Get userlist failed.'; return $this->formatOutput($output, 'messageResponsePack', $returnRaw); } else { $output->result = 'success'; $output->users = $userID; $output->data = $users; $output->method = 'usergetlist'; if(empty($identities)) { $this->app->loadLang('user'); $roles = $this->lang->user->roleList; $allDepts = $this->loadModel('dept')->getListByType('dept'); $depts = array(); foreach($allDepts as $id => $dept) { $depts[$id] = array('name' => $dept->name, 'order' => (int)$dept->order, 'parent' => (int)$dept->parent); } $output->roles = $roles; $output->depts = $depts; } else { $output->partial = $identities; } } return $this->formatOutput($output, 'usergetlistResponse', $returnRaw); } /** * Get output data of chat list. * * @param int $userID * @param bool $returnRaw * @param string $chatList * @access public * @return object */ public function getChatListOutput($userID, $returnRaw = false, $chatList = '') { if(empty($chatList)) $chatList = $this->chatGetListByUserID($userID); if(dao::isError()) return $this->formatOutput(array('result' => 'fail', 'message' => 'Get chat list fail.'), 'messageResponsePack', $returnRaw); return $this->formatOutput(array('result' => 'success', 'method' => 'chatgetlist', 'data' => $chatList, 'users' => $userID), 'chatgetlistResponse', $returnRaw); } /** * Get output data of offline messages. * * @param int $userID * @param bool $returnRaw * @access public * @return object */ public function getOfflineMessagesOutput($userID, $returnRaw = false) { $messages = $this->messageGetOfflineList($userID); if(empty($messages)) return $this->formatOutput(array('result' => 'fail', 'message' => 'Get offline messages list fail.'), 'messageResponsePack', $returnRaw); if(dao::isError()) return $this->formatOutput(array('result' => 'fail', 'message' => 'Get offline messages list fail.'), 'messageResponsePack', $returnRaw); return $this->formatOutput(array('result' => 'success', 'method' => 'messagesend', 'data' => $messages, 'users' => $userID), 'messagesendResponse', $returnRaw); } /** * Get output data of offline notify. * * @param int $userID * @param bool $returnRaw * @access public * @return object */ public function getOfflineNotifyOutput($userID, $returnRaw = false) { $messages = $this->messageGetNotifyByUserID($userID); if(empty($messages)) return null; if(dao::isError()) return $this->formatOutput(array('result' => 'fail', 'message' => 'Get offline notification list fail.'), 'messageResponsePack', $returnRaw); return $this->formatOutput(array('result' => 'success', 'method' => 'syncnotifications', 'data' => $messages, 'users' => $userID), 'syncnotificationsResponse', $returnRaw); } /** * Get output data of open conferences. * * @param int $userID * @param boolean $returnRaw * @param string $userChatList * @access public * @return object */ public function getOpenConferencesOutput($userID, $returnRaw = false, $userChatList = '', $ignoreActions = false) { if(empty($userChatList)) $userChatList = $this->chatGetListByUserID($userID); $conferences = $this->conferenceGetOpenConferencesByChatList($userChatList, $userID, $ignoreActions); if(empty($conferences)) return null; if(dao::isError()) return $this->formatOutput(array('result' => 'fail', 'message' => 'Get open conferences list fail.'), 'messageResponsePack', $returnRaw); return $this->formatOutput(array('result' => 'success', 'method' => 'syncconferences', 'data' => $conferences, 'users' => $userID), 'syncconferencesResponse', $returnRaw); } /** * Create gid. * @access public * @return string */ public static function createGID() { $id = md5(microtime() . mt_rand()); return substr($id, 0, 8) . '-' . substr($id, 8, 4) . '-' . substr($id, 12, 4) . '-' . substr($id, 16, 4) . '-' . substr($id, 20, 12); } /** * Download xxd. * * @param object $setting * @param string $downloadType * @access public * @return array */ public function downloadXXD($setting, $downloadType) { set_time_limit(0); $system = $this->getSystem($setting->os); $version = $this->config->xuanxuan->version; $xxdDirectory = $this->app->tmpRoot . 'xxd' . DS . $version; $basePackage = $xxdDirectory . DS . $system . ".base.zip"; $xxdFileName = 'xxd.' . $version . ".$system" . ".zip"; $downloadCDNLink = $this->config->im->xxdDownloadUrl . $version . "/xxd." . $version . ".$system" . ".zip"; if(!is_dir($xxdDirectory)) mkdir($xxdDirectory, 0777, true); if(!file_exists($basePackage) && $downloadType == 'package') { $agent = $this->app->loadClass('snoopy'); $agent->fetch($downloadCDNLink); $error = json_decode($agent->results)->error; if(!empty($error)) return array('result' => 'fail', 'message' => "$basePackage is not exists"); $fopenPackage = fopen($basePackage, "w"); fwrite($fopenPackage, $agent->results); } $data = new stdClass(); $data->xxdDirectory = $xxdDirectory; $data->sslcrt = $setting->sslcrt; $data->sslkey = $setting->sslkey; $data->basePackage = $basePackage; $data->xxdFileName = $xxdFileName; $data->host = trim($this->getServer(), '/') . (zget($this->config->xuanxuan, 'backend', 'xxb') == 'ranzhi' ? dirname($this->config->webRoot) : $this->config->webRoot); $data->ip = $setting->ip ?: '0.0.0.0'; $data->commonPort = $setting->commonPort ?: '11443'; $data->chatPort = $setting->chatPort ?: '11444'; $data->https = $setting->https ?: 'on'; $data->enableAES = $setting->aes == 'off' ? 0 : 1; $data->uploadPath = 'files/'; $data->uploadFileSize = $setting->uploadFileSize ?: '20'; $data->pollingInterval = isset($this->config->xuanxuan->pollingInterval) ? $this->config->xuanxuan->pollingInterval : 15; $data->maxOnlineUser = isset($setting->maxOnlineUser) ? $setting->maxOnlineUser : 0; $data->logPath = 'log/'; $data->certPath = 'cert/'; $data->debug = 0; $data->key = $this->config->xuanxuan->key; $data->syncConfig = 1; $data->thumbnail = 1; if($downloadType == 'config') { $configContent = $this->createXxdConfigFile($data); if(!empty($configContent)) $this->loadModel('file')->sendDownHeader('xxd.conf', 'conf', $configContent['zh']); } elseif($downloadType == 'package') { $packageFileName = $this->createXxdPackage($data); if(!empty($packageFileName)) return array('result' => 'success', 'message' => helper::createLink('im', 'downloadXxdPackage', "xxdFileName=$xxdFileName")); } return array('result' => 'fail', 'message' => 'error'); } /** * create xxd config file * * @param object $setting * @access public * @return array */ public function createXxdConfigFile($setting) { $configParamsList = $this->config->im->xxdConfig; // Replace template variable. $lineMaxLength = 0; foreach($configParamsList as $configParams) { if($configParams == 'host' || $configParams == 'key') { $config[$configParams] = $setting->$configParams; } elseif(strpos($configParams, '=') !== false) { $configItem = explode('=', $configParams); $config[$configItem[0]] = $configItem[0] . '=' . $configItem[1]; } else { $config[$configParams] = $configParams . '=' . $setting->$configParams; if($configParams == 'uploadFileSize') $config[$configParams] .= 'M'; } $lineMaxLength = strlen($configParams) > $lineMaxLength ? strlen($configParams) : $lineMaxLength; } $lineMaxLength += 10; // Add parameter notes $contentZH = '[server]' . "\n"; $contentEN = '[server]' . "\n"; foreach($config as $type => $configValue) { if($type == 'host' || $type == 'key') continue; $configValue = str_replace(PHP_EOL, '', $configValue); $configlength = strlen($configValue); for($i = 0; $i < ($lineMaxLength - $configlength); $i++) $configValue .= ' '; $contentZH .= $configValue . $this->lang->im->xxdConfigNote['zh'][$type] . "\n"; $contentEN .= $configValue . $this->lang->im->xxdConfigNote['en'][$type] . "\n"; } // Add backend $backend = "\n" . '[backend]' . "\n"; $backendFoot = 'default=' . $config['host'] . 'x.php,' . $config['key']; $backendFoot = str_replace(PHP_EOL, '', $backendFoot) . "\n"; $backendZH = $backend . $this->lang->im->xxdConfigNote['zh']['backend'] . "\n" . $backendFoot; $backendEN = $backend . $this->lang->im->xxdConfigNote['en']['backend'] . "\n" . $backendFoot; $contentZH .= $backendZH; $contentEN .= $backendEN; return array('zh' => $contentZH, 'en' => $contentEN); } /** * create xxd package * * @param object * @access public * @return string */ public function createXxdPackage($setting) { $configContent = $this->createXxdConfigFile($setting); if(empty($configContent)) return false; // unzip package $this->app->loadClass('pclzip', true); $basePackage = new pclzip($setting->basePackage); $result = $basePackage->extract( PCLZIP_OPT_PATH, $setting->xxdDirectory ); if($result == 0) $basePackage->errorInfo(true); // Replace config file. $baseFilePath = $result[0]['filename']; $packageName = $result[0]['stored_filename']; unlink($baseFilePath . 'config/xxd.conf'); unlink($baseFilePath . 'config/xxd.en.conf'); file_put_contents($baseFilePath . 'config/xxd.conf', $configContent['zh']); file_put_contents($baseFilePath . 'config/xxd.en.conf', $configContent['en']); // https add certificate if(isset($setting->https) && $setting->https == 'on') { if(!is_dir($baseFilePath . 'cert')) mkdir($baseFilePath . 'cert', 0777); file_put_contents($baseFilePath . 'cert/xxd.crt', $setting->sslcrt); file_put_contents($baseFilePath . 'cert/xxd.key', $setting->sslkey); } // zip xxd file chdir($setting->xxdDirectory); $xxdZipName = $setting->xxdDirectory . "/" . $setting->xxdFileName; $xxdZip = new pclzip($xxdZipName); $xxdResult = $xxdZip->create($packageName, PCLZIP_OPT_TEMP_FILE_ON); if($xxdResult == 0) return false; return $xxdResult[0]['filename']; } /** * revise operating system name. * * @param string $os name * @access public * @return string */ public function getSystem($os) { return zget($this->config->im->osMap, $os, 'win64'); } /** * Get server. * * @access public * @return string */ public function getServer() { if(!empty($this->config->xuanxuan->server)) return $this->config->xuanxuan->server; return commonModel::getSysURL(); } /** * UploadFile a file. * * @param string $fileName * @param string $path * @param int $size * @param int $time * @param int $userID * @param string $users * @param object $chat * @access public * @return int */ public function uploadFile($fileName, $path, $size, $time, $userID, $users, $chat) { $user = $this->userGetByID($userID); $extension = $this->loadModel('file')->getExtension($fileName); // if file has no extension or is "danger",return "txt, but $fileName is the origin file name" $file = new stdclass(); $file->pathname = $path; $file->title = preg_replace("/\.$extension$/", '', $fileName); $file->extension = $extension; $file->size = $size; $file->objectType = 'chat'; $file->objectID = $chat->id; $file->createdBy = !empty($user->account) ? $user->account : ''; $file->createdDate = date(DT_DATETIME1, $time); $this->dao->insert(TABLE_FILE)->data($file)->exec(); $fileID = $this->dao->lastInsertID(); $path .= md5($fileName . $fileID . $time); $this->dao->update(TABLE_FILE)->set('pathname')->eq($path)->where('id')->eq($fileID)->exec(); return $fileID; } /** * Save xxd start time. * * @access public * @return bool */ public function setXxdStartTime() { $this->loadModel('setting')->setItem('system.common.xxd.start', helper::now()); return !dao::isError(); } /** * update last poll. * * @access public * @return void */ public function updateLastPoll() { $this->loadModel('setting')->setItem('system.common.xxd.lastPoll', helper::now()); } /** * check xxb config. * * @access public * @return bool */ public function checkXXBConfig() { $xxbConfig = $this->config->xuanxuan; $notEmptyFields = array('key', 'server', 'ip', 'chatPort', 'commonPort'); foreach($notEmptyFields as $field) if(empty($xxbConfig->$field)) return false; if($xxbConfig->https == 'on' && (empty($xxbConfig->sslcrt) || empty($xxbConfig->sslkey))) return false; return true; } /** * Get xxd run time. * * @param int $timestamp * @param int $count * @access public * @return string */ public function getXxdRunTime($timestamp, $count = 0) { if($count > 1) return ''; if($timestamp > 86400) { return floor($timestamp / 86400) . $this->lang->im->day . $this->getXxdRunTime($timestamp%86400, ++$count); } else if($timestamp > 3600) { return floor($timestamp / 3600) . $this->lang->im->hours . $this->getXxdRunTime($timestamp%3600, ++$count); } else if($timestamp > 60) { return floor($timestamp / 60) . $this->lang->im->minute . $this->getXxdRunTime($timestamp%60, ++$count); } else { return $timestamp . $this->lang->im->secs; } } /** * Get xxd status. * * @access public * @return string */ public function getXxdStatus() { $this->app->loadLang('client'); $now = helper::now(); $xxdStatus = 'offline'; $polling = empty($this->config->xuanxuan->pollingInterval) ? 60 : $this->config->xuanxuan->pollingInterval; $lastPoll = $this->loadModel('setting')->getItem("owner=system&module=common§ion=xxd&key=lastPoll"); $xxdStartDate = zget($this->config->xxd, 'start', $this->lang->client->noData); if((strtotime($now) - strtotime($xxdStartDate) < $polling) || (strtotime($now) - strtotime($lastPoll)) < (3 + $polling)) { $xxdStatus = 'online'; } else if((strtotime($now) - strtotime($lastPoll)) > (3 + $polling)) { $xxdStatus = 'offline'; } return $xxdStatus; } /** * Get signed time. * Other program can extend this function. * * @param string $account * @access public * @return string | int */ public function getSignedTime($account = '') { return 0; } /** * Get extension list. * @param $userID * @return array */ public function getExtensionList($userID) { $entries = array(); $allEntries = array(); $time = time(); $baseURL = commonModel::getSysURL(); $entryList = $this->dao->select('*')->from(TABLE_ENTRY)->orderBy('`order`, id')->fetchAll(); $files = $this->dao->select('id, pathname, objectID')->from(TABLE_FILE)->where('objectType')->eq('entry')->fetchAll('objectID'); foreach($entryList as $entry) { $data = new stdclass(); $data->id = $entry->id; $data->url = strpos($entry->login, 'http') !== 0 ? str_replace('../', $baseURL . $this->config->webRoot, $entry->login) : $entry->login; $allEntries[] = $data; } $_SERVER['SCRIPT_NAME'] = str_replace('x.php', 'index.php', $_SERVER['SCRIPT_NAME']); foreach($entryList as $entry) { if($entry->status != 'online') continue; if(strpos(',' . $entry->platform . ',', ',xuanxuan,') === false) continue; $token = ''; if(isset($files[$entry->id]->pathname)) { $token = '&time=' . $time . '&token=' . md5($files[$entry->id]->pathname . $time); } $data = new stdClass(); $data->entryID = (int)$entry->id; $data->name = $entry->code; $data->displayName = $entry->name; $data->abbrName = $entry->abbr; $data->optional = $entry->optional; $data->enable = $entry->enable; $data->webViewUrl = strpos($entry->login, 'http') !== 0 ? str_replace('../', $baseURL . $this->config->webRoot, $entry->login) : $entry->login; $data->download = empty($entry->package) ? '' : $baseURL . helper::createLink('file', 'download', "fileID={$entry->package}&mouse=" . $token); $data->md5 = empty($entry->package) ? '' : md5($entry->package); $data->logo = empty($entry->logo) ? '' : $baseURL . $this->config->webRoot . ltrim($entry->logo, '/'); if($entry->sso) $data->data = $allEntries; $entries[] = $data; } return $entries; } /** * transfer ip to number * * @param string $ip * @return int */ public function getIPLong($ip) { return bindec(decbin(ip2long($ip))); } /** * Check whether IP is valid * * @param string $ip * @return bool */ public function checkIPValidity($ip) { if(filter_var($ip, FILTER_VALIDATE_IP)) return true; return false; } /** * Check whether CIDR is valid * * @param string $ip * @return bool */ public function checkCIDRValidity($cidr) { $parts = explode('/', $cidr); if(count($parts) != 2) return false; $ip = $parts[0]; if(!$this->checkIPValidity($ip)) return false; $netmask = $parts[1]; if(!is_numeric($netmask)) return false; $netmask = intval($parts[1]); if($netmask < 0 || $netmask > 32) return false; return true; } /** * check whether IP in CIDR * @param string|number $ip * @param string $cidr * @return bool */ public function checkIPInCIDR($ip, $cidr) { $cidr = explode('/', $cidr); if(is_string($ip)) $ip = $this->getIPLong($ip); $startIp = long2ip((ip2long($cidr[0])) & ((-1 << (32 - (int)$cidr[1])))); $endIp = long2ip((ip2long($startIp)) + pow(2, (32 - (int)$cidr[1])) - 1); $startIp = $this->getIPLong($startIp); $endIp = $this->getIPLong($endIp); return $ip >= $startIp && $ip <= $endIp; } /** * check whether IP in CIDRs * @param string $ip * @param string $startIp * @param string $endIp * @return bool */ public function checkIPInCIDRs($ip, $cidrs) { $originCidrs = $cidrs; $cidrs = explode(',', $cidrs); if(count($cidrs) == 0) { if($this->checkCIDRValidity($originCidrs)) { $cidrs = array($originCidrs); } else { return false; } } $ip = $this->getIPLong($ip); foreach($cidrs as $cidr) if($this->checkIPInCIDR($ip, $cidr)) return true; return false; } /** * __call functions defined in model. * * @param string $function * @param array $arguments * @access public * @return void */ public function __call($function, $arguments) { foreach($this->models as $model) { if(strpos(strtolower($function), $model) === 0) { $trimedFunction = substr($function, strlen($model)); if(is_callable(array($this->$model, $trimedFunction))) return call_user_func_array(array($this->$model, $trimedFunction), $arguments); } } $this->app->triggerError("Method im::$function not exists.", __FILE__, __LINE__, $exit = true); } }