model.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. <?php
  2. /**
  3. * The model file of misc module of ZenTaoPMS.
  4. * @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.cnezsoft.com)
  5. * @license ZPL(http://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
  6. * @author Chunsheng Wang <chunsheng@cnezsoft.com>
  7. * @package misc
  8. * @version $Id: model.php 4129 2013-01-18 01:58:14Z wwccss $
  9. * @link https://www.zentao.net
  10. */
  11. ?>
  12. <?php
  13. class miscModel extends model
  14. {
  15. /**
  16. * 获取禅道数据库的表名和状态。
  17. * Get table and status.
  18. *
  19. * @param string $type check|repair
  20. * @access public
  21. * @return array|false
  22. */
  23. public function getTableAndStatus($type = 'check')
  24. {
  25. if($type != 'check' && $type != 'repair') return false;
  26. $tables = array();
  27. $stmt = $this->dao->query("show full tables");
  28. while($table = $stmt->fetch(PDO::FETCH_ASSOC))
  29. {
  30. $tableName = $table["Tables_in_{$this->config->db->name}"];
  31. $tableType = strtolower($table['Table_type']);
  32. if($tableType == 'base table')
  33. {
  34. $tableStatus = $this->dao->query("$type table $tableName")->fetch();
  35. $tables[$tableName] = strtolower($tableStatus->Msg_text);
  36. }
  37. }
  38. return $tables;
  39. }
  40. /**
  41. * 获取新增年度总结功能的通知。
  42. * Get remind.
  43. *
  44. * @access public
  45. * @return string
  46. */
  47. public function getRemind()
  48. {
  49. $remind = '';
  50. if(!empty($this->config->global->showAnnual) && empty($this->config->global->annualShowed))
  51. {
  52. $remind = '<h4>' . $this->lang->misc->showAnnual . '</h4>';
  53. $remind .= '<p>' . sprintf($this->lang->misc->annualDesc, helper::createLink('report', 'annualData')) . '</p>';
  54. $this->loadModel('setting')->setItem("{$this->app->user->account}.common.global.annualShowed", 1);
  55. }
  56. return $remind;
  57. }
  58. /**
  59. * 获取插件到期通知。
  60. * Get the notification information about plugin expiration.
  61. *
  62. * @access public
  63. * @return string
  64. */
  65. public function getPluginRemind()
  66. {
  67. $plugins = $this->loadModel('extension')->getExpiringPlugins();
  68. $remind = '';
  69. $today = helper::today();
  70. $showPluginRemind = (empty($this->config->global->showPluginRemind) || $this->config->global->showPluginRemind != $today) ? true : false;
  71. if(!empty($plugins) && $this->app->user->admin && $showPluginRemind)
  72. {
  73. $pluginButton = html::a(helper::createLink('extension', 'browse'), $this->lang->misc->view, '', "id='pluginButton' class='btn primary wide mr-2' data-app='admin'");
  74. $remind = '<p>' . sprintf($this->lang->misc->expiredTipsForAdmin, count($plugins)) . '</p>';
  75. $remind .= '<p class="text-center mt-4">' . $pluginButton . '</p>';
  76. $this->loadModel('setting')->setItem("{$this->app->user->account}.common.global.showPluginRemind", $today);
  77. }
  78. return $remind;
  79. }
  80. /**
  81. * 获取更新度量库表索引的通知。
  82. * Get the notification information about updating metriclib table index.
  83. *
  84. * @access public
  85. * @return string
  86. */
  87. public function getMetriclibRemind()
  88. {
  89. if(!$this->app->user->admin) return '';
  90. if($this->config->edition == 'open') return '';
  91. if($this->config->vision != 'rnd') return '';
  92. $remind = '';
  93. if(empty($this->config->global->metriclibShowed))
  94. {
  95. $remind .= '<p>' . $this->lang->misc->metriclibTips . '</p>';
  96. $remind .= "<p class='text-center mt-4'>" . html::a(helper::createLink('admin', 'metriclib'), $this->lang->misc->view, '', "id='metriclibButton' class='btn primary wide' data-app='admin'", false) . '</p>';
  97. $this->loadModel('setting')->setItem("{$this->app->user->account}.common.global.metriclibShowed", 1);
  98. }
  99. return $remind;
  100. }
  101. /**
  102. * 检查一键安装包的安全性。
  103. * Check one click package.
  104. *
  105. * @access public
  106. * @return array
  107. */
  108. public function checkOneClickPackage()
  109. {
  110. $weakSites = array();
  111. if(strpos('|/zentao/|/biz/|/max/|', "|{$this->config->webRoot}|") !== false)
  112. {
  113. $databases = array('zentao' => 'zentao', 'zentaobiz' => 'zentaobiz', 'zentaoep' => 'zentaoep', 'zentaomax' => 'zentaomax');
  114. $basePath = dirname($this->app->getBasePath());
  115. foreach($databases as $database)
  116. {
  117. $zentaoDirName = $database;
  118. if(!is_dir($basePath . '/' . $zentaoDirName))
  119. {
  120. if($zentaoDirName == 'zentaobiz' && !is_dir($basePath . '/zentaoep')) continue;
  121. if($zentaoDirName == 'zentaoep' && !is_dir($basePath . '/zentaobiz')) continue;
  122. if($zentaoDirName == 'zentao' || $zentaoDirName == 'zentaomax') continue;
  123. if($zentaoDirName == 'zentaobiz') $zentaoDirName = 'zentaoep';
  124. }
  125. try
  126. {
  127. $webRoot = "/{$database}/";
  128. if($database == 'zentao') $webRoot = '/zentao/';
  129. if($database == 'zentaobiz') $webRoot = '/biz/';
  130. if($database == 'zentaoep') $webRoot = '/biz/';
  131. if($database == 'zentaomax') $webRoot = '/max/';
  132. $user = $this->dbh->query("select * from {$database}.`zt_user` where account = 'admin' and password='" . md5('123456') . "'")->fetch();
  133. if($user)
  134. {
  135. $site = array();
  136. $site['path'] = basename($basePath) . '/' . $zentaoDirName;
  137. $site['database'] = $database;
  138. $weakSites[$database] = $site;
  139. }
  140. }
  141. catch(Exception $e){}
  142. }
  143. }
  144. return $weakSites;
  145. }
  146. /**
  147. * 获取升级提示的通知。
  148. * Get upgrade remind.
  149. *
  150. * @access public
  151. * @return bool
  152. */
  153. public function getUpgradeRemind()
  154. {
  155. if(!empty($this->config->global->hideUpgradeGuide)) return false;
  156. $remind = false;
  157. if(empty($this->config->global->showUpgradeGuide))
  158. {
  159. $remind = true;
  160. $this->loadModel('setting')->setItem("{$this->app->user->account}.common.global.showUpgradeGuide", 1);
  161. }
  162. return $remind;
  163. }
  164. /**
  165. * 获取最近版本列表.
  166. * Get the latest version list
  167. * @param string $url
  168. * @param string|array $data
  169. * @param array $options This is option and value pair, like CURLOPT_HEADER => true. Use curl_setopt function to set options.
  170. * @param array $headers Set request headers.
  171. * @param string $dataType
  172. * @param string $method POST|PATCH|PUT
  173. * @param int $timeout
  174. * @param bool $httpCode
  175. * @param bool $log
  176. * @static
  177. * @access public
  178. * @return string
  179. */
  180. public function getLatestVersionList($url, $data = null, $options = array(), $headers = array(), $dataType = 'data', $method = 'POST', $timeout = 30, $httpCode = false, $log = true)
  181. {
  182. /* Module detection. */
  183. global $lang, $app;
  184. if(!extension_loaded('curl'))
  185. {
  186. if($dataType == 'json') return print($lang->error->noCurlExt);
  187. return json_encode(array('result' => 'fail', 'message' => $lang->error->noCurlExt));
  188. }
  189. /* Set HTTP request header. */
  190. commonModel::$requestErrors = array();
  191. $requestType = 'GET';
  192. if(!is_array($headers)) $headers = (array)$headers;
  193. $headers[] = 'API-RemoteIP: ' . helper::getRemoteIp(); /* Real IP of real user. */
  194. $headers[] = 'API-LocalIP: ' . zget($_SERVER, 'SERVER_ADDR', ''); /* Server IP of self. */
  195. if($dataType == 'json')
  196. {
  197. $headers[] = 'Content-Type: application/json;charset=utf-8';
  198. if(!empty($data)) $data = json_encode($data);
  199. }
  200. /* Set curl configuration options. */
  201. $curl = curl_init();
  202. curl_setopt($curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
  203. curl_setopt($curl, CURLOPT_USERAGENT, 'ZenTao PMS ' . $app->config->version);
  204. curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, $timeout);
  205. curl_setopt($curl, CURLOPT_TIMEOUT, $timeout);
  206. curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
  207. curl_setopt($curl, CURLOPT_ENCODING,'');
  208. curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
  209. curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
  210. curl_setopt($curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
  211. curl_setopt($curl, CURLOPT_HEADER, $httpCode);
  212. curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 2);
  213. curl_setopt($curl, CURLINFO_HEADER_OUT, true);
  214. curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
  215. curl_setopt($curl, CURLOPT_URL, $url);
  216. curl_setopt($curl, CURLOPT_REFERER, $_SERVER['HTTP_REFERER']);
  217. if(!empty($data))
  218. {
  219. if(is_object($data)) $data = (array) $data;
  220. if($method == 'POST') curl_setopt($curl, CURLOPT_POST, true);
  221. if(in_array($method, array('PATCH', 'PUT'))) curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
  222. $requestType = $method;
  223. curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
  224. }
  225. /* Send request. */
  226. if($options) curl_setopt_array($curl, $options);
  227. $response = curl_exec($curl);
  228. $errno = curl_errno($curl);
  229. $errors = empty($errno) ? 0 : curl_error($curl);
  230. $info = curl_getinfo($curl);
  231. /* Processing response data. */
  232. if($httpCode)
  233. {
  234. $httpCode = $info['http_code'] ?? curl_getinfo($curl, CURLINFO_HTTP_CODE);
  235. $headerSize = $info['header_size'] ?? curl_getinfo($curl, CURLINFO_HEADER_SIZE);
  236. $headerString = substr($response, 0, $headerSize);
  237. $body = substr($response, $headerSize);
  238. /* Parse header. */
  239. $header = explode("\n", $headerString);
  240. $newHeader = array();
  241. foreach($header as $item)
  242. {
  243. $field = explode(':', $item);
  244. if(count($field) < 2) continue;
  245. $headerKey = array_shift($field);
  246. $newHeader[$headerKey] = implode('', $field);
  247. }
  248. }
  249. curl_close($curl);
  250. if($log or $app->config->debug)
  251. {
  252. $runMode = PHP_SAPI == 'cli' ? '_cli' : '';
  253. $logFile = $app->getLogRoot() . 'saas' . $runMode . '.' . date('Ymd') . '.log.php';
  254. if(!file_exists($logFile)) file_put_contents($logFile, '<?php die(); ?' . '>');
  255. $fh = fopen($logFile, 'a');
  256. if($fh)
  257. {
  258. fwrite($fh, date('Ymd H:i:s') . ': ' . $app->getURI() . "\n");
  259. fwrite($fh, "{$requestType} url: " . $url . "\n");
  260. if(!empty($data)) fwrite($fh, 'data: ' . print_r($data, true) . "\n");
  261. fwrite($fh, 'results:' . print_r($response, true) . "\n");
  262. if(!empty($errors))
  263. {
  264. fwrite($fh, 'errno: ' . $errno . "\n");
  265. fwrite($fh, 'errors: ' . $errors . "\n");
  266. fwrite($fh, 'info: ' . print_r($info, true) . "\n");
  267. }
  268. fclose($fh);
  269. }
  270. }
  271. if($errors) commonModel::$requestErrors[] = $errors;
  272. return $httpCode ? array($response, $httpCode, 'body' => $body, 'header' => $newHeader, 'errno' => $errno, 'info' => $info, 'response' => $response) : $response;
  273. }
  274. /**
  275. * Get statistics for API.
  276. *
  277. * @access public
  278. * @return array
  279. */
  280. public function getStatisticsForAPI()
  281. {
  282. /*
  283. 如果用户不同意用户体验,则不返回统计数据。
  284. If the user does not agree with the user experience, do not return statistical data.
  285. */
  286. $this->app->loadConfig('admin');
  287. $register = zget($this->config->admin, 'register', array());
  288. $bindCommunity = zget($register, 'bindCommunity', 'false');
  289. $agreeUX = zget($register, 'agreeUX', 'false');
  290. if($bindCommunity == 'false' || $agreeUX == 'false') return array();
  291. $data = array();
  292. if(isset($this->config->misc->statistics))
  293. {
  294. $statistics = json_decode($this->config->misc->statistics, true);
  295. if(helper::today() == $statistics['date']) return $statistics['data']; // Only statistics once a day
  296. }
  297. $data['user'] = $this->dao->select('COUNT(1) AS count')->from(TABLE_USER)->where('deleted')->eq(0)->fetch('count');
  298. $data['project'] = $this->dao->select('model, COUNT(1) AS count')->from(TABLE_PROJECT)->where('deleted')->eq(0)->andWhere('type')->eq('project')->groupBy('model')->fetchPairs('model', 'count');
  299. $data['execution'] = $this->dao->select('COUNT(1) AS count')->from(TABLE_PROJECT)->where('deleted')->eq(0)->andWhere('type')->in(array('sprint', 'stage', 'kanban'))->fetch('count');
  300. $data['task'] = $this->dao->select('COUNT(1) AS count')->from(TABLE_TASK)->where('deleted')->eq(0)->fetch('count');
  301. $data['product'] = $this->dao->select('COUNT(1) AS count')->from(TABLE_PRODUCT)->where('deleted')->eq(0)->fetch('count');
  302. $data['story'] = $this->dao->select('COUNT(1) AS count')->from(TABLE_STORY)->where('deleted')->eq(0)->fetch('count');
  303. $data['bug'] = $this->dao->select('COUNT(1) AS count')->from(TABLE_BUG)->where('deleted')->eq(0)->fetch('count');
  304. $data['case'] = $this->dao->select('COUNT(1) AS count')->from(TABLE_CASE)->where('deleted')->eq(0)->fetch('count');
  305. $data['doc'] = $this->dao->select('COUNT(1) AS count')->from(TABLE_DOC)->where('deleted')->eq(0)->fetch('count');
  306. $data['OS'] = PHP_OS;
  307. $data['phpversion'] = PHP_VERSION;
  308. $data['dbversion'] = $this->dao->getVersion();
  309. $this->loadModel('setting')->updateItem('system.misc.statistics', json_encode(array('date' => date('Y-m-d'), 'data' => $data)));
  310. return $data;
  311. }
  312. /**
  313. * 发送安装过程埋点。
  314. * Sending installation process buried points.
  315. *
  316. * @param object $data
  317. * @access public
  318. * @return bool
  319. */
  320. public function sendInstallEvent($data)
  321. {
  322. if(!$data->location || !$data->fingerprint) return false;
  323. $apiRoot = $this->config->misc->zentaonet;
  324. $apiURL = $apiRoot . "/zentaoevent-install.json";
  325. if(empty($this->config->global->sn))
  326. {
  327. $sn = $this->loadModel('setting')->setSN();
  328. }
  329. else
  330. {
  331. $sn = $this->config->global->sn;
  332. }
  333. $httpData['sn'] = $sn;
  334. $httpData['location'] = $data->location;
  335. $httpData['fingerprint'] = $data->fingerprint;
  336. $httpData['edition'] = $this->config->edition;
  337. $httpData['version'] = $this->config->version;
  338. $httpData['phpVersion'] = phpversion();
  339. $httpData['os'] = PHP_OS . ' ' . php_uname('m');
  340. $httpData['isDocker'] = file_exists('/.dockerenv');
  341. $httpData['installType'] = '';
  342. $httpData['ip'] = helper::getRemoteIP();
  343. $this->loadModel('common');
  344. $response = common::http($apiURL, $httpData);
  345. $response = json_decode($response);
  346. if($response && $response->result == 'success') return true;
  347. return false;
  348. }
  349. }