znt/module/misc/model.php

389 lines
16 KiB
PHP

<?php
declare(strict_types=1);
/**
* The model file of misc module of ZenTaoPMS.
* @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.cnezsoft.com)
* @license ZPL(http://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
* @author Chunsheng Wang <chunsheng@cnezsoft.com>
* @package misc
* @version $Id: model.php 4129 2013-01-18 01:58:14Z wwccss $
* @link https://www.zentao.net
*/
?>
<?php
class miscModel extends model
{
/**
* 获取禅道数据库的表名和状态。
* Get table and status.
*
* @param string $type check|repair
* @access public
* @return array|false
*/
public function getTableAndStatus(string $type = 'check'): array|false
{
if($type != 'check' && $type != 'repair') return false;
$tables = array();
$stmt = $this->dao->query("show full tables");
while($table = $stmt->fetch(PDO::FETCH_ASSOC))
{
$tableName = $table["Tables_in_{$this->config->db->name}"];
$tableType = strtolower($table['Table_type']);
if($tableType == 'base table')
{
$tableStatus = $this->dao->query("$type table $tableName")->fetch();
$tables[$tableName] = strtolower($tableStatus->Msg_text);
}
}
return $tables;
}
/**
* 获取新增年度总结功能的通知。
* Get remind.
*
* @access public
* @return string
*/
public function getRemind(): string
{
$remind = '';
if(!empty($this->config->global->showAnnual) && empty($this->config->global->annualShowed))
{
$remind = '<h4>' . $this->lang->misc->showAnnual . '</h4>';
$remind .= '<p>' . sprintf($this->lang->misc->annualDesc, helper::createLink('report', 'annualData')) . '</p>';
$this->loadModel('setting')->setItem("{$this->app->user->account}.common.global.annualShowed", 1);
}
return $remind;
}
/**
* 获取插件到期通知。
* Get the notification information about plugin expiration.
*
* @access public
* @return string
*/
public function getPluginRemind(): string
{
$plugins = $this->loadModel('extension')->getExpiringPlugins();
$remind = '';
$today = helper::today();
$showPluginRemind = (empty($this->config->global->showPluginRemind) || $this->config->global->showPluginRemind != $today) ? true : false;
if(!empty($plugins) && $this->app->user->admin && $showPluginRemind)
{
$pluginButton = html::a(helper::createLink('extension', 'browse'), $this->lang->misc->view, '', "id='pluginButton' class='btn primary wide mr-2' data-app='admin'");
$remind = '<p>' . sprintf($this->lang->misc->expiredTipsForAdmin, count($plugins)) . '</p>';
$remind .= '<p class="text-center mt-4">' . $pluginButton . '</p>';
$this->loadModel('setting')->setItem("{$this->app->user->account}.common.global.showPluginRemind", $today);
}
return $remind;
}
/**
* 获取更新度量库表索引的通知。
* Get the notification information about updating metriclib table index.
*
* @access public
* @return string
*/
public function getMetriclibRemind(): string
{
if(!$this->app->user->admin) return '';
if($this->config->edition == 'open') return '';
if($this->config->vision != 'rnd') return '';
$remind = '';
if(empty($this->config->global->metriclibShowed))
{
$remind .= '<p>' . $this->lang->misc->metriclibTips . '</p>';
$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>';
$this->loadModel('setting')->setItem("{$this->app->user->account}.common.global.metriclibShowed", 1);
}
return $remind;
}
/**
* 检查一键安装包的安全性。
* Check one click package.
*
* @access public
* @return array
*/
public function checkOneClickPackage(): array
{
$weakSites = array();
if(strpos('|/zentao/|/biz/|/max/|', "|{$this->config->webRoot}|") !== false)
{
$databases = array('zentao' => 'zentao', 'zentaobiz' => 'zentaobiz', 'zentaoep' => 'zentaoep', 'zentaomax' => 'zentaomax');
$basePath = dirname($this->app->getBasePath());
foreach($databases as $database)
{
$zentaoDirName = $database;
if(!is_dir($basePath . '/' . $zentaoDirName))
{
if($zentaoDirName == 'zentaobiz' && !is_dir($basePath . '/zentaoep')) continue;
if($zentaoDirName == 'zentaoep' && !is_dir($basePath . '/zentaobiz')) continue;
if($zentaoDirName == 'zentao' || $zentaoDirName == 'zentaomax') continue;
if($zentaoDirName == 'zentaobiz') $zentaoDirName = 'zentaoep';
}
try
{
$webRoot = "/{$database}/";
if($database == 'zentao') $webRoot = '/zentao/';
if($database == 'zentaobiz') $webRoot = '/biz/';
if($database == 'zentaoep') $webRoot = '/biz/';
if($database == 'zentaomax') $webRoot = '/max/';
$user = $this->dbh->query("select * from {$database}.`zt_user` where account = 'admin' and password='" . md5('123456') . "'")->fetch();
if($user)
{
$site = array();
$site['path'] = basename($basePath) . '/' . $zentaoDirName;
$site['database'] = $database;
$weakSites[$database] = $site;
}
}
catch(Exception $e){}
}
}
return $weakSites;
}
/**
* 获取升级提示的通知。
* Get upgrade remind.
*
* @access public
* @return bool
*/
public function getUpgradeRemind(): bool
{
if(!empty($this->config->global->hideUpgradeGuide)) return false;
$remind = false;
if(empty($this->config->global->showUpgradeGuide))
{
$remind = true;
$this->loadModel('setting')->setItem("{$this->app->user->account}.common.global.showUpgradeGuide", 1);
}
return $remind;
}
/**
* 获取最近版本列表.
* Get the latest version list
* @param string $url
* @param string|array $data
* @param array $options This is option and value pair, like CURLOPT_HEADER => true. Use curl_setopt function to set options.
* @param array $headers Set request headers.
* @param string $dataType
* @param string $method POST|PATCH|PUT
* @param int $timeout
* @param bool $httpCode
* @param bool $log
* @static
* @access public
* @return string
*/
public function getLatestVersionList($url, $data = null, $options = array(), $headers = array(), $dataType = 'data', $method = 'POST', $timeout = 30, $httpCode = false, $log = true)
{
/* Module detection. */
global $lang, $app;
if(!extension_loaded('curl'))
{
if($dataType == 'json') return print($lang->error->noCurlExt);
return json_encode(array('result' => 'fail', 'message' => $lang->error->noCurlExt));
}
/* Set HTTP request header. */
commonModel::$requestErrors = array();
$requestType = 'GET';
if(!is_array($headers)) $headers = (array)$headers;
$headers[] = 'API-RemoteIP: ' . helper::getRemoteIp(); /* Real IP of real user. */
$headers[] = 'API-LocalIP: ' . zget($_SERVER, 'SERVER_ADDR', ''); /* Server IP of self. */
if($dataType == 'json')
{
$headers[] = 'Content-Type: application/json;charset=utf-8';
if(!empty($data)) $data = json_encode($data);
}
/* Set curl configuration options. */
$curl = curl_init();
curl_setopt($curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
curl_setopt($curl, CURLOPT_USERAGENT, 'ZenTao PMS ' . $app->config->version);
curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, $timeout);
curl_setopt($curl, CURLOPT_TIMEOUT, $timeout);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_ENCODING,'');
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
curl_setopt($curl, CURLOPT_HEADER, $httpCode);
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 2);
curl_setopt($curl, CURLINFO_HEADER_OUT, true);
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_REFERER, $_SERVER['HTTP_REFERER']);
if(!empty($data))
{
if(is_object($data)) $data = (array) $data;
if($method == 'POST') curl_setopt($curl, CURLOPT_POST, true);
if(in_array($method, array('PATCH', 'PUT'))) curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
$requestType = $method;
curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
}
/* Send request. */
if($options) curl_setopt_array($curl, $options);
$response = curl_exec($curl);
$errno = curl_errno($curl);
$errors = empty($errno) ? 0 : curl_error($curl);
$info = curl_getinfo($curl);
/* Processing response data. */
if($httpCode)
{
$httpCode = $info['http_code'] ?? curl_getinfo($curl, CURLINFO_HTTP_CODE);
$headerSize = $info['header_size'] ?? curl_getinfo($curl, CURLINFO_HEADER_SIZE);
$headerString = substr($response, 0, $headerSize);
$body = substr($response, $headerSize);
/* Parse header. */
$header = explode("\n", $headerString);
$newHeader = array();
foreach($header as $item)
{
$field = explode(':', $item);
if(count($field) < 2) continue;
$headerKey = array_shift($field);
$newHeader[$headerKey] = implode('', $field);
}
}
curl_close($curl);
if($log or $app->config->debug)
{
$runMode = PHP_SAPI == 'cli' ? '_cli' : '';
$logFile = $app->getLogRoot() . 'saas' . $runMode . '.' . date('Ymd') . '.log.php';
if(!file_exists($logFile)) file_put_contents($logFile, '<?php die(); ?' . '>');
$fh = fopen($logFile, 'a');
if($fh)
{
fwrite($fh, date('Ymd H:i:s') . ': ' . $app->getURI() . "\n");
fwrite($fh, "{$requestType} url: " . $url . "\n");
if(!empty($data)) fwrite($fh, 'data: ' . print_r($data, true) . "\n");
fwrite($fh, 'results:' . print_r($response, true) . "\n");
if(!empty($errors))
{
fwrite($fh, 'errno: ' . $errno . "\n");
fwrite($fh, 'errors: ' . $errors . "\n");
fwrite($fh, 'info: ' . print_r($info, true) . "\n");
}
fclose($fh);
}
}
if($errors) commonModel::$requestErrors[] = $errors;
return $httpCode ? array($response, $httpCode, 'body' => $body, 'header' => $newHeader, 'errno' => $errno, 'info' => $info, 'response' => $response) : $response;
}
/**
* Get statistics for API.
*
* @access public
* @return array
*/
public function getStatisticsForAPI(): array
{
/*
如果用户不同意用户体验,则不返回统计数据。
If the user does not agree with the user experience, do not return statistical data.
*/
$this->app->loadConfig('admin');
$register = zget($this->config->admin, 'register', array());
$bindCommunity = zget($register, 'bindCommunity', 'false');
$agreeUX = zget($register, 'agreeUX', 'false');
if($bindCommunity == 'false' || $agreeUX == 'false') return array();
$data = array();
if(isset($this->config->misc->statistics))
{
$statistics = json_decode($this->config->misc->statistics, true);
if(helper::today() == $statistics['date']) return $statistics['data']; // Only statistics once a day
}
$data['user'] = $this->dao->select('COUNT(1) AS count')->from(TABLE_USER)->where('deleted')->eq(0)->fetch('count');
$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');
$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');
$data['task'] = $this->dao->select('COUNT(1) AS count')->from(TABLE_TASK)->where('deleted')->eq(0)->fetch('count');
$data['product'] = $this->dao->select('COUNT(1) AS count')->from(TABLE_PRODUCT)->where('deleted')->eq(0)->fetch('count');
$data['story'] = $this->dao->select('COUNT(1) AS count')->from(TABLE_STORY)->where('deleted')->eq(0)->fetch('count');
$data['bug'] = $this->dao->select('COUNT(1) AS count')->from(TABLE_BUG)->where('deleted')->eq(0)->fetch('count');
$data['case'] = $this->dao->select('COUNT(1) AS count')->from(TABLE_CASE)->where('deleted')->eq(0)->fetch('count');
$data['doc'] = $this->dao->select('COUNT(1) AS count')->from(TABLE_DOC)->where('deleted')->eq(0)->fetch('count');
$data['OS'] = PHP_OS;
$data['phpversion'] = PHP_VERSION;
$data['dbversion'] = $this->dao->getVersion();
$this->loadModel('setting')->updateItem('system.misc.statistics', json_encode(array('date' => date('Y-m-d'), 'data' => $data)));
return $data;
}
/**
* 发送安装过程埋点。
* Sending installation process buried points.
*
* @param object $data
* @access public
* @return bool
*/
public function sendInstallEvent($data)
{
if(!$data->location || !$data->fingerprint) return false;
$apiRoot = $this->config->misc->zentaonet;
$apiURL = $apiRoot . "/zentaoevent-install.json";
if(empty($this->config->global->sn))
{
$sn = $this->loadModel('setting')->setSN();
}
else
{
$sn = $this->config->global->sn;
}
$httpData['sn'] = $sn;
$httpData['location'] = $data->location;
$httpData['fingerprint'] = $data->fingerprint;
$httpData['edition'] = $this->config->edition;
$httpData['version'] = $this->config->version;
$httpData['phpVersion'] = phpversion();
$httpData['os'] = PHP_OS . ' ' . php_uname('m');
$httpData['isDocker'] = file_exists('/.dockerenv');
$httpData['installType'] = '';
$httpData['ip'] = helper::getRemoteIP();
$this->loadModel('common');
$response = common::http($apiURL, $httpData);
$response = json_decode($response);
if($response && $response->result == 'success') return true;
return false;
}
}