/ plugins / robot9000.php
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('/&gt;&gt;\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  }