class_restful.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575
  1. <?php
  2. define('ROOT_PATH', dirname(__FILE__).'/../../');
  3. class _dzRestful {
  4. private $apiParam;
  5. private $postParam;
  6. public $token;
  7. public $tokenData;
  8. private $_appId;
  9. private $_m;
  10. const SignTTL = 300;
  11. const TokenTTL = 3600;
  12. const TokenSaveTTL = 86400 * 30;
  13. const AuthTokenTTL = 300;
  14. const TokenPre = 'rToken_';
  15. const AuthTokenPre = 'rAuthToken_';
  16. const AppIdConfPre = 'rApp_';
  17. const ApiConfPre = 'rApi_';
  18. const ApiFreqPre = 'rFreq_';
  19. const ApiFreqTTL = 60;
  20. const AuthPre = 'rAuth_';
  21. const AuthTTL = 60;
  22. const Error = [
  23. -100 => 'run: script is exception',
  24. -101 => 'checkSign: param is missing',
  25. -102 => 'checkSign: appid is invalid',
  26. -103 => 'checkSign: sign is expire',
  27. -104 => 'checkSign: sign is invalid',
  28. -105 => 'checkToken: token is missing',
  29. -106 => 'checkToken: token is expire',
  30. -107 => 'checkToken: appid is error',
  31. -108 => 'getTokenData: token is error',
  32. -109 => 'getApiParam: api is invalid',
  33. -110 => 'getAppidPerm: appid is invalid',
  34. -111 => 'getSecret: appid is invalid',
  35. -112 => 'parseQuery: api url is empty',
  36. -113 => 'parseQuery: api url is error',
  37. -114 => 'initParam: api is invalid',
  38. -115 => 'apiPermCheck: api is invalid',
  39. -116 => 'apiFreqCheck: out of frequency',
  40. -117 => 'scriptCheck: script is empty',
  41. -118 => 'scriptCheck: script format is error',
  42. -119 => 'callback: authtoken is invalid',
  43. ];
  44. const Developer = false;
  45. public function __construct($postParam = []) {
  46. require_once ROOT_PATH.'./source/function/function_core.php';
  47. $this->postParam = $postParam;
  48. }
  49. public function parseQuery() {
  50. $s = rawurldecode($_SERVER['QUERY_STRING']);
  51. if(!$s) {
  52. $this->error(-112);
  53. }
  54. if(str_ends_with($s, '=')) {
  55. $s = substr($s, 0, -1);
  56. }
  57. if(!str_starts_with($s, '/')) {
  58. $s = '/'.$s;
  59. }
  60. $e = explode('/', $s);
  61. $c = count($e);
  62. if(preg_match('/^v\d+$/', $e[$c - 1])) {
  63. $api = array_slice($e, 1, -1);
  64. $ver = $e[$c - 1];
  65. } else {
  66. $api = array_slice($e, 1);
  67. $ver = 'v1';
  68. }
  69. if(!$api || !$ver) {
  70. $this->error(-113);
  71. }
  72. return [$api, $ver];
  73. }
  74. public function initParam($api, $ver) {
  75. $apiParams = $this->_getApiParam($api, $ver);
  76. $key = '/'.implode('/', $api);
  77. if(empty($apiParams[$key])) {
  78. $this->error(-114);
  79. }
  80. $params = $apiParams[$key];
  81. $params['perms'] = [$key.'/'.$ver];
  82. $this->apiParam = $params;
  83. return $params['script'];
  84. }
  85. public function paramDecode($key) {
  86. if(empty($this->apiParam[$key])) {
  87. return [];
  88. }
  89. if(is_array($this->apiParam[$key])) {
  90. $v = $this->apiParam[$key];
  91. } else {
  92. $_tmp = unserialize($this->apiParam[$key]);
  93. $v = $_tmp === false ? $this->apiParam[$key] : $_tmp;
  94. }
  95. return $this->_replacePostParams($v);
  96. }
  97. public function getRequestParam() {
  98. return !empty($this->postParam['_REQUEST']) ? json_decode($this->postParam['_REQUEST'], true) : [];
  99. }
  100. public function getShutdownFunc() {
  101. $output = !empty($this->apiParam['output']) ? $this->apiParam['output'] : [];
  102. $rawOutput = !empty($this->apiParam['raw']);
  103. $shutdownFunc = 'showOutput';
  104. if($rawOutput) {
  105. $shutdownFunc = 'rawOutput';
  106. } elseif($output) {
  107. $shutdownFunc = 'convertOutput';
  108. }
  109. return [$shutdownFunc, $output];
  110. }
  111. private function _replacePostParams($v) {
  112. foreach($v as $_k => $_v) {
  113. if(is_array($_v)) {
  114. $v[$_k] = $this->_replacePostParams($_v);
  115. } elseif(is_string($_v)) {
  116. if(str_starts_with($_v, '{') && preg_match('/^\{:(\w+):\}$/', $_v, $r)) {
  117. $v[$_k] = !empty($this->postParam[$r[1]]) ? $this->postParam[$r[1]] : null;
  118. }
  119. }
  120. }
  121. return $v;
  122. }
  123. public function apiPermCheck() {
  124. if(!empty($this->tokenData['_conf']['apis']) && !array_intersect($this->apiParam['perms'], $this->tokenData['_conf']['apis'])) {
  125. $this->error(-115);
  126. }
  127. }
  128. public function apiFreqCheck() {
  129. $api = $this->apiParam['perms'][0];
  130. if(!empty($this->tokenData['_conf']['freq'][$api])) {
  131. $key = self::ApiFreqPre.$this->_appId.'_'.$api;
  132. $v = $this->_memory('get', [$key]);
  133. if(!$v) {
  134. $this->_memory('inc', [$key]);
  135. $this->_memory('expire', [$key, 10]);//self::ApiFreqTTL
  136. } elseif($v >= $this->tokenData['_conf']['freq'][$api]) {
  137. $this->error(-116);
  138. } else {
  139. $this->_memory('inc', [$key]);
  140. }
  141. }
  142. }
  143. public function scriptCheck() {
  144. if(empty($this->apiParam['script'])) {
  145. $this->error(-117);
  146. }
  147. if(!preg_match('/^\w+$/', $this->apiParam['script'])) {
  148. $this->error(-118);
  149. }
  150. return $this->apiParam['script'];
  151. }
  152. public function error($code) {
  153. $this->output([
  154. 'ret' => $code,
  155. 'msg' => !empty(self::Error[$code]) ? self::Error[$code] : '',
  156. ]);
  157. }
  158. public function output($value) {
  159. $this->_setSysVar($return['data'], $value);
  160. echo json_encode($value);
  161. exit;
  162. }
  163. public function showOutput() {
  164. $return = ['ret' => 0];
  165. $s = ob_get_contents();
  166. ob_end_clean();
  167. $return['data']['content'] = $s;
  168. $this->plugin('after', $return['data']);
  169. $this->output($return);
  170. }
  171. public function rawOutput() {
  172. $newTokenData = $this->updateTokenData();
  173. if($newTokenData) {
  174. $ttl = $this->tokenData['refreshExptime'] - time();
  175. $this->setToken($this->token, $newTokenData, $ttl);
  176. }
  177. exit;
  178. }
  179. public function convertOutput($output) {
  180. ob_end_clean();
  181. $return = ['ret' => 0];
  182. $tmp = $GLOBALS;
  183. foreach($output as $k => $v) {
  184. if(str_contains($k, '/')) {
  185. $return['data'][$v] = $this->_arrayVar($tmp, $k);
  186. } else {
  187. $return['data'][$v] = $this->_singleVar($tmp, $k);
  188. }
  189. }
  190. $this->plugin('after', $return['data']);
  191. $this->output($return);
  192. }
  193. public function plugin($type, &$data) {
  194. if(empty($this->apiParam['plugin'][$type])) {
  195. return;
  196. }
  197. foreach($this->apiParam['plugin'][$type] as $method => $value) {
  198. $vars = explode(':', $method);
  199. if(count($vars) == 2) {
  200. if(!preg_match('/^\w+$/', $vars[0])) {
  201. continue;
  202. }
  203. require_once ROOT_PATH.'./source/function/function_path.php';
  204. $f = ROOT_PATH.PLUGIN_ROOT.$vars[0].'/restful.class.php';
  205. $c = 'restful_'.$vars[0];
  206. $m = $vars[1];
  207. } else {
  208. $f = ROOT_PATH.'./source/class/class_restfulplugin.php';
  209. $c = 'restfulplugin';
  210. $m = $method;
  211. }
  212. if(!file_exists($f)) {
  213. continue;
  214. }
  215. @require_once $f;
  216. if(!class_exists($c) || !method_exists($c, $m)) {
  217. continue;
  218. }
  219. call_user_func_array([$c, $m], [&$data, explode(',', $value['param'] ?? '')]);
  220. }
  221. }
  222. public function sessionDecode() {
  223. $session = unserialize(base64_decode($this->tokenData['_session']));
  224. if(!empty($this->postParam['_authSign'])) {
  225. $this->decodeAuthSign($session);
  226. }
  227. return $session;
  228. }
  229. public function checkSign() {
  230. if(empty($_SERVER['HTTP_APPID']) ||
  231. empty($_SERVER['HTTP_SIGN']) ||
  232. empty($_SERVER['HTTP_NONCE']) ||
  233. empty($_SERVER['HTTP_T'])) {
  234. $this->error(-101);
  235. }
  236. $this->_getAppidPerm($_SERVER['HTTP_APPID']);
  237. $secret = $this->_getSecret();
  238. if(!$secret) {
  239. $this->error(-102);
  240. }
  241. if(time() - $_SERVER['HTTP_T'] > self::SignTTL) {
  242. $this->error(-103);
  243. }
  244. if($_SERVER['HTTP_SIGN'] != base64_encode(hash('sha256', $_SERVER['HTTP_NONCE'].$_SERVER['HTTP_T'].$secret))) {
  245. $this->error(-104);
  246. }
  247. }
  248. public function checkToken() {
  249. if(empty($this->_getToken())) {
  250. $this->error(-105);
  251. }
  252. $v = $this->_getTokenData();
  253. if(time() >= $v['exptime']) {
  254. $this->error(-106);
  255. }
  256. $this->tokenData = $v;
  257. if(!$v['_appid'] || $v['_appid'] != $_SERVER['HTTP_APPID']) {
  258. $this->error(-107);
  259. }
  260. $this->_getAppidPerm($v['_appid']);
  261. $this->postParam['formhash'] = !empty($this->tokenData['_formhash']) ? $this->tokenData['_formhash'] : '';
  262. $this->token = $this->_getToken();
  263. }
  264. public function setToken($key, $value, $ttl = self::TokenSaveTTL) {
  265. $this->_memory('set', [self::TokenPre.$key, serialize($value), $ttl]);
  266. }
  267. public function setAuthToken($token, $value) {
  268. if($this->getAuthToken($token)) {
  269. return false;
  270. }
  271. global $_G;
  272. $value = authcode(serialize($value), 'ENCODE', md5($_G['config']['security']['authkey']));
  273. $this->_memory('set', [self::AuthTokenPre.$token, $value, self::AuthTokenTTL]);
  274. return true;
  275. }
  276. public function getAuthToken($token) {
  277. global $_G;
  278. $value = $this->_memory('get', [self::AuthTokenPre.$token]);
  279. return $value ? dunserialize(authcode($value, 'DECODE', md5($_G['config']['security']['authkey']))) : [];
  280. }
  281. public function isRefreshToken() {
  282. return !empty($this->_getToken());
  283. }
  284. private function _getToken() {
  285. return !empty($_SERVER['HTTP_TOKEN']) ? $_SERVER['HTTP_TOKEN'] : (!empty($this->postParam['token']) ? $this->postParam['token'] : '');
  286. }
  287. private function _getTokenData() {
  288. $v = dunserialize($this->_memory('get', [self::TokenPre.$this->_getToken()]));
  289. if(empty($v)) {
  290. $this->error(-108);
  291. }
  292. return $v;
  293. }
  294. public function refreshTokenData() {
  295. $v = $this->_getTokenData();
  296. $data['_session'] = $v['_session'];
  297. $data['_formhash'] = $this->_singleVar($_G, 'formhash');
  298. $data['_appid'] = $_SERVER['HTTP_APPID'];
  299. $data['_conf'] = $this->tokenData['_conf'];
  300. $data['exptime'] = time() + self::TokenTTL;
  301. $data['refreshExptime'] = time() + self::TokenSaveTTL;
  302. return serialize($data);
  303. }
  304. public function delTokenData() {
  305. $this->_memory('rm', [self::TokenPre.$this->_getToken()]);
  306. }
  307. public function newTokenData() {
  308. global $_G;
  309. $data = [];
  310. $data['_session'] = $this->_sessionEncode($_COOKIE);
  311. $data['_formhash'] = $this->_singleVar($_G, 'formhash');
  312. $data['_appid'] = $_SERVER['HTTP_APPID'];
  313. $data['_conf'] = $this->tokenData['_conf'];
  314. $data['exptime'] = time() + self::TokenTTL;
  315. $data['refreshExptime'] = time() + self::TokenSaveTTL;
  316. return serialize($data);
  317. }
  318. private function _getApiParam($api, $ver) {
  319. if(self::Developer) {
  320. return $this->_getApiParam_Developer($api, $ver);
  321. }
  322. $v = $this->_memory('get', [self::ApiConfPre.'/'.$api[0].'_'.$ver]);
  323. if(!$v) {
  324. $this->error(-109);
  325. }
  326. return $this->apiParam = dunserialize($v);
  327. }
  328. private function _getAppidPerm($appid) {
  329. $v = $this->_memory('get', [self::AppIdConfPre.$appid]);
  330. if(!$v) {
  331. if(!empty($GLOBALS['discuz'])) {
  332. require_once libfile('function/cache');
  333. updatecache('restful');
  334. $v = $this->_memory('get', [self::AppIdConfPre.$appid]);
  335. if(!$v) {
  336. $this->error(-110);
  337. }
  338. } else {
  339. $this->error(-110);
  340. }
  341. }
  342. $this->_appId = $appid;
  343. $v = dunserialize($v);
  344. $this->tokenData['_conf'] = $v;
  345. }
  346. public function updateTokenData() {
  347. global $_G;
  348. $data = $this->tokenData;
  349. $_session = $this->_sessionEncode($_COOKIE);
  350. if(!empty($this->tokenData['_session']) && $_session == $this->tokenData['_session']) {
  351. return null;
  352. }
  353. $data['_session'] = $_session;
  354. $data['_formhash'] = $this->_singleVar($_G, 'formhash');
  355. return serialize($data);
  356. }
  357. private function _getSecret() {
  358. if(empty($this->tokenData['_conf']['secret'])) {
  359. $this->error(-111);
  360. }
  361. return $this->tokenData['_conf']['secret'];
  362. }
  363. private function _sessionEncode($v) {
  364. return base64_encode(serialize($v));
  365. }
  366. private function _setSysVar(&$data, &$output = []) {
  367. global $_G;
  368. $newTokenData = $this->updateTokenData();
  369. if(!empty($this->tokenData['refreshExptime']) && $newTokenData) {
  370. $ttl = $this->tokenData['refreshExptime'] - time();
  371. $this->setToken($this->token, $newTokenData, $ttl);
  372. }
  373. if(!empty($_G['_multi'])) {
  374. $output['multi'] = $_G['_multi'];
  375. }
  376. unset($_G['config'],
  377. $_G['setting']['siteuniqueid'],
  378. $_G['setting']['ec_tenpay_opentrans_chnid'],
  379. $_G['setting']['ec_tenpay_opentrans_key'],
  380. $_G['setting']['ec_tenpay_bargainor'],
  381. $_G['setting']['ec_tenpay_key'],
  382. $_G['setting']['ec_account'],
  383. $_G['setting']['ec_contract']);
  384. }
  385. private function _singleVar(&$var, $k) {
  386. return $var[$k] ?? null;
  387. }
  388. private function _arrayVar(&$var, $k) {
  389. unset($GLOBALS['_L']['_G']);
  390. $value = null;
  391. $sVar = &$var;
  392. $e = explode('/', $k);
  393. $count = count($e);
  394. foreach($e as $i => $_k) {
  395. if($_k == '*') {
  396. foreach($sVar as $_k3 => $_v3) {
  397. $nKey = implode('/', array_slice($e, $i + 1));
  398. $value[$_k3] = $this->_arrayVar($_v3, $nKey);
  399. }
  400. break;
  401. }
  402. $isMulti = str_contains($_k, ',');
  403. if(!isset($sVar[$_k]) && !$isMulti) {
  404. break;
  405. }
  406. if($isMulti) {
  407. $value = null;
  408. foreach(explode(',', $_k) as $_k2) {
  409. $value[$_k2] = $this->_singleVar($sVar, $_k2);
  410. }
  411. break;
  412. } else {
  413. if($count - 1 == $i) {
  414. $value = $this->_singleVar($sVar, $_k);
  415. }
  416. $sVar = &$sVar[$_k];
  417. }
  418. }
  419. return $value;
  420. }
  421. private function _memory($method, $params = []) {
  422. if($this->_m == null) {
  423. require_once ROOT_PATH.'./source/class/memory/memory_driver_redis.php';
  424. $_config = [];
  425. require ROOT_PATH.'./config/config_global.php';
  426. $this->_m = new memory_driver_redis();
  427. if(empty($_config['memory']['redis'])) {
  428. return null;
  429. }
  430. $this->_m->init($_config['memory']['redis']);
  431. if(!$this->_m->enable) {
  432. return null;
  433. }
  434. $this->_config = $_config;
  435. }
  436. if($this->_m == null) {
  437. return null;
  438. }
  439. if(!method_exists($this->_m, $method)) {
  440. return null;
  441. }
  442. return call_user_func_array([$this->_m, $method], $params);
  443. }
  444. // for Developer
  445. private function _getApiParam_Developer($api, $ver) {
  446. $xml = ROOT_PATH.'./data/discuz_restful.xml';
  447. require_once ROOT_PATH.'./source/class/class_xml.php';
  448. $data = file_get_contents($xml);
  449. $xmldata = xml2array($data);
  450. foreach($xmldata['Data']['api'] as $ver => $apis) {
  451. if(!preg_match('/^v(\d+)$/', $ver, $r)) {
  452. continue;
  453. }
  454. $this->_parseApis($apis, $ver, '/');
  455. }
  456. if(empty($_ENV['api'][$ver]['/'.$api[0]])) {
  457. $this->error(-109);
  458. }
  459. return $this->apiParam = $_ENV['api'][$ver]['/'.$api[0]];
  460. }
  461. private function _parseApis($apis, $ver, $uriPre = '/') {
  462. if(!is_array($apis)) {
  463. return;
  464. }
  465. foreach($apis as $uri => $api) {
  466. $k = $uriPre.$uri;
  467. list(, $baseuri) = explode('/', $k);
  468. $baseuri = '/'.$baseuri;
  469. if(!empty($api['script']) && is_string($api['script'])) {
  470. $_ENV['api'][$ver][$baseuri][$k] = [];
  471. $_ENV['api'][$ver][$baseuri][$k] = $api;
  472. } else {
  473. $this->_parseApis($api, $ver, $k.'/');
  474. }
  475. }
  476. }
  477. public function getAuthSign() {
  478. static $authSign = null;
  479. if($authSign !== null) {
  480. return $authSign;
  481. }
  482. global $_G;
  483. $this->_memory('set', [self::AuthPre.$_G['cookie']['sid'], serialize([
  484. 'auth' => $_G['cookie']['auth'],
  485. 'saltkey' => $_G['cookie']['saltkey'],
  486. 'sid' => $_G['cookie']['sid']
  487. ]), self::AuthTTL]);
  488. return $authSign = authcode($_G['cookie']['sid'], 'ENCODE', md5($_G['config']['security']['authkey']), self::AuthTTL);
  489. }
  490. public function decodeAuthSign(&$session) {
  491. $sid = authcode($this->postParam['_authSign'], 'DECODE', md5($this->_config['security']['authkey']));
  492. if(!$sid) {
  493. return;
  494. }
  495. $data = $this->_memory('get', [self::AuthPre.$sid]);
  496. if(!$data) {
  497. return;
  498. }
  499. $cookiepre = $this->_config['cookie']['cookiepre'].substr(md5($this->_config['cookie']['cookiepath'].'|'.$this->_config['cookie']['cookiedomain']), 0, 4).'_';
  500. $session[$cookiepre.'auth'] = $data['auth'];
  501. $session[$cookiepre.'saltkey'] = $data['saltkey'];
  502. $session[$cookiepre.'sid'] = $data['sid'];
  503. }
  504. }