lib.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  1. <?php
  2. use \PDO as PDO;
  3. use \PDOException as PDOException;
  4. $cfg = "config.json";
  5. foreach (json_decode(file_get_content($cfg)) as $k => $v)
  6. define($k, $v);
  7. class Db
  8. {
  9. private static $pdo = null;
  10. public static function pdo()
  11. {
  12. if (self::$pdo !== null) {
  13. return self::$pdo;
  14. }
  15. try {
  16. $dsn = sprintf('mysql:host=%s;dbname=%s;charset=utf8', DB_HOST, DB_NAME);
  17. $option = [
  18. PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_OBJ,
  19. PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
  20. ];
  21. self::$pdo = new PDO($dsn, DB_USER, DB_PASS, $option);
  22. if (self::$pdo === null)
  23. {
  24. die("Failed to create database object");
  25. }
  26. return self::$pdo;
  27. } catch (PDOException $e) {
  28. die("Database connection error");
  29. }
  30. }
  31. }
  32. class SqlResult
  33. {
  34. public $result;
  35. public $error;
  36. public $isOk;
  37. public function __construct($res, $err)
  38. {
  39. $this->result = $res;
  40. $this->error = $err;
  41. $this->isOk = empty($this->error);
  42. }
  43. }
  44. // this class based on and modified from: https://www.awaimai.com/128.html
  45. class Sql
  46. {
  47. public $table;
  48. private $filter = '';
  49. private $param = [];
  50. private $special_cols = [];
  51. public $error = null;
  52. private $distinct = "";
  53. private $debug = 0;
  54. /**
  55. * changing the table to $t
  56. */
  57. public function table($t, $debugLevel = 0)
  58. {
  59. if (strpos($t, '.') !== false)
  60. $t = implode('`.`', explode('.', $t));
  61. $this->table = $t;
  62. $this->filter = '';
  63. $this->param = [];
  64. $this->error = "";
  65. $this->distinct = "";
  66. $this->debug = $debugLevel;
  67. return $this;
  68. }
  69. /**
  70. * enable/disable the distinction while query
  71. */
  72. public function distinct($t = true)
  73. {
  74. $this->distinct = $t ? "DISTINCT " : "";
  75. return $this;
  76. }
  77. /**
  78. * execute SQL directly with only affected row count returned.
  79. *
  80. * @param $statment : SQL statment that want to be executed.
  81. * @param $param : inputs that used in the statment. will be filtered by PDO.
  82. * @param $fetchMode: [0: none, 1: fetch, 2: fetchAll, 3: rowCount]
  83. * @param $fetchType: only if fetchMode in [1, 2]
  84. * @return SqlResult: execution result
  85. */
  86. public function exec($statment, $param = [], $fetchMode = 0, $fetchType = PDO::FETCH_OBJ)
  87. {
  88. $sth = Db::pdo()->prepare($statment);
  89. $sth = $this->formatParam($sth, $param);
  90. if ($sth->execute() == false)
  91. {
  92. // have error!
  93. $this->error = $sth->errorInfo();
  94. }
  95. switch ($fetchMode)
  96. {
  97. case 1:
  98. $mode_result = $sth->fetch($fetchType);
  99. break;
  100. case 2:
  101. $mode_result = $sth->fetchAll($fetchType);
  102. break;
  103. case 3:
  104. $mode_result = (int)$sth->rowCount();
  105. break;
  106. default:
  107. $mode_result = true;
  108. }
  109. return new SqlResult($mode_result, $this->error);
  110. }
  111. /**
  112. * newly write, me hea ar, 5 write desc la :(
  113. *
  114. * ie:
  115. * CAST(... AS ...) as sp_col
  116. *
  117. * ... -> group(...) -> having("decrypted LIKE '%?%'", ["keyword"], 'CAST(... ?) as decrypted', ["value"])
  118. */
  119. public function having(string $having_stmt, array $val, string $col, array $param = [])
  120. {
  121. array_unshift($this->special_cols, $col);
  122. $this->param = array_merge($param, $this->param); // prepend merge
  123. $this->filter .= " HAVING $having_stmt ";
  124. $this->param = array_merge($this->param, $val);
  125. return $this;
  126. }
  127. /**
  128. * in function (SQL: IN):
  129. *
  130. * ie:
  131. * // "where" must be used before "in"
  132. * 1. $this->table('log')->where(['status = ?'], [$status])->in('AND', 'sid', $staffIDarray)->fetch();
  133. * // OR without "where"
  134. * 2. $this->table('log')->in('', 'sid', $staffIDarray)->fetch();
  135. *
  136. * @param string $connector : AND / OR
  137. * @param string $colName : column name
  138. * @param array $param : value set
  139. * @param string $ending : [Optional] (default: ""), after "in", put what at the end of SQL (currently)
  140. * @return $this : this Object
  141. */
  142. public function in(string $connector, string $colName, array $param, string $ending = "")
  143. {
  144. if (!empty($param))
  145. {
  146. if (empty($this->filter) || strpos($this->filter, 'WHERE') === false)
  147. $this->filter .= ' WHERE ';
  148. else if (!empty($this->filter))
  149. $this->filter .= " " . $connector . " ";
  150. if (strpos($colName, "`") === false)
  151. {
  152. $colName = '`' . $colName . '`';
  153. }
  154. $this->filter .= " " . $colName . ' IN (';
  155. $this->filter .= implode(',', array_fill(0, count($param), '?'));
  156. $this->filter .= ') ';
  157. if (!empty($ending))
  158. $this->filter .= $ending;
  159. $this->param = array_merge($this->param, $param);
  160. }
  161. return $this;
  162. }
  163. /**
  164. * where function (SQL condition):
  165. *
  166. * ie:
  167. * 1. $this->table('login')->where(['id = :id'], [':id' => $id])->fetch();
  168. * 2. $this->table('login')->where(['id = ?'], [$id])->fetch();
  169. *
  170. * // multiple conditions
  171. * 3. $this->table('login')->where(['id = :id', 'AND uname = :uname'], [':id' => $id, ':uname' => $uname])->fetch();
  172. * 4. $this->table('login')->where(['id = ?', 'OR uname = ?'], [$id, $uname])->fetch();
  173. *
  174. * @param array $where : condition
  175. * @param array $param : values
  176. * @return $this : this Object
  177. */
  178. public function where(array $where = [], array $param = [])
  179. {
  180. if ($where) {
  181. $this->filter .= ' WHERE ';
  182. $this->filter .= implode(' ', $where);
  183. $this->param = array_merge($this->param, $param);
  184. }
  185. return $this;
  186. }
  187. /**
  188. * order function (SQL: ORDER BY):
  189. *
  190. * ie:
  191. * 1. $this->order(['id DESC'])->fetch();
  192. * 2. $this->order(['id DESC', 'username ASC'])->fetch();
  193. *
  194. * @param array $order : order condition
  195. * @return $this : this Object
  196. */
  197. public function order($order = [])
  198. {
  199. if ($order) {
  200. $this->filter .= ' ORDER BY ';
  201. if (!is_array($order))
  202. $this->filter .= $order;
  203. else
  204. $this->filter .= implode(',', $order);
  205. }
  206. return $this;
  207. }
  208. /**
  209. * group function (SQL: GROUP BY):
  210. *
  211. * ie:
  212. * 1. $this->group(['id'])->fetch();
  213. * 2. $this->group(['id', 'username'])->fetch();
  214. *
  215. * @param array $group
  216. * @return $this
  217. */
  218. public function group($group = [])
  219. {
  220. if($group) {
  221. $this->filter .= ' GROUP BY ';
  222. $this->filter .= implode(',', $group);
  223. }
  224. return $this;
  225. }
  226. /**
  227. * fetchAll function:
  228. *
  229. * ie:
  230. * 1. $this->table('login')->where(['username=?', 'AND hashed=?'], [$username, $hashed])->fetchAll();
  231. *
  232. * @param array $cols : [Optional] (default: all columns), the required column names
  233. * @param array $dataType : [Optional] (default: PDO::FETCH_OBJ), the required data type (PDO::FETCH_OBJ, PDO::FETCH_NUM, PDO::FETCH_BOTH, ...)
  234. *
  235. * @return array $return:
  236. * @ if success:
  237. * ["result"] => true
  238. * ["data"] => $sth->fetchAll() : same as PDOStatement::fetchAll()
  239. *
  240. * @ if failed:
  241. * ["result"] => false
  242. * ["error"] => $this->error (raw error message)
  243. */
  244. public function fetchAll($cols = ["*"], $dataType = PDO::FETCH_OBJ)
  245. {
  246. $sql = sprintf("SELECT %s %s FROM `%s` %s", $this->distinct, implode(',', array_merge($cols, $this->special_cols)), $this->table, $this->filter);
  247. $sth = Db::pdo()->prepare($sql);
  248. $sth = $this->formatParam($sth, $this->param);
  249. if ($this->debug)
  250. $this->debugDump($sth);
  251. if ($sth->execute() == false)
  252. {
  253. // have error!
  254. $this->error = $sth->errorInfo();
  255. }
  256. return new SqlResult($sth->fetchAll($dataType), $this->error);
  257. }
  258. /**
  259. * fetch function:
  260. *
  261. * ie:
  262. * 1. $this->table('login')->where(['username=?', 'AND hashed=?'], [$username, $hashed])->fetch();
  263. *
  264. * @param array $cols : [Optional] (default: all columns), the required column names
  265. * @param array $dataType : [Optional] (default: PDO::FETCH_OBJ), the required data type (PDO::FETCH_OBJ, PDO::FETCH_NUM, PDO::FETCH_BOTH, ...)
  266. *
  267. * @return array $return:
  268. * @ if success:
  269. * ["result"] => true
  270. * ["data"] => $sth->fetch() : same as PDOStatement::fetch()
  271. *
  272. * @ if failed:
  273. * ["result"] => false
  274. * ["error"] => $this->error (raw error message)
  275. */
  276. public function fetch($cols = ["*"], $dataType = PDO::FETCH_OBJ)
  277. {
  278. $sql = sprintf("SELECT %s %s FROM `%s` %s", $this->distinct, implode(',', array_merge($cols, $this->special_cols)), $this->table, $this->filter);
  279. $sth = (Db::pdo())->prepare($sql);
  280. $sth = $this->formatParam($sth, $this->param);
  281. if ($sth->execute() == false)
  282. {
  283. // have error!
  284. $this->error = $sth->errorInfo();
  285. }
  286. return new SqlResult($sth->fetch($dataType), $this->error);
  287. }
  288. /**
  289. * rowCount function:
  290. *
  291. * ie:
  292. * 1. $this->table('login')->where(['username=?', 'AND hashed=?'], [$username, $hashed])->rowCount();
  293. *
  294. * @return int $sth->fetchColumn()
  295. */
  296. public function rowCount()
  297. {
  298. $sql = sprintf("SELECT %s COUNT(*) FROM `%s` %s", $this->distinct, $this->table, $this->filter);
  299. $sth = Db::pdo()->prepare($sql);
  300. $sth = $this->formatParam($sth, $this->param);
  301. if ($sth->execute() == false)
  302. {
  303. // have error!
  304. $this->error = $sth->errorInfo();
  305. }
  306. return new SqlResult((int)$sth->fetchColumn(), $this->error);
  307. }
  308. /**
  309. * delete function:
  310. *
  311. * ie:
  312. * 1. $this->table('login')->where(['username=?', 'AND hashed=?'], [$username, $hashed])->delete();
  313. *
  314. * @return integer $sth->rowCount() : return the number of changed rows.
  315. * return 0 when no changes.
  316. * return -1 when trying to delete the whole database.
  317. */
  318. public function delete()
  319. {
  320. if (empty($this->filter))
  321. return -1; // do not allow delete whole db.
  322. $sql = sprintf("DELETE FROM `%s` %s", $this->table, $this->filter);
  323. $sth = Db::pdo()->prepare($sql);
  324. $sth = $this->formatParam($sth, $this->param);
  325. if ($sth->execute() == false)
  326. {
  327. // have error!
  328. $this->error = $sth->errorInfo();
  329. }
  330. return new SqlResult($sth->rowCount(), $this->error);
  331. }
  332. /**
  333. * add function:
  334. *
  335. * ie:
  336. * 1. $this->table('login')->add($data);
  337. *
  338. * @param array $data : the data with column name & data
  339. * ie:
  340. * $data = [
  341. * "username" => "user01",
  342. * "hash" => "678e82d907d3e6e71f81d5cf3ddacc3671dc618c38a1b7a9f9393a83d025b296" // plaintext: test01
  343. * ];
  344. *
  345. * @return integer $sth->rowCount() : return the number of changed rows.
  346. * return 0 when insertion failed.
  347. */
  348. public function add($data)
  349. {
  350. $sql = sprintf("INSERT INTO `%s` %s", $this->table, $this->formatInsert($data));
  351. $sth = Db::pdo()->prepare($sql);
  352. $sth = $this->formatParam($sth, $data);
  353. if ($sth->execute() == false)
  354. {
  355. // have error!
  356. print_r($sth->errorInfo());
  357. $this->error = $sth->errorInfo();
  358. }
  359. return new SqlResult($sth->rowCount(), $this->error);
  360. }
  361. /**
  362. * update function:
  363. *
  364. * ie:
  365. * 1. $this->table('login')->where(['id=1'])->update($data);
  366. *
  367. * @param array $data : the data with column name & data
  368. * ie:
  369. * $data = [
  370. * "username" => "user01",
  371. * "hash" => "678e82d907d3e6e71f81d5cf3ddacc3671dc618c38a1b7a9f9393a83d025b296" // plaintext: test01
  372. * ];
  373. *
  374. * @return integer $sth->rowCount() : return the number of changed rows.
  375. * return 0 when update failed.
  376. */
  377. public function update($data)
  378. {
  379. $sql = "UPDATE `{$this->table}` SET {$this->formatUpdate($data)} {$this->filter}";
  380. $sth = Db::pdo()->prepare($sql);
  381. $sth = $this->formatParam($sth, $data);
  382. $sth = $this->formatParam($sth, $this->param);
  383. if ($sth->execute() == false)
  384. {
  385. // have error!
  386. $this->error = $sth->errorInfo();
  387. }
  388. return new SqlResult($sth->rowCount(), $this->error);
  389. }
  390. /**
  391. * paramesters binding
  392. * @param PDOStatement $sth PDOStatement that want to be binded
  393. * @param array $params value of params:
  394. * 1. if using "?" as binding symbol, then $params should be like:
  395. * [$a, $b, $c]
  396. * 2. if using ":" as binding symbol, then $params should be like:
  397. * 2.1 without binding symbol:
  398. * ['a' => $a, 'b' => $b, 'c' => $c]
  399. * OR
  400. * 2.2 with binding symbol (":"):
  401. * [':a' => $a, ':b' => $b, ':c' => $c]
  402. *
  403. * @return PDOStatement
  404. */
  405. private function formatParam(PDOStatement $sth, $params = [])
  406. {
  407. foreach ($params as $param => &$value) {
  408. $param = is_int($param) ? $param + 1 : ':' . ltrim($param, ':');
  409. if (gettype($value) == "array")
  410. $sth->bindParam($param, $value[0], $value[1], $value[2] ?? sizeof($value[0]));
  411. else
  412. $sth->bindParam($param, $value);
  413. }
  414. return $sth;
  415. }
  416. // parse data to insertion SQL format (value pairs)
  417. private function formatInsert($data)
  418. {
  419. $fields = [];
  420. $names = [];
  421. foreach ($data as $key => $value) {
  422. $fields[] = "`{$key}`";
  423. $names[] = ":{$key}";
  424. }
  425. $field = implode(',', $fields);
  426. $name = implode(',', $names);
  427. return "({$field}) values ({$name})";
  428. }
  429. // parse data to update SQL format (key-value pairs)
  430. private function formatUpdate($data)
  431. {
  432. $fields = [];
  433. foreach ($data as $key => $value) {
  434. $fields[] = "`{$key}` = :{$key}";
  435. }
  436. return implode(',', $fields);
  437. }
  438. // database DATETIME() function
  439. protected function DATETIME()
  440. {
  441. return date('Y-m-d H:i:s');
  442. }
  443. protected function debugDump($sth, $useOfficial = true)
  444. {
  445. $useOfficial ? $sth->debugDumpParams() : var_dump($sth, $this->param);
  446. }
  447. }
  448. $sql = new Sql();
  449. function generateRandomString($length) {
  450. // src: https://stackoverflow.com/a/13212994
  451. return substr(str_shuffle(str_repeat($x='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', ceil($length/strlen($x)) )),1,$length);
  452. }