Oppaitime's version of Gazelle
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

mysql.class.php 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. <?php
  2. //-----------------------------------------------------------------------------------
  3. /////////////////////////////////////////////////////////////////////////////////////
  4. /*//-- MySQL wrapper class ----------------------------------------------------------
  5. This class provides an interface to mysqli. You should always use this class instead
  6. of the mysql/mysqli functions, because this class provides debugging features and a
  7. bunch of other cool stuff.
  8. Everything returned by this class is automatically escaped for output. This can be
  9. turned off by setting $Escape to false in next_record or to_array.
  10. //--------- Basic usage -------------------------------------------------------------
  11. * Creating the object.
  12. require(SERVER_ROOT.'/classes/mysql.class.php');
  13. $DB = NEW DB_MYSQL;
  14. -----
  15. * Making a query
  16. $DB->query("
  17. SELECT *
  18. FROM table...");
  19. Is functionally equivalent to using mysqli_query("SELECT * FROM table...")
  20. Stores the result set in $this->QueryID
  21. Returns the result set, so you can save it for later (see set_query_id())
  22. -----
  23. * Getting data from a query
  24. $array = $DB->next_record();
  25. Is functionally equivalent to using mysqli_fetch_array($ResultSet)
  26. You do not need to specify a result set - it uses $this-QueryID
  27. -----
  28. * Escaping a string
  29. db_string($str);
  30. Is a wrapper for $DB->escape_str(), which is a wrapper for
  31. mysqli_real_escape_string(). The db_string() function exists so that you
  32. don't have to keep calling $DB->escape_str().
  33. USE THIS FUNCTION EVERY TIME YOU USE AN UNVALIDATED USER-SUPPLIED VALUE IN
  34. A DATABASE QUERY!
  35. //--------- Advanced usage ---------------------------------------------------------
  36. * The conventional way of retrieving a row from a result set is as follows:
  37. list($All, $Columns, $That, $You, $Select) = $DB->next_record();
  38. -----
  39. * This is how you loop over the result set:
  40. while (list($All, $Columns, $That, $You, $Select) = $DB->next_record()) {
  41. echo "Do stuff with $All of the ".$Columns.$That.$You.$Select;
  42. }
  43. -----
  44. * There are also a couple more mysqli functions that have been wrapped. They are:
  45. record_count()
  46. Wrapper to mysqli_num_rows()
  47. affected_rows()
  48. Wrapper to mysqli_affected_rows()
  49. inserted_id()
  50. Wrapper to mysqli_insert_id()
  51. close
  52. Wrapper to mysqli_close()
  53. -----
  54. * And, of course, a few handy custom functions.
  55. to_array($Key = false)
  56. Transforms an entire result set into an array (useful in situations where you
  57. can't order the rows properly in the query).
  58. If $Key is set, the function uses $Key as the index (good for looking up a
  59. field). Otherwise, it uses an iterator.
  60. For an example of this function in action, check out forum.php.
  61. collect($Key)
  62. Loops over the result set, creating an array from one of the fields ($Key).
  63. For an example, see forum.php.
  64. set_query_id($ResultSet)
  65. This class can only hold one result set at a time. Using set_query_id allows
  66. you to set the result set that the class is using to the result set in
  67. $ResultSet. This result set should have been obtained earlier by using
  68. $DB->query().
  69. Example:
  70. $FoodRS = $DB->query("
  71. SELECT *
  72. FROM food");
  73. $DB->query("
  74. SELECT *
  75. FROM drink");
  76. $Drinks = $DB->next_record();
  77. $DB->set_query_id($FoodRS);
  78. $Food = $DB->next_record();
  79. Of course, this example is contrived, but you get the point.
  80. -------------------------------------------------------------------------------------
  81. *///---------------------------------------------------------------------------------
  82. if (!extension_loaded('mysqli')) {
  83. die('Mysqli Extension not loaded.');
  84. }
  85. //Handles escaping
  86. function db_string($String, $DisableWildcards = false)
  87. {
  88. global $DB;
  89. //Escape
  90. $String = $DB->escape_str($String);
  91. //Remove user input wildcards
  92. if ($DisableWildcards) {
  93. $String = str_replace(array('%','_'), array('\%','\_'), $String);
  94. }
  95. return $String;
  96. }
  97. function db_array($Array, $DontEscape = [], $Quote = false)
  98. {
  99. foreach ($Array as $Key => $Val) {
  100. if (!in_array($Key, $DontEscape)) {
  101. if ($Quote) {
  102. $Array[$Key] = '\''.db_string(trim($Val)).'\'';
  103. } else {
  104. $Array[$Key] = db_string(trim($Val));
  105. }
  106. }
  107. }
  108. return $Array;
  109. }
  110. //TODO: revisit access levels once Drone is replaced by ZeRobot
  111. class DB_MYSQL
  112. {
  113. public $LinkID = false;
  114. protected $QueryID = false;
  115. protected $StatementID = false;
  116. protected $PreparedQuery = false;
  117. protected $Record = [];
  118. protected $Row;
  119. protected $Errno = 0;
  120. protected $Error = '';
  121. public $Queries = [];
  122. public $Time = 0.0;
  123. protected $Database = '';
  124. protected $Server = '';
  125. protected $User = '';
  126. protected $Pass = '';
  127. protected $Port = 0;
  128. protected $Socket = '';
  129. public function __construct($Database = SQLDB, $User = SQLLOGIN, $Pass = SQLPASS, $Server = SQLHOST, $Port = SQLPORT, $Socket = SQLSOCK)
  130. {
  131. $this->Database = $Database;
  132. $this->Server = $Server;
  133. $this->User = $User;
  134. $this->Pass = $Pass;
  135. $this->Port = $Port;
  136. $this->Socket = $Socket;
  137. }
  138. public function halt($Msg)
  139. {
  140. global $Debug, $argv;
  141. $DBError = 'MySQL: '.strval($Msg).' SQL error: '.strval($this->Errno).' ('.strval($this->Error).')';
  142. if ($this->Errno == 1194) {
  143. send_irc('PRIVMSG '.ADMIN_CHAN.' :'.$this->Error);
  144. }
  145. $Debug->analysis('!dev DB Error', $DBError, 3600 * 24);
  146. if (DEBUG_MODE || check_perms('site_debug') || isset($argv[1])) {
  147. echo '<pre>'.display_str($DBError).'</pre>';
  148. if (DEBUG_MODE || check_perms('site_debug')) {
  149. print_r($this->Queries);
  150. }
  151. die();
  152. } else {
  153. error('-1');
  154. }
  155. }
  156. public function connect()
  157. {
  158. if (!$this->LinkID) {
  159. $this->LinkID = mysqli_connect($this->Server, $this->User, $this->Pass, $this->Database, $this->Port, $this->Socket); // defined in config.php
  160. if (!$this->LinkID) {
  161. $this->Errno = mysqli_connect_errno();
  162. $this->Error = mysqli_connect_error();
  163. $this->halt('Connection failed (host:'.$this->Server.':'.$this->Port.')');
  164. }
  165. }
  166. mysqli_set_charset($this->LinkID, "utf8mb4");
  167. }
  168. public function prepare_query($Query, &...$BindVars)
  169. {
  170. $this->connect();
  171. $this->StatementID = mysqli_prepare($this->LinkID, $Query);
  172. if (!empty($BindVars)) {
  173. $Types = '';
  174. $TypeMap = ['string'=>'s', 'double'=>'d', 'integer'=>'i', 'boolean'=>'i'];
  175. foreach ($BindVars as $BindVar) {
  176. $Types .= $TypeMap[gettype($BindVar)] ?? 'b';
  177. }
  178. mysqli_stmt_bind_param($this->StatementID, $Types, ...$BindVars);
  179. }
  180. $this->PreparedQuery = $Query;
  181. return $this->StatementID;
  182. }
  183. public function exec_prepared_query()
  184. {
  185. $QueryStartTime = microtime(true);
  186. mysqli_stmt_execute($this->StatementID);
  187. $this->QueryID = mysqli_stmt_get_result($this->StatementID);
  188. $QueryRunTime = (microtime(true) - $QueryStartTime) * 1000;
  189. $this->Queries[] = [$this->PreppedQuery, $QueryRunTime, null];
  190. $this->Time += $QueryRunTime;
  191. }
  192. public function query($Query, &...$BindVars)
  193. {
  194. global $Debug;
  195. /*
  196. * If there was a previous query, we store the warnings. We cannot do
  197. * this immediately after mysqli_query because mysqli_insert_id will
  198. * break otherwise due to mysqli_get_warnings sending a SHOW WARNINGS;
  199. * query. When sending a query, however, we're sure that we won't call
  200. * mysqli_insert_id (or any similar function, for that matter) later on,
  201. * so we can safely get the warnings without breaking things.
  202. * Note that this means that we have to call $this->warnings manually
  203. * for the last query!
  204. */
  205. if ($this->QueryID) {
  206. $this->warnings();
  207. }
  208. $QueryStartTime = microtime(true);
  209. $this->connect();
  210. // In the event of a MySQL deadlock, we sleep allowing MySQL time to unlock, then attempt again for a maximum of 5 tries
  211. for ($i = 1; $i < 6; $i++) {
  212. $this->StatementID = mysqli_prepare($this->LinkID, $Query);
  213. if (!empty($BindVars)) {
  214. $Types = '';
  215. $TypeMap = ['string'=>'s', 'double'=>'d', 'integer'=>'i', 'boolean'=>'i'];
  216. foreach ($BindVars as $BindVar) {
  217. $Types .= $TypeMap[gettype($BindVar)] ?? 'b';
  218. }
  219. mysqli_stmt_bind_param($this->StatementID, $Types, ...$BindVars);
  220. }
  221. mysqli_stmt_execute($this->StatementID);
  222. $this->QueryID = mysqli_stmt_get_result($this->StatementID);
  223. if (DEBUG_MODE) {
  224. // in DEBUG_MODE, return the full trace on a SQL error (super useful
  225. // for debugging). do not attempt to retry to query
  226. if (!$this->QueryID) {
  227. echo '<pre>' . mysqli_error($this->LinkID) . '<br><br>';
  228. debug_print_backtrace();
  229. echo '</pre>';
  230. die();
  231. }
  232. }
  233. if (!in_array(mysqli_errno($this->LinkID), array(1213, 1205))) {
  234. break;
  235. }
  236. $Debug->analysis('Non-Fatal Deadlock:', $Query, 3600 * 24);
  237. trigger_error("Database deadlock, attempt $i");
  238. sleep($i * rand(2, 5)); // Wait longer as attempts increase
  239. }
  240. $QueryEndTime = microtime(true);
  241. $this->Queries[] = array($Query, ($QueryEndTime - $QueryStartTime) * 1000, null);
  242. $this->Time += ($QueryEndTime - $QueryStartTime) * 1000;
  243. if (!$this->QueryID && !$this->StatementID) {
  244. $this->Errno = mysqli_errno($this->LinkID);
  245. $this->Error = mysqli_error($this->LinkID);
  246. $this->halt("Invalid Query: $Query");
  247. }
  248. $this->Row = 0;
  249. return $this->QueryID;
  250. }
  251. public function query_unb($Query)
  252. {
  253. $this->connect();
  254. mysqli_real_query($this->LinkID, $Query);
  255. }
  256. public function inserted_id()
  257. {
  258. if ($this->LinkID) {
  259. return mysqli_insert_id($this->LinkID);
  260. }
  261. }
  262. public function next_record($Type = MYSQLI_BOTH, $Escape = true)
  263. {
  264. // $Escape can be true, false, or an array of keys to not escape
  265. if ($this->LinkID) {
  266. $this->Record = mysqli_fetch_array($this->QueryID, $Type);
  267. $this->Row++;
  268. if (!is_array($this->Record)) {
  269. $this->QueryID = false;
  270. } elseif ($Escape !== false) {
  271. $this->Record = Misc::display_array($this->Record, $Escape);
  272. }
  273. return $this->Record;
  274. }
  275. }
  276. public function close()
  277. {
  278. if ($this->LinkID) {
  279. if (!mysqli_close($this->LinkID)) {
  280. $this->halt('Cannot close connection or connection did not open.');
  281. }
  282. $this->LinkID = false;
  283. }
  284. }
  285. /*
  286. * returns an integer with the number of rows found
  287. * returns a string if the number of rows found exceeds MAXINT
  288. */
  289. public function record_count()
  290. {
  291. if ($this->QueryID) {
  292. return mysqli_num_rows($this->QueryID);
  293. }
  294. }
  295. /*
  296. * returns true if the query exists and there were records found
  297. * returns false if the query does not exist or if there were 0 records returned
  298. */
  299. public function has_results()
  300. {
  301. return ($this->QueryID && $this->record_count() !== 0);
  302. }
  303. public function affected_rows()
  304. {
  305. if ($this->LinkID) {
  306. return mysqli_affected_rows($this->LinkID);
  307. }
  308. }
  309. public function info()
  310. {
  311. return mysqli_get_host_info($this->LinkID);
  312. }
  313. // You should use db_string() instead.
  314. public function escape_str($Str)
  315. {
  316. $this->connect(0);
  317. if (is_array($Str)) {
  318. trigger_error('Attempted to escape array.');
  319. return '';
  320. }
  321. return mysqli_real_escape_string($this->LinkID, $Str);
  322. }
  323. // Creates an array from a result set
  324. // If $Key is set, use the $Key column in the result set as the array key
  325. // Otherwise, use an integer
  326. public function to_array($Key = false, $Type = MYSQLI_BOTH, $Escape = true)
  327. {
  328. $Return = [];
  329. while ($Row = mysqli_fetch_array($this->QueryID, $Type)) {
  330. if ($Escape !== false) {
  331. $Row = Misc::display_array($Row, $Escape);
  332. }
  333. if ($Key !== false) {
  334. $Return[$Row[$Key]] = $Row;
  335. } else {
  336. $Return[] = $Row;
  337. }
  338. }
  339. mysqli_data_seek($this->QueryID, 0);
  340. return $Return;
  341. }
  342. // Loops through the result set, collecting the $ValField column into an array with $KeyField as keys
  343. public function to_pair($KeyField, $ValField, $Escape = true)
  344. {
  345. $Return = [];
  346. while ($Row = mysqli_fetch_array($this->QueryID)) {
  347. if ($Escape) {
  348. $Key = display_str($Row[$KeyField]);
  349. $Val = display_str($Row[$ValField]);
  350. } else {
  351. $Key = $Row[$KeyField];
  352. $Val = $Row[$ValField];
  353. }
  354. $Return[$Key] = $Val;
  355. }
  356. mysqli_data_seek($this->QueryID, 0);
  357. return $Return;
  358. }
  359. // Loops through the result set, collecting the $Key column into an array
  360. public function collect($Key, $Escape = true)
  361. {
  362. $Return = [];
  363. while ($Row = mysqli_fetch_array($this->QueryID)) {
  364. $Return[] = $Escape ? display_str($Row[$Key]) : $Row[$Key];
  365. }
  366. mysqli_data_seek($this->QueryID, 0);
  367. return $Return;
  368. }
  369. public function set_query_id(&$ResultSet)
  370. {
  371. $this->QueryID = $ResultSet;
  372. $this->Row = 0;
  373. }
  374. public function get_query_id()
  375. {
  376. return $this->QueryID;
  377. }
  378. public function beginning()
  379. {
  380. mysqli_data_seek($this->QueryID, 0);
  381. $this->Row = 0;
  382. }
  383. /**
  384. * This function determines whether the last query caused warning messages
  385. * and stores them in $this->Queries.
  386. */
  387. public function warnings()
  388. {
  389. $Warnings = [];
  390. if (!is_bool($this->LinkID) && mysqli_warning_count($this->LinkID)) {
  391. $e = mysqli_get_warnings($this->LinkID);
  392. do {
  393. if ($e->errno == 1592) {
  394. // 1592: Unsafe statement written to the binary log using statement format since BINLOG_FORMAT = STATEMENT.
  395. continue;
  396. }
  397. $Warnings[] = 'Code ' . $e->errno . ': ' . display_str($e->message);
  398. } while ($e->next());
  399. }
  400. $this->Queries[count($this->Queries) - 1][2] = $Warnings;
  401. }
  402. }