Vanilla commit.
[tinybbs.git] / includes / functions.php
blob9eaea11df182de10ad0ce1d8860f6be7ce5cfbd9
1 <?php
2 $errors = array();
3 $erred = false;
5 /* ==============================================
6 USER FUNCTIONS
7 ===============================================*/
9 function create_id()
11 global $link;
13 $user_id = uniqid('', true);
14 $password = generate_password();
16 $stmt = $link->prepare('INSERT INTO users (uid, password, ip_address, first_seen) VALUES (?, ?, ?, UNIX_TIMESTAMP())');
17 $stmt->bind_param('sss', $user_id, $password, $_SERVER['REMOTE_ADDR']);
18 $stmt->execute();
20 $_SESSION['first_seen'] = $_SERVER['REQUEST_TIME'];
21 $_SESSION['notice'] = 'Welcome to <strong>' . SITE_TITLE . '</strong>. An account has automatically been created and assigned to you. You don\'t have to register or log in to use the board. Please don\'t clear your cookies unless you have <a href="/dashboard">set a memorable name and password</a>.';
23 setcookie('UID', $user_id, $_SERVER['REQUEST_TIME'] + 315569260, '/');
24 setcookie('password', $password, $_SERVER['REQUEST_TIME'] + 315569260, '/');
25 $_SESSION['UID'] = $user_id;
28 function generate_password()
30 $characters = str_split('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789');
31 $password = '';
33 for($i = 0; $i < 32; ++$i)
35 $password .= $characters[array_rand($characters)];
37 return $password;
40 function activate_id()
42 global $link;
44 $stmt = $link->prepare('SELECT password, first_seen FROM users WHERE uid = ?');
45 $stmt->bind_param('s', $_COOKIE['UID']);
46 $stmt->execute();
47 $stmt->bind_result($db_password, $first_seen);
48 $stmt->fetch();
49 $stmt->close();
51 if( ! empty($db_password) && $_COOKIE['password'] === $db_password)
53 // The password is correct!
54 $_SESSION['UID'] = $_COOKIE['UID'];
55 // Our ID wasn't just created.
56 $_SESSION['ID_activated'] = true;
57 // For post.php
58 $_SESSION['first_seen'] = $first_seen;
60 return true;
63 // If the password was wrong, create a new ID.
64 create_id();
67 function force_id()
69 if( ! isset($_SESSION['ID_activated']))
71 add_error('The page that you tried to access requires that you have a valid internal ID. This is supposed to be automatically created the first time you load a page here. Maybe you were linked directly to this page? Upon loading this page, assuming that you have cookies supported and enabled in your Web browser, you have been assigned a new ID. If you keep seeing this page, something is wrong with your setup; stop refusing/modifying/deleting cookies!', true);
75 function update_activity($action_name, $action_id = '')
77 global $link;
79 if( ! isset($_SESSION['UID']))
81 return false;
84 $update_activity = $link->prepare('INSERT INTO activity (time, uid, action_name, action_id) VALUES (UNIX_TIMESTAMP(), ?, ?, ?) ON DUPLICATE KEY UPDATE time = UNIX_TIMESTAMP(), action_name = ?, action_id = ?;');
85 $update_activity->bind_param('ssisi', $_SESSION['UID'], $action_name, $action_id, $action_name, $action_id);
86 $update_activity->execute();
87 $update_activity->close();
90 function id_exists($id)
92 global $link;
94 $uid_exists = $link->prepare('SELECT 1 FROM users WHERE uid = ?');
95 $uid_exists->bind_param('s', $_GET['id']);
96 $uid_exists->execute();
97 $uid_exists->store_result();
99 if($uid_exists->num_rows < 1)
101 $uid_exists->close();
102 return false;
105 $uid_exists->close();
106 return true;
109 function remove_id_ban($id)
111 global $link;
113 $remove_ban = $link->prepare('DELETE FROM uid_bans WHERE uid = ?');
114 $remove_ban->bind_param('s', $id);
115 $remove_ban->execute();
116 $remove_ban->close();
119 function remove_ip_ban($ip)
121 global $link;
123 $remove_ban = $link->prepare('DELETE FROM ip_bans WHERE ip_address = ?');
124 $remove_ban->bind_param('s', $ip);
125 $remove_ban->execute();
126 $remove_ban->close();
129 function fetch_ignore_list() // For ostrich mode.
131 global $link;
133 if($_COOKIE['ostrich_mode'] == 1)
135 $fetch_ignore_list = $link->prepare('SELECT ignored_phrases FROM ignore_lists WHERE uid = ?');
136 $fetch_ignore_list->bind_param('s', $_COOKIE['UID']);
137 $fetch_ignore_list->execute();
138 $fetch_ignore_list->bind_result($ignored_phrases);
139 $fetch_ignore_list->fetch();
140 $fetch_ignore_list->close();
142 // To make this work with Windows input, we need to strip out the return carriage.
143 $ignored_phrases = explode("\n", str_replace("\r", '', $ignored_phrases));
145 return $ignored_phrases;
149 function show_trash($uid, $silence = false) // For profile and trash can.
151 global $link;
153 $output = '<table><thead><tr> <th class="minimal">Headline</th> <th>Body</th> <th class="minimal">Time since deletion ▼</th> </tr></thead> <tbody>';
155 $fetch_trash = $link->prepare('SELECT headline, body, time FROM trash WHERE uid = ? ORDER BY time DESC');
156 $fetch_trash->bind_param('s', $uid);
157 $fetch_trash->execute();
158 $fetch_trash->bind_result($trash_headline, $trash_body, $trash_time);
160 $table = new table();
161 $columns = array
163 'Headline',
164 'Body',
165 'Time since deletion ▼'
167 $table->define_columns($columns, 'Body');
169 while($fetch_trash->fetch())
171 if(empty($trash_headline))
173 $trash_headline = '<span class="unimportant">(Reply.)</span>';
175 else
177 $trash_headline = htmlspecialchars($trash_headline);
180 $values = array
182 $trash_headline,
183 nl2br(htmlspecialchars($trash_body)),
184 '<span class="help" title="' . format_date($trash_time) . '">' . calculate_age($trash_time) . '</span>'
187 $table->row($values);
189 $fetch_trash->close();
191 if($table->num_rows_fetched === 0)
193 return false;
195 return $table->output();
198 /* ==============================================
199 OUTPUT
200 ===============================================*/
202 // Prettify dynamic mark-up
203 function indent($num_tabs = 1)
205 return "\n" . str_repeat("\t", $num_tabs);
208 // Print a <table>. 100 rows takes ~0.0035 seconds on my computer.
209 class table
211 public $num_rows_fetched = 0;
213 private $output = '';
215 private $primary_key;
216 private $columns = array();
217 private $td_classes = array();
219 private $marker_printed = false;
220 private $last_seen = false;
221 private $order_time = false;
223 public function define_columns($all_columns, $primary_column)
225 $this->columns = $all_columns;
227 $this->output .= '<table>' . indent() . '<thead>' . indent(2) . '<tr>';
229 foreach($all_columns as $key => $column)
231 $this->output .= indent(3) . ' <th';
232 if($column != $primary_column)
234 $this->output .= ' class="minimal"';
236 else
238 $this->primary_key = $key;
240 $this->output .= '>' . $column . '</th>';
243 $this->output .= indent(2) . '</tr>' . indent() . '</thead>' . indent() . '<tbody>';
246 public function add_td_class($column_name, $class)
248 $this->td_classes[$column_name] = $class;
251 public function last_seen_marker($last_seen, $order_time)
253 $this->last_seen = $last_seen;
254 $this->order_time = $order_time;
257 public function row($values)
259 // Print <tr>
260 $this->output .= indent(2) . '<tr';
261 if($this->num_rows_fetched & 1)
263 $this->output .= ' class="odd"';
265 // Print the last seen marker.
266 if($this->last_seen && ! $this->marker_printed && $this->order_time <= $this->last_seen)
268 $this->marker_printed = true;
269 if($this->num_rows_fetched != 0)
271 $this->output .= ' id="last_seen_marker"';
274 $this->output .= '>';
276 // Print each <td>
277 foreach($values as $key => $value)
279 $classes = array();
281 $this->output .= indent(3) . '<td';
283 // If this isn't the primary column (as set in define_columns()), its length should be minimal.
284 if($key !== $this->primary_key)
286 $classes[] = 'minimal';
288 // Check if a class has been added via add_td_class.
289 if( isset( $this->td_classes[ $this->columns[$key] ] ) )
291 $classes[] = $this->td_classes[$this->columns[$key]];
293 // Print any classes added by the above two conditionals.
294 if( ! empty($classes))
296 $this->output .= ' class="' . implode(' ', $classes) . '"';
299 $this->output .= '>' . $value . '</td>';
302 $this->output .= indent(2) . '</tr>';
304 $this->num_rows_fetched++;
307 public function output($items = 'items', $silence = false)
309 $this->output .= indent() . '</tbody>' . "\n" . '</table>' . "\n";
311 if($this->num_rows_fetched > 0)
313 return $this->output;
315 else if( ! $silence)
317 return '<p>(No ' . $items . ' to show.)</p>';
320 // Silence.
321 return '';
326 function add_error($message, $critical = false)
328 global $errors, $erred;
330 $errors[] = $message;
331 $erred = true;
333 if($critical)
335 print_errors(true);
339 function print_errors($critical = false)
341 global $errors;
343 $number_errors = count($errors);
345 if($number_errors > 0)
347 echo '<h3 id="error">';
348 if($number_errors > 1)
350 echo $number_errors . ' errors';
352 else
354 echo 'Error';
356 echo '</h3><ul class="body standalone">';
358 foreach($errors as $error_message)
360 echo '<li>' . $error_message . '</li>';
363 echo '</ul>';
365 if($critical)
367 if( ! isset($page_title))
369 $page_title = 'Fatal error';
371 require('footer.php');
372 exit;
377 function page_navigation($section_name, $current_page, $num_items_fetched)
379 $output = '';
380 if($current_page != 1)
382 $output .= indent() . '<li><a href="/' . $section_name . '">Latest</a></li>';
384 if($current_page != 1 && $current_page != 2)
386 $newer = $current_page - 1;
387 $output .= indent() . '<li><a href="/' . $section_name . '/' . $newer . '">Newer</a></li>';
389 if($num_items_fetched == ITEMS_PER_PAGE)
391 $older = $current_page + 1;
392 $output .= indent() . '<li><a href="/' . $section_name . '/' . $older . '">Older</a></li>';
395 if( ! empty($output))
397 echo "\n" . '<ul class="menu">' . $output . "\n" . '</ul>' . "\n";
401 function edited_message($original_time, $edit_time, $edit_mod)
403 if($edit_time)
405 echo '<p class="unimportant">(Edited ' . calculate_age($original_time, $edit_time) . ' later';
406 if($edit_mod)
408 echo ' by a moderator';
410 echo '.)</p>';
414 function dummy_form()
416 echo "\n" . '<form id="dummy_form" class="noscreen" action="" method="post">' . indent() . '<div> <input type="hidden" name="some_var" value="" /> </div>' . "\n" . '</form>' . "\n";
419 // To redirect to index, use redirect($notice, ''). To redirect back to referrer,
420 // use redirect($notice). To redirect to /topic/1, use redirect($notice, 'topic/1')
421 function redirect($notice = '', $location = NULL)
423 if( ! empty($notice))
425 $_SESSION['notice'] = $notice;
428 if( ! is_null($location) || empty($_SERVER['HTTP_REFERER']))
430 $location = DOMAIN . $location;
432 else
434 $location = $_SERVER['HTTP_REFERER'];
437 header('Location: ' . $location);
438 exit;
441 // Unused
442 function regenerate_config()
444 global $link;
446 $output = '<?php' . "\n\n" . '#### DO NOT EDIT THIS FILE. ####' . "\n\n";
448 $result = $link->query('SELECT `option`, `value` FROM configuration');
449 while( $row = $result->fetch_assoc() )
451 if( ! ctype_digit($row['value']))
453 $row['value'] = "'" . $row['value'] . "'";
455 $output .= "define('" . strtoupper($row['option']) . "', " . $row['value'] . ");\n";
457 $result->close();
459 $output .= "\n" . '?>';
461 file_put_contents('cache/config.php', $output, LOCK_EX);
464 /* ==============================================
465 CHECKING
466 ===============================================*/
468 function check_length($text, $name, $min_length, $max_length)
470 $text_length = strlen($text);
472 if($min_length > 0 && empty($text))
474 add_error('The ' . $name . ' cannot be blank.');
476 else if($text_length > $max_length)
478 add_error('The ' . $name . ' was ' . number_format($text_length - $max_length) . ' characters over the limit (' . number_format($max_length) . ').');
480 else if($text_length < $min_length)
482 add_error('The ' . $name . ' was too short.');
486 function check_tor($ip_address) //query TorDNSEL
488 // Reverse the octets of our IP address.
489 $ip_address = implode('.', array_reverse( explode('.', $ip_address) ));
491 // Returns true if Tor, false if not. 80.208.77.188.166 is of no significance.
492 return checkdnsrr($ip_address . '.80.208.77.188.166.ip-port.exitlist.torproject.org', 'A');
495 // Prevent cross-site redirection forgeries.
496 function csrf_token()
498 if( ! isset($_SESSION['token']))
500 $_SESSION['token'] = md5(SALT . mt_rand());
502 echo '<div class="noscreen"> <input type="hidden" name="CSRF_token" value="' . $_SESSION['token'] . '" /> </div>' . "\n";
505 function check_token()
507 if($_POST['CSRF_token'] !== $_SESSION['token'])
509 add_error('Session error. Try again.');
510 return false;
512 return true;
515 /* ==============================================
516 FORMATTING
517 ===============================================*/
519 function parse($text)
521 $text = htmlspecialchars($text);
522 $text = str_replace("\r", '', $text);
524 $markup = array
526 // Strong emphasis.
527 "/'''(.+?)'''/",
528 // Emphasis.
529 "/''(.+?)''/",
530 // Linkify URLs.
531 '@\b(?<!\[)(https?|ftp)://(www\.)?([A-Z0-9.-]+)(/)?([A-Z0-9/&#+%~=_|?.,!:;-]*[A-Z0-9/&#+%=~_|])?@i',
532 // Linkify text in the form of [http://example.org text]
533 '@\[(https?|ftp)://([A-Z0-9/&#+%~=_|?.,!:;-]+) (.+?)\]@i',
534 // Quotes.
535 '/^&gt;(.+)/m',
536 // Headers.
537 '/^==(.+?)==\s+/m'
540 $html = array
542 '<strong>$1</strong>',
543 '<em>$1</em>',
544 '<a href="$0">$1://$2<strong>$3</strong>$4$5</a>',
545 '<a href="$1://$2">$3</a>',
546 '<span class="quote"><strong>&gt;</strong> $1</span>',
547 '<h4 class="user">$1</h4>'
550 $text = preg_replace($markup, $html, $text);
551 return nl2br($text);
554 function snippet($text, $snippet_length = 80)
556 $patterns = array
558 "/'''?(.*?)'''?/", // strip formatting
559 '/^(@|>)(.*)/m' //replace quotes and citations
562 $replacements = array
564 '$1',
565 ' ~ '
568 $text = preg_replace($patterns, $replacements, $text);
569 $text = str_replace( array("\r", "\n"), ' ', $text ); // strip line breaks
570 $text = htmlspecialchars($text);
572 if(ctype_digit($_COOKIE['snippet_length']))
574 $snippet_length = $_COOKIE['snippet_length'];
576 if(strlen($text) > $snippet_length)
578 $text = substr($text, 0, $snippet_length) . '&hellip;';
580 return $text;
583 function super_trim($text)
585 // Strip return carriage and non-printing characters.
586 $nonprinting_characters = array
588 "\r",
589 '­', //soft hyphen ( U+00AD)
590 '', // zero width no-break space ( U+FEFF)
591 '​', // zero width space (U+200B)
592 '‍', // zero width joiner (U+200D)
593 '‌' // zero width non-joiner (U+200C)
595 $text = str_replace($nonprinting_characters, '', $text);
596 //Trim and kill excessive newlines (maximum of 3)
597 return preg_replace( '/(\r?\n[ \t]*){3,}/', "\n\n\n", trim($text) );
600 function sanitize_for_textarea($text)
602 $text = str_ireplace('/textarea', '&#47;textarea', $text);
603 $text = str_replace('<!--', '&lt;!--', $text);
604 return $text;
607 function calculate_age($timestamp, $comparison = '')
609 $units = array(
610 'second' => 60,
611 'minute' => 60,
612 'hour' => 24,
613 'day' => 7,
614 'week' => 4.25, // FUCK YOU GREGORIAN CALENDAR
615 'month' => 12
618 if(empty($comparison))
620 $comparison = $_SERVER['REQUEST_TIME'];
622 $age_current_unit = abs($comparison - $timestamp);
623 foreach($units as $unit => $max_current_unit)
625 $age_next_unit = $age_current_unit / $max_current_unit;
626 if($age_next_unit < 1) // are there enough of the current unit to make one of the next unit?
628 $age_current_unit = floor($age_current_unit);
629 $formatted_age = $age_current_unit . ' ' . $unit;
630 return $formatted_age . ($age_current_unit == 1 ? '' : 's');
632 $age_current_unit = $age_next_unit;
635 $age_current_unit = round($age_current_unit, 1);
636 $formatted_age = $age_current_unit . ' year';
637 return $formatted_age . (floor($age_current_unit) == 1 ? '' : 's');
641 function format_date($timestamp)
643 return date('Y-m-d H:i:s \U\T\C — l \t\h\e jS \o\f F Y, g:i A', $timestamp);
646 function format_number($number)
648 if($number == 0)
650 return '-';
652 return number_format($number);
655 function number_to_letter($number)
657 $alphabet = range('A', 'Y');
658 if($number < 24)
660 return $alphabet[$number];
662 $number = $number - 23;
663 return 'Z-' . $number;
666 function replies($topic_id, $topic_replies)
668 global $visited_topics;
670 $output = '';
671 if( ! isset($visited_topics[$topic_id]))
673 $output = '<strong>';
675 $output .= format_number($topic_replies);
677 if( ! isset($visited_topics[$topic_id]))
679 $output .= '</strong>';
681 else if($visited_topics[$topic_id] < $topic_replies)
683 $output .= ' (<a href="/topic/' . $topic_id . '#new">';
684 $new_replies = $topic_replies - $visited_topics[$topic_id];
685 if($new_replies != $topic_replies)
687 $output .= '<strong>' . $new_replies . '</strong> ';
689 else
691 $output .= 'all-';
693 $output .= 'new</a>)';
696 return $output;
699 function thumbnail($source, $dest_name, $type)
701 switch($type)
703 case 'jpg':
704 $image = imagecreatefromjpeg($source);
705 break;
707 case 'gif':
708 $image = imagecreatefromgif($source);
709 break;
711 case 'png':
712 $image = imagecreatefrompng($source);
715 $width = imagesx($image);
716 $height = imagesy($image);
718 if($width > MAX_IMAGE_DIMENSIONS || $height > MAX_IMAGE_DIMENSIONS)
720 $percent = MAX_IMAGE_DIMENSIONS / ( ($width > $height) ? $width : $height );
722 $new_width = $width * $percent;
723 $new_height = $height * $percent;
725 $thumbnail = imagecreatetruecolor($new_width, $new_height) ;
726 imagecopyresampled($thumbnail, $image, 0, 0, 0, 0, $new_width, $new_height, $width, $height);
728 else
730 $thumbnail = $image;
733 switch($type)
735 case 'jpg':
736 imagejpeg($thumbnail, 'thumbs/' . $dest_name, 70);
737 break;
739 case 'gif':
740 imagegif($thumbnail, 'thumbs/' . $dest_name);
741 break;
743 case 'png':
744 imagepng($thumbnail, 'thumbs/' . $dest_name);
747 imagedestroy($thumbnail);
748 imagedestroy($image);