JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
fix (notice level) syntax error
[wfpl.git] / db.php
1 <?php
2
3 # This program is in the public domain within the United States. Additionally,
4 # we waive copyright and related rights in the work worldwide through the CC0
5 # 1.0 Universal public domain dedication, which can be found at
6 # http://creativecommons.org/publicdomain/zero/1.0/
7
8
9 require_once(__DIR__.'/'.'encode.php');
10 require_once(__DIR__.'/'.'format.php');
11
12 # db_connect() -- connect to a mysql database (automatically, later, when/if needed)
13 # db_connect_now() -- connect to a mysql database immediately
14 #
15 # PARAMETERS:
16 #
17 #   database: the name of the database you want to connect to. Defaults to the
18 #   second-to-last part of the domain name. eg for foo.example.com it would be
19 #   "example".
20
21 #   user: username for connecting to the database. Defaults to
22 #   $GLOBALS['db_username'] or (if that's not set) "www".
23
24 #   password: password for connecting to the database. Defaults to
25 #   $GLOBALS['db_password'] or (if that's not set "".
26 #
27 # RETURNS:
28 #
29 #   the database connection handle. You'll only need this if you want to have
30 #   multiple databases open at once.
31 function db_connect() {
32         if (isset($GLOBALS['wfpl_db_handle'])) {
33                 mysqli_close($GLOBALS['wfpl_db_handle']);
34                 unset($GLOBALS['wfpl_db_handle']);
35         }
36         $GLOBALS['wfpl_db_connect_args'] = func_get_args();
37 }
38 function db_connect_now($database = 'auto', $user = 'auto', $pass = 'auto', $host = 'localhost', $encoding = 'utf8') {
39         if($database == 'auto') {
40                 if(isset($GLOBALS['db_name'])) {
41                         $database = $GLOBALS['db_name'];
42                 } else {
43                         $host = $_SERVER['SERVER_NAME'];
44                         $host = explode('.', $host);
45                         array_pop($host);
46                         $database = array_pop($host);
47                         unset($host);
48                 }
49         }
50
51         if($user == 'auto') {
52                 if(isset($GLOBALS['db_username'])) {
53                         $user = $GLOBALS['db_username'];
54                 } else {
55                         $user = 'www';
56                 }
57         }
58
59         if($pass == 'auto') {
60                 if(isset($GLOBALS['db_password'])) {
61                         $pass = $GLOBALS['db_password'];
62                 } else {
63                         $pass = '';
64                 }
65         }
66
67         $GLOBALS['wfpl_db_handle'] = mysqli_connect($host, $user, $pass);
68         if(!$GLOBALS['wfpl_db_handle']) {
69                 die('Could not connect to the database: ' . mysqli_error());
70         }
71
72         mysqli_set_charset($GLOBALS['wfpl_db_handle'], $encoding);
73
74         if(!mysqli_select_db($GLOBALS['wfpl_db_handle'], $database)) {
75                 die("Couldn not access database \"$database\": " . mysqli_error($GLOBALS['wfpl_db_handle']));
76         }
77
78         return $GLOBALS['wfpl_db_handle'];
79 }
80 # for internal use (helps implement the auto-connect feature of db_connect())
81 function _db_connection_needed() {
82         if (isset($GLOBALS['wfpl_db_handle'])) {
83                 return;
84         }
85         if (isset($GLOBALS['wfpl_db_connect_args'])) {
86                 return call_user_func_array('db_connect_now', $GLOBALS['wfpl_db_connect_args']);
87         }
88         die('Error: you must call db_connect() or db_auto_connect() first!');
89 }
90
91 function db_enc_sql($str) {
92         _db_connection_needed();
93         return mysqli_real_escape_string(isset($GLOBALS['wfpl_db_handle']) ? $GLOBALS['wfpl_db_handle'] : null, $str);
94 }
95
96 # Unless you're doing something unusual like an ALTER TABLE don't call this directly
97 function db_send_query($sql) {
98         _db_connection_needed();
99         #echo("Sending query: " . enc_html($sql) . "<br>\n");
100         $result = mysqli_query($GLOBALS['wfpl_db_handle'], $sql);
101         if(!$result) {
102                 die(enc_html('DATABASE ERROR: ' . mysqli_error($GLOBALS['wfpl_db_handle']) . ' in the following query: ' . $sql));
103         }
104
105         return $result;
106 }
107
108 # All select queries use this to generate the where clause, so they can work
109 # like printf. Currently three % codes are supported:
110 #
111 # %%  put a % in the output
112 # %i  put an integer in the output (strips non-numeric digits, and puts in 0 if blank)
113 # %f  put a floating point value in the output (strips non-numeric digits, puts in 0.0 if not valid)
114 # %"  output double quotes, surrounding the variable which is encoded to be in there.
115 # %s  output encoded to be in double quotes, but don't output the quotes
116 # %$  output argument as-is, no encoding. Make sure you quote everything from the user!
117 #
118 # complex example: db_get_rows('mytable', 'id', 'where name=%" or company like "%%%s%%"', $name, $company_partial);
119
120 function db_printf($str) {
121         $args = func_get_args();
122         $args = array_slice($args, 1);
123         return _db_printf($str, $args);
124 }
125
126 # This function does the work, but takes the parameters in an array
127 function _db_printf($str, $args) {
128         $out = '';
129         while($str) {
130                 $pos = strpos($str, '%');
131                 if($pos === false) { # not found
132                         # we hit the end.
133                         return $out . $str;
134                 }
135                 # move everything up to (but not including) % to the output
136                 $out .= substr($str, 0, $pos);
137
138                 # grab the character after the %
139                 $chr = substr($str, $pos + 1, 1);
140
141                 # remove the stuff we've read from input
142                 $str = substr($str, $pos + 2);
143
144                 if($chr == '"') {
145                         $out .= '"' . db_enc_sql(array_shift($args)) . '"';
146                 } elseif($chr == 's') {
147                         $out .= db_enc_sql(array_shift($args));
148                 } elseif($chr == 'i') {
149                         $int = format_int(array_shift($args));
150                         if($int == '') $int = '0';
151                         $out .= $int;
152                 } elseif($chr == 'f') {
153                         $arg = array_shift($args);
154                         if(is_numeric($arg)) {
155                                 $arg = sprintf("%f", $arg);
156                         }
157                         $arg = format_decimal($arg);
158                         if(strlen($arg) < 1) {
159                                 $arg = '0.0';
160                         }
161                         $out .= $arg;
162                 } elseif($chr == '$') {
163                         $out .= array_shift($args);
164                 } else {
165                         $out .= $chr;
166                 }
167         }
168
169         return $out;
170 }
171
172
173 # helper function
174 function db_send_get($table, $columns, $where, $args) {
175         $sql = "SELECT $columns FROM $table";
176         if($where) {
177                 $sql .= ' ' . _db_printf($where, $args);
178         }
179
180         return db_send_query($sql);
181 }
182
183
184 # if no results: returs []
185 function db_get_rows($table, $columns, $where = '') {
186         $args = func_get_args();
187         $args = array_slice($args, 3);
188         $result = db_send_get($table, $columns, $where, $args);
189
190         $rows = array();
191         while($row = mysqli_fetch_row($result)) {
192                 $rows[] = $row;
193         }
194
195         mysqli_free_result($result);
196
197         return $rows;
198 }
199
200 # like db_get_rows, but return array of hashes.
201 # if no results: returs []
202 function db_get_assocs($table, $columns, $where = '') {
203         $args = func_get_args();
204         $args = array_slice($args, 3);
205         $result = db_send_get($table, $columns, $where, $args);
206
207         $rows = array();
208         while($row = mysqli_fetch_assoc($result)) {
209                 $rows[] = $row;
210         }
211
212         mysqli_free_result($result);
213
214         return $rows;
215 }
216
217 # if no results: returs []
218 function db_get_column($table, $columns, $where = '') {
219         $args = func_get_args();
220         $args = array_slice($args, 3);
221         $result = db_send_get($table, $columns, $where, $args);
222
223         $column = array();
224         while($row = mysqli_fetch_row($result)) {
225                 $column[] = $row[0];
226         }
227
228         mysqli_free_result($result);
229
230         return $column;
231 }
232
233 # returns first matching row
234 # if no results: returns false
235 function db_get_row($table, $columns, $where = '') {
236         $args = func_get_args();
237         $args = array_slice($args, 3);
238         $result = db_send_get($table, $columns, $where, $args);
239
240         $row = mysqli_fetch_row($result);
241
242         mysqli_free_result($result);
243
244         return $row;
245 }
246
247 # like db_get_row, but return a hash.
248 # if no results: returns false
249 function db_get_assoc($table, $columns, $where = '') {
250         $args = func_get_args();
251         $args = array_slice($args, 3);
252         $result = db_send_get($table, $columns, $where, $args);
253
254         $row = mysqli_fetch_assoc($result);
255
256         mysqli_free_result($result);
257
258         return $row;
259 }
260
261 # if no results: returns false
262 function db_get_value($table, $column, $where = '') {
263         $args = func_get_args();
264         $args = array_slice($args, 3);
265         $result = db_send_get($table, $column, $where, $args);
266
267         $value = mysqli_fetch_row($result);
268         if($value !== false) {
269                 $value = $value[0];
270         }
271
272         mysqli_free_result($result);
273
274         return $value;
275 }
276
277 # returns an integer
278 function db_count($table, $where = '') {
279         $args = func_get_args();
280         array_splice($args, 1, 0, array('count(*)'));
281         return (int) call_user_func_array('db_get_value', $args);
282 }
283
284 # call either of these ways:
285 #
286 # db_insert('people', 'name,company', 'jason', 'widgets ltd');
287 # or
288 # db_insert('people', 'name,company', array('jason', 'widgets ltd'));
289 function db_insert($table, $columns, $values) {
290         if(!is_array($values)) {
291                 $values = func_get_args();
292                 $values = array_slice($values, 2);
293         }
294         
295         db_insert_ish('INSERT', $table, $columns, $values);
296 }
297
298 # like db_insert() above, but instead of passing columns and data separately,
299 # you can pass one array with the column names as keys and the data as values
300 function db_insert_assoc($table, $data) {
301         $args = func_get_args();
302         $args = array_slice($args, 2);
303         $columns = array();
304         $values = array();
305         foreach($data as $key => $value) {
306                 $columns[] = $key;
307                 $values[] = $value;
308         }
309         array_unshift($args, $table, join(',', $columns), $values);
310         call_user_func_array('db_insert', $args);
311 }
312
313 # same as above, except uses the "replace" command instead of "insert"
314 function db_replace($table, $columns, $values) {
315         if(!is_array($values)) {
316                 $values = func_get_args();
317                 $values = array_slice($values, 2);
318         }
319         
320         db_insert_ish('REPLACE', $table, $columns, $values);
321 }
322         
323 # return the value mysql made up for the auto_increment field (for the last insert)
324 function db_auto_id() {
325         _db_connection_needed();
326         return mysqli_insert_id($GLOBALS['wfpl_db_handle']);
327 }
328
329
330 # used to implement db_insert() and db_replace()
331 function db_insert_ish($command, $table, $columns, $values) {
332
333         $sql = '';
334         foreach($values as $value) {
335                 if($sql) $sql .= ',';
336                 $sql .= '"' . db_enc_sql($value) . '"';
337         }
338
339         $sql = "$command INTO $table ($columns) values($sql)";
340
341         db_send_query($sql);
342 }
343
344 # to be consistent with the syntax of the other db functions, $values can be an
345 # array, a single value, or multiple parameters.
346 #
347 # as usual the where clause stuff is optional, but it will of course update the
348 # whole table if you leave it off.
349 #
350 # examples:
351 #
352 # # name everybody Bruce
353 # db_update('users', 'name', 'Bruce');
354 #
355 # # name user #6 Bruce
356 # db_update('users', 'name', 'Bruce', 'where id=%i', 6);
357 #
358 # # update the whole bit for user #6
359 # db_update('users', 'name,email,description', 'Bruce', 'bruce@example.com', 'is a cool guy', 'where id=%i', 6);
360 #
361 # # update the whole bit for user #6 (passing data as an array)
362 # $data = array('Bruce', 'bruce@example.com', 'is a cool guy');
363 # db_update('users', 'name,email,description', $data, 'where id=%i', 6);
364
365 # The prototype is really something like this:
366 # db_update(table, columns, values..., where(optional), where_args...(optional))
367 function db_update($table, $columns, $values) {
368         $args = func_get_args();
369         $args = array_slice($args, 2);
370         $columns = explode(',', $columns);
371         $num_fields = count($columns);
372
373         if(is_array($values)) {
374                 $values = array_values($values);
375                 $args = array_slice($args, 1);
376         } else {
377                 $values = array_slice($args, 0, $num_fields);
378                 $args = array_slice($args, $num_fields);
379         }
380
381         $sql = '';
382         for($i = 0; $i < $num_fields; ++$i) {
383                 if($sql != '') {
384                         $sql .= ', ';
385                 }
386                 $sql .= $columns[$i] . ' = "' . db_enc_sql($values[$i]) . '"';
387         }
388
389
390         $sql = "UPDATE $table SET $sql";
391
392         # if there's any more arguments
393         if($args) {
394                 $where = $args[0];
395                 $args = array_slice($args, 1);
396
397                 $sql .= ' ';
398                 # any left for printf arguments?
399                 if($args) {
400                         $sql .= _db_printf($where, $args);
401                 } else {
402                         $sql .= $where;
403                 }
404
405         }
406
407         db_send_query($sql);
408 }
409
410 # like db_update() above, but instead of passing columns and data separately,
411 # you can pass one array with the column names as keys and the data as values
412 function db_update_assoc($table, $data) {
413         $args = func_get_args();
414         $args = array_slice($args, 2);
415         $columns = array();
416         $values = array();
417         foreach($data as $key => $value) {
418                 $columns[] = $key;
419                 $values[] = $value;
420         }
421         array_unshift($args, $values);
422         array_unshift($args, join(',', $columns));
423         array_unshift($args, $table);
424         call_user_func_array('db_update', $args);
425 }
426
427 # pass args for printf-style where clause as usual
428 function db_delete($table, $where = '') {
429         $sql = "DELETE FROM $table";
430         if($where) {
431                 $sql .= ' ';
432                 $args = func_get_args();
433                 $args = array_slice($args, 2);
434                 if($args) {
435                         $sql .= _db_printf($where, $args);
436                 } else {
437                         $sql .= $where;
438                 }
439         }
440
441         db_send_query($sql);
442 }
443
444
445 define('DB_ORD_MAX', 2000000000);
446
447 function db_reposition_respace($table, $field, $where = '') {
448         if($where) {
449                 $andand = " && ($where) ";
450         }
451         $ids = db_get_column($table, 'id', "where $field != 0 $andand order by $field");
452         $c = count($ids);
453         if(!$c) {
454                 # should never happen
455                 return;
456         }
457         $inc = floor(DB_ORD_MAX / ($c + 1));
458         $cur = $inc;
459         foreach($ids as $id) {
460                 db_update($table, $field, $cur, 'where id=%i', $id);
461                 $cur += $inc;
462         }
463 }
464
465 # this function facilitates letting the user manually sort records (with (int) $field != 0)
466 #
467 # When editing a particular row, give the user a pulldown, with 0 -> first, 1 -> second, etc, and pass this integer to db_reposition (3rd parameter). The value "ignored" can be passed, and the row will be given a sort value of 0 and ignored for all sorting.
468 #
469 # $pretty is used in error messages to refer to the row, it defaults to whatever you pass for $table.
470 #
471 # return value is the "ord" value you should set/insert into your database
472
473 function db_reposition($table, $row_id, $new_pos, $field = 'ord', $pretty = 'same as $table', $where = '', $renumbered_already = false) {
474         if($pretty == 'same as $table') {
475                 $pretty = $table;
476         }
477         if($where) {
478                 $andand = " && ($where) ";
479         }
480
481         if($new_pos === 'ignored') {
482                 # not sorted
483                 return '0';
484         }
485
486         # strategy: calculate $prev_ord and $next_ord. If there's no space between, renumber and recurse
487         if($new_pos == '0') {
488                 $row = db_get_row($table, "id,$field", "where $field != 0 $andand order by $field limit 1");
489                 if($row) {
490                         list($first_row_id, $first_row_ord) = $row;
491                         if($first_row_id == $row_id) {
492                                 # already first
493                                 return $first_row_ord;
494                         }
495                         $next_ord = $first_row_ord;
496                 } else {
497                         # this is the only row, put it in the middle
498                         return '' + floor(DB_ORD_MAX / 2);
499                 }
500
501                 $prev_ord = 0;
502         } else {
503                 $new_pos = format_int_0($new_pos);
504                 $rows = db_get_rows($table, "id,$field", "where $field != 0 $andand order by $field limit %i,2", $new_pos - 1);
505                 if(!$rows) {
506                         message("Sorry, couldn't find the $pretty you asked to put this $pretty after. Putting it first instead.");
507                         return db_reposition($table, $row_id, '0', $field, $pretty, $where);
508                 } else {
509                         list($prev_id, $prev_ord) = $rows[0];
510                         if($prev_id == $row_id) {
511                                 # after self? this shouldn't happen
512                                 return $prev_ord;
513                         }
514                         if(count($rows) == 1) {
515                                 # we should be last
516                                 $next_ord = DB_ORD_MAX;
517                         } else {
518                                 list($next_id, $next_ord) = $rows[1];
519                                 if($next_id == $row_id) {
520                                         # after prev (already there)
521                                         return $next_ord;
522                                 }
523                         }
524                 }
525         }
526         if($prev_ord + 1 == $next_ord || $prev_ord == $next_ord) { # the latter should never happen
527                 if($renumbered_already) {
528                         message("Programmer error in $pretty ordering code. Please tell your website administrator.");
529                         return '' . rand(2, DB_ORD_MAX - 2); # reasonably unlikely to be the same as some other ord
530                 }
531                 db_reposition_respace($table, $field, $where);
532                 return db_reposition($table, $row_id, $new_pos, $field, $pretty, $where, $renumbered_already = true);
533         } else {
534                 return $prev_ord + round(($next_ord - $prev_ord) / 2);
535         }
536 }