robot9000.php
1 <?php 2 # Parameters: 3 # com: user supplied comment field (before or after wordfilters? dunno) 4 # md5: md5 of the supplied image. null if no image. 5 # ip: the IP of the user, in integer (packed) form 6 # 7 # Return value: A string. If the string is "OK", the post should go through. 8 # If the string is anything else, abort posting and display that message 9 # to the user. 10 # 11 # Integration info; 12 # This should run before wordfilters (including >>num) and duplicate(md5) detection. 13 # It doesn't know about bans, so those need to be done seperately, but it 14 # doesn't care if that is before or after. 15 # It really should run after valid file checks (jpg/png/gif, >0x0, etc) but that's not critical. 16 # 17 # Synchronization: There is (in theory) a minor race condition because the tables are not locked. 18 # It's not exploitable for any useful purpose, and it's blocked by the floodcheck 19 # 20 # Changelog: 21 # 2008/02/20 04:20: Added changelog, fixed $txt error that killed all posts 22 # 2008/02/20 04:56: Fixed the signal-ratio filter to handle the stupid HTML 23 # 2008/02/20 05:21: Added a check for repeated characters 24 # 2008/02/20 07:05: Added a check for long spams 25 # 2008/02/20 13:02: Rearranged the filters for better results. 26 # 2008/02/20 23:58: Fixed a bug that broke posts with two quotes far apart 27 # 2008/02/21 01:57: Fixed a dumb bug with the number filter.. 28 # 2008/02/21 02:06: Adding content-percentage info to the content filter. 29 # 2008/02/21 02:15: Adjusted long-text filter. 30 # 2008/02/21 02:18: Removed long-text filter. 31 # 2008/02/22 16:43: Added mute-expiring. 32 # 2008/02/22 17:54: Fixed mute-expiring. 33 # 2008/02/22 18:13: Added #nextnow and #muteinfo secret mod capcodes 34 # 2008/02/22 18:21: Fixed #muteinfo for mods. 35 # 2015/10/24 16:36: Cleanup the code and put the robot back. 36 # $email, $sub, $name fields aren't used anymore. 37 # removed $mod parameter. 38 # 2020/11/16 08:09: Update text hashes for every post to prune stale entries 39 40 define('R9K_SIGNAL_RATIO', 0.1); 41 define('R9K_MAX_DURATION', 31536000); // one year 42 define('R9K_DATE_FORMAT', '%m/%d/%y %H:%M:%S'); 43 define('R9K_DEMUTE_PERIOD', 86400); // one day 44 define('R9K_SNR_MIN_LEN', 10); // minimum txt length for signal ratio check 45 46 define('R9K_OK', 'OK'); 47 define('R9K_DB_ERROR', 'Database error.'); 48 define('R9K_EMPTY_COM', 'Textless posts are not allowed.'); 49 define('R9K_ASCII_ONLY', 'Non-ASCII text is not allowed.'); 50 define('R9K_MUTED', "You're muted! You cannot post until %s, %s from now"); 51 define('R9K_MUTE_ERROR', "You have been muted for %s, because %s"); 52 define('R9K_LOW_SNR', 'your comment was too low in content (%0.2f%% content).'); 53 define('R9K_DUP_TXT', 'your comment was not original.'); 54 define('R9K_DUP_IMG', 'your image was not original.'); 55 56 function r9k_process($com, $md5, $ip) { 57 // Blank file 58 if ($md5 == 'd41d8cd98f00b204e9800998ecf8427e') { 59 $md5 = null; 60 } 61 62 if ($com === ''){ 63 return R9K_EMPTY_COM; 64 } 65 66 if (preg_match('/[\\x80-\\xFF]/', $com)) { 67 return R9K_ASCII_ONLY; 68 } 69 70 $table_mutes = ROBOT9000_MUTES; 71 $table_posts = ROBOT9000_POSTS; 72 73 $ip = (int)$ip; 74 75 $mute = false; 76 $demute = false; 77 $timeout_power = 0; 78 79 $query = <<<SQL 80 SELECT timeout_power, 81 UNIX_TIMESTAMP(mute_until) as mute_until, 82 UNIX_TIMESTAMP(next_expire) as next_expire 83 FROM `$table_mutes` WHERE ip = $ip 84 SQL; 85 86 $res = mysql_board_call($query); 87 88 if (!$res) { 89 //return R9K_OK; 90 return R9K_DB_ERROR; 91 } 92 93 $row = mysql_fetch_assoc($res); 94 95 if ($row) { 96 $now = time(); 97 $timeout_power = $row['timeout_power']; 98 99 if ($row['mute_until'] > $now) { 100 $duration = r9k_pretty_duration($row['mute_until'] - $now); 101 $when = strftime(R9K_DATE_FORMAT, $row['mute_until']); 102 return sprintf(R9K_MUTED, $when, $duration); 103 } 104 105 if ($row['next_expire'] < $now){ 106 $demute = true; 107 } 108 } 109 110 $txt = strtolower($com); 111 112 // Strip HTML 113 $stxt=preg_replace('/<.*?>/s','', $txt); 114 115 // Original byte length 116 $olength = strlen($stxt); 117 118 // Strip >>123 quotelinks 119 $stxt = preg_replace('/>>\d+/', '', $stxt); 120 121 // Strip html entities 122 $stxt = preg_replace('/&#?\w+;/', '', $stxt); 123 124 // Strip non-alnum chars 125 $stxt = preg_replace('/[^a-z\d-]+/', '', $stxt); 126 127 // Trim leading and trailing numeric characters 128 $stxt = preg_replace('/^\d*(.*)\d*$/', '\1', $stxt); 129 130 // Compress repeated characters: aaa -> a 131 $stxt = preg_replace('/(.)\\1{2,}/', '\\1', $stxt); 132 133 // Check signal ratio 134 if (strlen($txt) > R9K_SNR_MIN_LEN) { 135 $ratio = strlen($stxt) / $olength; 136 137 if ($ratio < R9K_SIGNAL_RATIO) { 138 $mute = sprintf(R9K_LOW_SNR, $ratio * 100.0); 139 } 140 } 141 142 if ($mute === false) { 143 $txt_hash = md5($stxt); 144 145 // Check if hashes match 146 $query = "SELECT text, image FROM `$table_posts` WHERE text = '%s'"; 147 /* 148 if ($md5) { 149 $query .= " OR image = '%s'"; 150 $res = mysql_board_call($query, $txt_hash, $md5); 151 } 152 else {*/ 153 $res = mysql_board_call($query, $txt_hash); 154 //} 155 156 if (!$res) { 157 //return R9K_OK; 158 return R9K_DB_ERROR; 159 } 160 161 // Post is good. Insert hashes. 162 if (mysql_num_rows($res) < 1) { 163 $query = "INSERT INTO `$table_posts` (text) VALUES('%s')"; 164 mysql_board_call($query, $txt_hash); 165 } 166 // Duplicates found. 167 else { 168 //$row = mysql_fetch_assoc($res); 169 170 //if ($row['text'] === $txt_hash) { 171 $mute = R9K_DUP_TXT; 172 //} 173 //else if ($md5 && $row['image'] === $md5) { 174 // $mute = R9K_DUP_IMG; 175 //} 176 177 // Update the hash with a new timestamp 178 $query = "UPDATE `$table_posts` SET created_on = NOW() WHERE text = '%s' LIMIT 1"; 179 mysql_board_call($query, $txt_hash); 180 } 181 } 182 183 // Muted 184 if ($mute !== false) { 185 ++$timeout_power; 186 187 $mute_duration = pow(2, $timeout_power); 188 189 if ($mute_duration > R9K_MAX_DURATION) { 190 $timeout_power--; 191 $mute_duration = R9K_MAX_DURATION; 192 } 193 194 $next_expire = R9K_DEMUTE_PERIOD; 195 196 $query = <<<SQL 197 INSERT INTO `$table_mutes` (ip, timeout_power, mute_until, next_expire) 198 VALUES ($ip, $timeout_power, DATE_ADD(NOW(), INTERVAL $mute_duration SECOND), 199 DATE_ADD(NOW(), INTERVAL $mute_duration SECOND)) 200 ON DUPLICATE KEY 201 UPDATE timeout_power = $timeout_power, mute_until = VALUES(mute_until), 202 next_expire = VALUES(next_expire) 203 SQL; 204 205 $res = mysql_board_call($query); 206 207 return sprintf(R9K_MUTE_ERROR, r9k_pretty_duration($mute_duration), $mute); 208 } 209 // Not muted 210 else { 211 if ($demute === true) { 212 $next_expire = R9K_DEMUTE_PERIOD; 213 214 $query = <<<SQL 215 UPDATE `$table_mutes` SET 216 timeout_power = IF(timeout_power > 0, timeout_power - 1, 0), 217 next_expire = DATE_ADD(NOW(), INTERVAL $next_expire SECOND) 218 WHERE ip = $ip 219 SQL; 220 221 $res = mysql_board_call($query); 222 } 223 224 return R9K_OK; 225 } 226 } 227 228 function r9k_pretty_duration($secs){ 229 $w = (int)($secs / 604800); 230 $d = (int)($secs / 86400) % 7; 231 $h = (int)($secs / 3600) % 24; 232 $m = ((int)($secs / 60)) % 60; 233 $s = ((int)$secs) % 60; 234 $out = array(); 235 $pairs = array( 236 array($w, 'week'), 237 array($d, 'day'), 238 array($h, 'hour'), 239 array($m, 'minute'), 240 array($s, 'second') 241 ); 242 243 foreach($pairs as $v){ 244 if ($v[0] !== 0) { 245 $out[] = $v[0] . ' ' . $v[1] . ($v[0] === 1 ? '' : 's'); 246 } 247 } 248 249 return implode(' ', $out); 250 }