init 4chan

This commit is contained in:
skidoodle 2025-04-17 09:20:34 +02:00
commit c850337f1e
Signed by: albert
SSH key fingerprint: SHA256:Cu/S7e7NSLXxcxcBsxd0qtCy6TJiN24ptIdit5aQXyI
390 changed files with 195936 additions and 0 deletions

201
lib/GoogleAuthenticator.php Normal file
View file

@ -0,0 +1,201 @@
<?php
/**
* PHP Class for handling Google Authenticator 2-factor authentication
*
* @author Michael Kliewe
* @copyright 2012 Michael Kliewe
* @license http://www.opensource.org/licenses/bsd-license.php BSD License
* @link http://www.phpgangsta.de/
*/
class PHPGangsta_GoogleAuthenticator
{
protected $_codeLength = 6;
/**
* Create new secret.
* 16 characters, randomly chosen from the allowed base32 characters.
*
* @param int $secretLength
* @return string
*/
public function createSecret($secretLength = 16)
{
$validChars = $this->_getBase32LookupTable();
unset($validChars[32]);
$secret = '';
for ($i = 0; $i < $secretLength; $i++) {
$secret .= $validChars[array_rand($validChars)];
}
return $secret;
}
/**
* Calculate the code, with given secret and point in time
*
* @param string $secret
* @param int|null $timeSlice
* @return string
*/
public function getCode($secret, $timeSlice = null)
{
if ($timeSlice === null) {
$timeSlice = floor(time() / 30);
}
$secretkey = $this->_base32Decode($secret);
// Pack time into binary string
$time = chr(0).chr(0).chr(0).chr(0).pack('N*', $timeSlice);
// Hash it with users secret key
$hm = hash_hmac('SHA1', $time, $secretkey, true);
// Use last nipple of result as index/offset
$offset = ord(substr($hm, -1)) & 0x0F;
// grab 4 bytes of the result
$hashpart = substr($hm, $offset, 4);
// Unpak binary value
$value = unpack('N', $hashpart);
$value = $value[1];
// Only 32 bits
$value = $value & 0x7FFFFFFF;
$modulo = pow(10, $this->_codeLength);
return str_pad($value % $modulo, $this->_codeLength, '0', STR_PAD_LEFT);
}
/**
* Get QR-Code URL for image, from google charts
*
* @param string $name
* @param string $secret
* @return string
*/
public function getQRCodeGoogleUrl($name, $secret) {
$urlencoded = urlencode('otpauth://totp/'.$name.'?secret='.$secret.'');
return 'https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl='.$urlencoded.'';
}
/**
* Check if the code is correct. This will accept codes starting from $discrepancy*30sec ago to $discrepancy*30sec from now
*
* @param string $secret
* @param string $code
* @param int $discrepancy This is the allowed time drift in 30 second units (8 means 4 minutes before or after)
* @return bool
*/
public function verifyCode($secret, $code, $discrepancy = 1)
{
$currentTimeSlice = floor(time() / 30);
for ($i = -$discrepancy; $i <= $discrepancy; $i++) {
$calculatedCode = $this->getCode($secret, $currentTimeSlice + $i);
if ($calculatedCode == $code ) {
return true;
}
}
return false;
}
/**
* Set the code length, should be >=6
*
* @param int $length
* @return PHPGangsta_GoogleAuthenticator
*/
public function setCodeLength($length)
{
$this->_codeLength = $length;
return $this;
}
/**
* Helper class to decode base32
*
* @param $secret
* @return bool|string
*/
protected function _base32Decode($secret)
{
if (empty($secret)) return '';
$base32chars = $this->_getBase32LookupTable();
$base32charsFlipped = array_flip($base32chars);
$paddingCharCount = substr_count($secret, $base32chars[32]);
$allowedValues = array(6, 4, 3, 1, 0);
if (!in_array($paddingCharCount, $allowedValues)) return false;
for ($i = 0; $i < 4; $i++){
if ($paddingCharCount == $allowedValues[$i] &&
substr($secret, -($allowedValues[$i])) != str_repeat($base32chars[32], $allowedValues[$i])) return false;
}
$secret = str_replace('=','', $secret);
$secret = str_split($secret);
$binaryString = "";
for ($i = 0; $i < count($secret); $i = $i+8) {
$x = "";
if (!in_array($secret[$i], $base32chars)) return false;
for ($j = 0; $j < 8; $j++) {
$x .= str_pad(base_convert(@$base32charsFlipped[@$secret[$i + $j]], 10, 2), 5, '0', STR_PAD_LEFT);
}
$eightBits = str_split($x, 8);
for ($z = 0; $z < count($eightBits); $z++) {
$binaryString .= ( ($y = chr(base_convert($eightBits[$z], 2, 10))) || ord($y) == 48 ) ? $y:"";
}
}
return $binaryString;
}
/**
* Helper class to encode base32
*
* @param string $secret
* @param bool $padding
* @return string
*/
protected function _base32Encode($secret, $padding = true)
{
if (empty($secret)) return '';
$base32chars = $this->_getBase32LookupTable();
$secret = str_split($secret);
$binaryString = "";
for ($i = 0; $i < count($secret); $i++) {
$binaryString .= str_pad(base_convert(ord($secret[$i]), 10, 2), 8, '0', STR_PAD_LEFT);
}
$fiveBitBinaryArray = str_split($binaryString, 5);
$base32 = "";
$i = 0;
while ($i < count($fiveBitBinaryArray)) {
$base32 .= $base32chars[base_convert(str_pad($fiveBitBinaryArray[$i], 5, '0'), 2, 10)];
$i++;
}
if ($padding && ($x = strlen($binaryString) % 40) != 0) {
if ($x == 8) $base32 .= str_repeat($base32chars[32], 6);
elseif ($x == 16) $base32 .= str_repeat($base32chars[32], 4);
elseif ($x == 24) $base32 .= str_repeat($base32chars[32], 3);
elseif ($x == 32) $base32 .= $base32chars[32];
}
return $base32;
}
/**
* Get array with all 32 characters for decoding from/encoding to base32
*
* @return array
*/
protected function _getBase32LookupTable()
{
return array(
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 7
'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 15
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 23
'Y', 'Z', '2', '3', '4', '5', '6', '7', // 31
'=' // padding char
);
}
}

612
lib/admin-test.php Normal file
View file

@ -0,0 +1,612 @@
<?
require_once 'db.php';
require_once 'rpc.php';
if( !defined( "SQLLOGMOD" ) ) {
define( 'SQLLOGBAN', 'banned_users' ); // FIXME move to config_db.php?
define( 'SQLLOGMOD', 'mod_users' );
}
// Parses the "email" field and returns a hash
function decode_user_meta($data) {
if (!$data) {
return [];
}
$data = explode(':', $data);
$fields = [];
$fields['browser_id'] = $data[0];
$fields['is_mobile'] = $data[0] && $data[0][0] === '1';
$fields['req_sig'] = $data[1];
$fields['known_status'] = (int)$data[2];
$fields['verified_level'] = (int)$data[3];
return $fields;
}
function user_known_status_to_str($status) {
if ($status === 0) {
return 'Trusted';
}
else if ($status === 1) {
return 'New';
}
else if ($status === 2) {
return 'Recent';
}
else if ($status === 3) {
return 'Regular';
}
return 'N/A';
}
// Encodes email field data for storage in the database
// Entries are separates by ":"
// known status: 1 = new user, 2 unknown user
function encode_user_meta($browser_id, $req_sig, $userpwd) {
// Default status is Trusted - above 7 days and 20 posts
$known_status = 0;
$verified_level = 0;
if ($userpwd) {
$post_count = $userpwd->postCount();
// New - below 1 hour and 1 post
if (!$userpwd->isUserKnown(60, 1) || $post_count < 1) {
$known_status = 1;
}
// Recent - above 1h and 1 post / below 3h and 6 posts
else if (!$userpwd->isUserKnown(4320) || $post_count < 6) {
$known_status = 2;
}
// Regular - above 3 days and 5 posts / below 7 days and 21 posts
else if (!$userpwd->isUserKnown(10080) || $post_count < 21) {
$known_status = 3;
}
if ($userpwd->verifiedLevel()) {
$verified_level = 1;
}
}
$data = [ $browser_id, $req_sig, $known_status, $verified_level ];
$data = implode(':', $data);
return $data;
}
function _grep_notjanitor( $a )
{
return ( $a != 'janitor' );
}
function get_random_string( $len = 16 )
{
$str = mt_rand( 1000000, 9999999 );
$str = hash( 'sha256', $str );
return substr( $str, -$len );
}
function derefer_url($url) {
return 'https://www.4chan.org/derefer?url=' . rawurlencode($url);
}
function access_check()
{
global $access;
$user = $_COOKIE['4chan_auser'];
$pass = $_COOKIE['apass'];
if( !$user || !$pass ) return;
$query = mysql_global_call( "SELECT allow,password_expired,level,flags,username,password,signed_agreement FROM mod_users WHERE username='%s' LIMIT 1", $user );
if (!mysql_num_rows($query)) {
return '';
}
list($allow, $expired, $level, $flags, $username, $password, $signed_agreement) = mysql_fetch_row($query);
$admin_salt = file_get_contents('/www/keys/2014_admin.salt');
if (!$admin_salt) {
die('Internal Server Error (s0)');
}
$hashed_admin_password = hash('sha256', $username . $password . $admin_salt);
if ($hashed_admin_password !== $pass) {
return '';
}
if( $expired ) {
die( 'Your password has expired; check IRC for instructions on changing it.' );
}
if ($signed_agreement == 0 && basename($_SERVER['SELF_PATH']) !== 'agreement.php') {
die('You must agree to the 4chan Volunteer Moderator Agreement in order to access moderation tools. Please check your e-mail for more information.');
}
if( $allow ) {
if( $level == 'janitor' ) {
$a = $access['janitor'];
$a['board'] = array_filter( explode( ',', $allow ), '_grep_notjanitor' );
if( in_array( "all", $a['board'] ) )
unset( $a['board'] );
return $a;
} elseif( $level == 'manager' ) {
return $access['manager'];
} elseif( $level == 'admin' ) {
return $access['admin'];
} elseif( $level == 'mod' ) {
if (is_array($access['mod'])) {
$flags = explode(',', $flags);
$access['mod']['is_developer'] = in_array('developer', $flags);
}
return $access['mod'];
} else {
die( 'oh no you are not a right user!' );
}
} else {
return '';
}
}
//based on team pages' valid(), need to merge with above!
//this sets different globals and respects deny
function access_check2( $func = 0 )
{
global $is_admin, $user, $pass;
$is_admin = 0;
$user = "";
$pass = "";
if( isset( $_COOKIE['4chan_auser'] ) && isset( $_COOKIE['4chan_apass'] ) ) {
$user = $_COOKIE['4chan_auser'];
$pass = $_COOKIE['4chan_apass'];
}
if( isset( $user ) && $user && $pass ) {
$result = mysql_global_call( "SELECT allow,deny,password_expired FROM " . SQLLOGMOD . " WHERE username='%s' and password='%s' limit 1", $user, $pass );
if( mysql_num_rows( $result ) != 0 ) {
list( $allowed, $denied, $expired ) = mysql_fetch_array( $result );
if( $expired ) {
die( 'Your password has expired; check IRC for instructions on changing it.' );
}
if( $func == "unban" ) {
$deny_arr = explode( ",", $denied );
if( in_array( "unban", $deny_arr ) ) die( "You do not have access to unban users." );
}
$allow_arr = explode( ",", $allowed );
if( in_array( "admin", $allow_arr ) || in_array( "manager", $allow_arr ) ) $is_admin = 1;
} else {
die( "Please login via admin panel first. (admin user not found)" );
}
if( $user && !$pass ) {
die( "Please login via admin panel first. (no pass specified)" );
} elseif( !$user && $pass ) {
die( "Please login via admin panel first. (no user specified)" );
}
} else {
die( "Please login via admin panel first." );
}
}
function form_post_values( $names )
{
$a = array();
foreach( $names as $n ) {
$v = $_REQUEST[$n];
if( $v ) $a[$n] = $v;
}
return $a;
}
//rebuild the bans for board $boards
function rebuild_bans( $boards )
{
// run in background
$cmd = "nohup /usr/local/bin/suid_run_global bin/rebuildbans $boards >/dev/null 2>&1 &";
// print "<br>Rebuilding bans in $boards<br>";
exec( $cmd );
}
//add list of bans to the file for $boards
function append_bans( $boards, $bans )
{
$str = is_array( $bans ) ? implode( ",", $bans ) : $bans;
$cmd = "nohup /usr/local/bin/suid_run_global bin/appendban $boards $str >/dev/null 2>&1 &";
// print "<br>Added new bans to $boards<br>";
exec( $cmd );
}
// IPs that can't be banned because they're known good proxy servers
// e.g. cloudflare, singapore
function whitelisted_ip( $ip = 0 )
{
list( $ips ) = post_filter_get( "ipwhitelist" );
if( $ip === 0 ) $ip = $_SERVER["REMOTE_ADDR"];
return find_ipxff_in( ip2long( $ip ), 0, $ips );
}
// add a global ban (indefinite for now)
// returns true if it was new (not already inserted)
function add_ban( $ip, $reason, $days = -1, $zonly = false, $origname = 'Anonymous', &$error, $no = 0, $pass = '', $no_reverse = false )
{
global $user;
if( ip2long( $ip ) === false ) {
$error = "invalid IP address";
return false;
}
if( whitelisted_ip( $ip ) ) {
$error = "IP is whitelisted";
return false;
}
// FIXME add unique index to banned_users instead
$prev = mysql_global_call( "SELECT COUNT(*)>0 FROM " . SQLLOGBAN . " WHERE active=1 AND global=1 AND host='%s'", $ip );
list( $nprev ) = mysql_fetch_array( $prev );
if( $nprev > 0 ) return false;
if ($no_reverse) {
$rev = $ip;
}
else {
$rev = gethostbyaddr( $ip );
}
$tripcode = '';
$name_bits = explode('</span> <span class="postertrip">!', $origname);
if ($name_bits[1]) {
$tripcode = preg_replace('/<[^>]+>/', '', $name_bits[1]);
}
$origname = str_replace( '</span> <span class="postertrip">!', ' #', $origname );
$origname = preg_replace( '/<[^>]+>/', '', $origname ); // remove all remaining html crap
$board = defined( 'BOARD_DIR' ) ? BOARD_DIR : "";
if( $days == -1 )
$length = "00000000000000";
else
$length = date( "Ymd", time() + $days * ( 24 * 60 * 60 ) ) . '000000';
echo "Banned $ip (" . htmlspecialchars( $rev ) . ")<br>\n";
if (!isset($user)) {
$banned_by = $_COOKIE['4chan_auser'];
}
else {
$banned_by = $user;
}
mysql_global_do( "INSERT INTO " . SQLLOGBAN . " (global,board,host,reverse,reason,admin,zonly,length,name,tripcode,4pass_id,post_num,admin_ip) values (%d,'%s','%s','%s','%s','%s',%d,'%s','%s','%s','%s',%d,'%s')", !$zonly, $board, $ip, $rev, "$reason", $banned_by, $zonly, $length, $origname, $tripcode, $pass, $no, $_SERVER['REMOTE_ADDR'] );
return true;
}
function is_real_board( $board )
{
// no board
if( $board === "-" || $board === '' ) return true;
$res = mysql_global_call( "select count(*) from boardlist where dir='%s'", $board );
$row = mysql_fetch_row( $res );
return ( $row[0] > 0 );
}
function remote_delete_things( $board, $nos, $tool = null )
{
// see reports/actions.php, action_delete()
$url = "https://sys.int/$board/";
if( $board != 'f' ) // XXX dumb. :( XXX
$url .= 'imgboard.php';
else
$url .= 'up.php';
// Build the appropriate POST and cookie...
$post = array();
$post['mode'] = 'usrdel';
$post['onlyimgdel'] = ''; // never delete only img
if ($tool) {
$post['tool'] = $tool;
}
// note multiple post number deletions
foreach( $nos as $no )
$post[$no] = 'delete';
$post['remote_addr'] = $_SERVER['REMOTE_ADDR'];
rpc_start_request($url, $post, $_COOKIE, true);
return "";
}
function clear_cookies()
{
if( strstr( $_SERVER["HTTP_HOST"], ".4chan.org" ) ) {
setcookie( "4chan_auser", "", time() - 3600, "/", ".4chan.org", true );
setcookie( "4chan_apass", "", time() - 3600, "/", ".4chan.org", true );
setcookie( "4chan_aflags", "", time() - 3600, "/", ".4chan.org", true );
} elseif( strstr( $_SERVER["HTTP_HOST"], ".4channel.org" ) ) {
setcookie( "4chan_auser", "", time() - 24 * 3600, "/", ".4channel.org", true );
setcookie( "4chan_apass", "", time() - 24 * 3600, "/", ".4channel.org", true );
} else {
setcookie( "4chan_auser", "", time() - 24 * 3600, "/", true );
setcookie( "4chan_apass", "", time() - 24 * 3600, "/", true );
setcookie( "4chan_aflags", "", time() - 24 * 3600, "/", true );
}
setcookie( 'extra_path', '', 1, '/', '.4chan.org' );
}
// record and autoban failed logins. assumes admin or imgboard.php as caller
function admin_login_fail()
{
$ip = ip2long( $_SERVER["REMOTE_ADDR"] );
clear_cookies();
mysql_global_call( "insert into user_actions (ip,board,action,time) values (%d,'%s','fail_login',now())", $ip, BOARD_DIR );
$query = mysql_global_call( "select count(*)>%d from user_actions where ip=%d and action='fail_login' and time >= subdate(now(), interval 1 hour)", LOGIN_FAIL_HOURLY, $ip );
if( mysql_result( $query, 0, 0 ) ) {
auto_ban_poster( "", -1, 1, "failed to login to /" . BOARD_DIR . "/admin.php " . LOGIN_FAIL_HOURLY . " times", "Repeated admin login failures." );
}
error( S_WRONGPASS );
}
// delete all posts everywhere by the poster's IP
// for autobans
function del_all_posts( $ip = false )
{
$q = mysql_global_call( "select sql_cache dir from boardlist" );
$boards = mysql_column_array( $q );
$host = $ip ? $ip : $_SERVER['REMOTE_ADDR'];
foreach( $boards as $b ) {
$q = mysql_board_call( "select no from `%s` where host='%s'", $b, $host );
$posts = mysql_column_array( $q );
if( !count( $posts ) ) continue;
remote_delete_things( $b, $posts );
}
}
function auto_ban_poster($nametrip, $banlength, $global, $reason, $pubreason = '', $is_filter = false, $pwd = null, $pass_id = null) {
if (!$nametrip) {
$nametrip = S_ANONAME;
}
if (strpos($nametrip, '</span> <span class="postertrip">!') !== false) {
$nameparts = explode('</span> <span class="postertrip">!', $nametrip);
$nametrip = "{$nameparts[0]} #{$nameparts[1]}";
}
$host = $_SERVER['REMOTE_ADDR'];
$reverse = mysql_real_escape_string(gethostbyaddr($host));
$nametrip = mysql_real_escape_string($nametrip);
$global = ($global ? 1 : 0);
$board = defined( 'BOARD_DIR' ) ? BOARD_DIR : '';
$reason = mysql_real_escape_string($reason);
$pubreason = mysql_real_escape_string($pubreason);
if ($pubreason) {
$pubreason .= "<>";
}
if ($pass_id) {
$pass_id = mysql_real_escape_string($pass_id);
}
else {
$pass_id = '';
}
if ($pwd) {
$pwd = mysql_real_escape_string($pwd);
}
else {
$pwd = '';
}
// check for whitelisted ban
if( whitelisted_ip() ) return;
//if they're already banned on this board, don't insert again
//since this is just a spam post
//i don't think it matters if the active ban is global=0 and this one is global=1
/*
if ($banlength == -1) {
$existingq = mysql_global_do("select count(*)>0 from " . SQLLOGBAN . " where host='$host' and active=1 AND global = 1 AND length = 0");
}
else {
$existingq = mysql_global_do("select count(*)>0 from " . SQLLOGBAN . " where host='$host' and active=1 and (board='$board' or global=1)");
}
$existingban = mysql_result( $existingq, 0, 0 );
if( $existingban > 0 ) {
delete_uploaded_files();
die();
}
*/
/*
if( $banlength == 0 ) { // warning
// check for recent warnings to punish spammers
$autowarnq = mysql_global_call( "SELECT COUNT(*) FROM " . SQLLOGBAN . " WHERE host='$host' AND admin='Auto-ban' AND now > DATE_SUB(NOW(),INTERVAL 3 DAY) AND reason like '%$reason'" );
$autowarncount = mysql_result( $autowarnq, 0, 0 );
if( $autowarncount > 3 ) {
$banlength = 14;
}
}
*/
if ($banlength == -1) { // permanent
$length = '0000' . '00' . '00'; // YYYY/MM/DD
}
else {
$banlength = (int)$banlength;
if ($banlength < 0) {
$banlength = 0;
}
$length = date('Ymd', time() + $banlength * (24 * 60 * 60));
}
$length .= "00" . "00" . "00"; // H:M:S
$sql = "INSERT INTO " . SQLLOGBAN . " (board,global,name,host,reason,length,admin,reverse,post_time,4pass_id,password) VALUES('$board','$global','$nametrip','$host','{$pubreason}Auto-ban: $reason','$length','Auto-ban','$reverse',NOW(),'$pass_id','$pwd')";
$res = mysql_global_call($sql);
if (!$res) {
die(S_SQLFAIL);
}
//append_bans( $global ? "global" : $board, array($host) );
//$child = stripos($pubreason, 'child') !== false || stripos($reason, 'child') !== false;
//if ($global && $child && !$is_filter) {
// del_all_posts();
//}
}
function cloudflare_purge_url_old($file,$secondary = false)
{
global $purges;
if (!defined('CLOUDFLARE_API_TOKEN')) {
internal_error_log('cf', "tried purging but token isn't set");
return null;
}
$post = array(
"tkn" => CLOUDFLARE_API_TOKEN,
"email" => CLOUDFLARE_EMAIL,
"a" => "zone_file_purge",
"z" => $secondary ? CLOUDFLARE_ZONE_2 : CLOUDFLARE_ZONE,
"url" => $file
);
//quick_log_to("/www/perhost/cf-purge.log", print_r($post, true));
$ch = rpc_start_request("https://www.cloudflare.com/api_json.html", $post, array(), false);
return $ch;
}
function write_to_event_log($event, $ip, $args = []) {
$sql = <<<SQL
INSERT INTO event_log(`type`, ip, board, thread_id, post_id, arg_num,
arg_str, pwd, req_sig, ua_sig, meta)
VALUES('%s', '%s', '%s', '%d', '%d', '%d',
'%s', '%s', '%s', '%s', '%s')
SQL;
return mysql_global_call($sql, $event, $ip,
$args['board'], $args['thread_id'], $args['post_id'], $args['arg_num'],
$args['arg_str'], $args['pwd'], $args['req_sig'], $args['ua_sig'], $args['meta']
);
}
function log_staff_event($event, $username, $ip, $pwd, $board, $post) {
$json_post = [];
if ($post['sub'] !== '') {
$json_post['sub'] = $post['sub'];
}
if ($post['name'] !== '') {
$json_post['name'] = $post['name'];
}
if ($post['com'] !== '') {
$json_post['com'] = $post['com'];
}
if ($post['fsize'] > 0) {
$json_post['file'] = $post["filename"].$post["ext"];
$json_post['md5'] = $post["md5"];
}
$json_post = json_encode($json_post, JSON_PARTIAL_OUTPUT_ON_ERROR);
return write_to_event_log($event, $ip, [
'board' => $board,
'thread_id' => $post['resto'] ? $post['resto'] : $post['no'],
'post_id' => $post['no'],
'arg_str' => $username,
'pwd' => $pwd,
'meta' => $json_post
]);
}
function cloudflare_purge_url($files, $zone2 = false) {
// 4cdn = ca66ca34d08802412ae32ee20b7e98af (zone2)
// 4chan = 363d1b9b6be563ffd5143c8cfcc29d52
$url = 'https://api.cloudflare.com/client/v4/zones/'
. ($zone2 ? 'ca66ca34d08802412ae32ee20b7e98af' : '363d1b9b6be563ffd5143c8cfcc29d52')
. '/purge_cache';
$opts = array(
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => array(
'Authorization: Bearer iTf0pQMTvn0zSHAN9vg5S1m_tiwmPKYDjepq8za9',
'Content-Type: application/json'
)
);
// Multiple files
if (is_array($files)) {
// Batching
if (count($files) > 30) {
$files = array_chunk($files, 30);
foreach ($files as $batch) {
$opts[CURLOPT_POSTFIELDS] = '{"files":' . json_encode($batch, JSON_UNESCAPED_SLASHES) . '}';
//print_r($opts[CURLOPT_POSTFIELDS]);
rpc_start_request_with_options($url, $opts);
}
}
else {
$opts[CURLOPT_POSTFIELDS] = '{"files":' . json_encode($files, JSON_UNESCAPED_SLASHES) . '}';
//print_r($opts[CURLOPT_POSTFIELDS]);
rpc_start_request_with_options($url, $opts);
}
}
// Single file
else {
$opts[CURLOPT_POSTFIELDS] = '{"files":["' . $files . '"]}';
//print_r($opts[CURLOPT_POSTFIELDS]);
rpc_start_request_with_options($url, $opts);
}
}
function cloudflare_purge_by_basename($board, $basename) {
preg_match("/([0-9]+)[sm]?\\.([a-z]{3,4})/", $basename, $m);
$tim = $m[1];
$ext = $m[2];
cloudflare_purge_url("https://i.4cdn.org/$board/$tim.$ext", true);
cloudflare_purge_url("https://i.4cdn.org/$board/${tim}s.jpg", true);
cloudflare_purge_url("https://i.4cdn.org/$board/${tim}m.jpg", true);
}

603
lib/admin.php Normal file
View file

@ -0,0 +1,603 @@
<?
require_once 'db.php';
require_once 'rpc.php';
if( !defined( "SQLLOGMOD" ) ) {
define( 'SQLLOGBAN', 'banned_users' ); // FIXME move to config_db.php?
define( 'SQLLOGMOD', 'mod_users' );
}
// Parses the "email" field and returns a hash
function decode_user_meta($data) {
if (!$data) {
return [];
}
$data = explode(':', $data);
$fields = [];
$fields['browser_id'] = $data[0];
$fields['is_mobile'] = $data[0] && $data[0][0] === '1';
$fields['req_sig'] = $data[1];
$known_status = (int)$data[2];
$fields['verified_level'] = (int)$data[3];
// Brand new user
if ($known_status === 1) {
$fields['is_new'] = true;
$fields['is_known'] = false;
}
// Not new but not trusted yet
else if ($known_status === 2) {
$fields['is_new'] = false;
$fields['is_known'] = false;
}
else {
$fields['is_new'] = false;
$fields['is_known'] = true;
}
$fields['known_status'] = $known_status;
return $fields;
}
// Encodes email field data for storage in the database
// Entries are separates by ":"
// known status: 1 = new user, 2 unknown user
function encode_user_meta($browser_id, $req_sig, $userpwd) {
$known_status = 0;
$verified_level = 0;
if ($userpwd) {
if (!$userpwd->isUserKnown(60, 1)) { // 1h
$known_status = 1; // New user
}
else if (!$userpwd->isUserKnown(1440)) { // 24h
$known_status = 2; // Not yet trusted
}
if ($userpwd->verifiedLevel()) {
$verified_level = 1;
}
}
$data = [ $browser_id, $req_sig, $known_status, $verified_level ];
$data = implode(':', $data);
return $data;
}
function _grep_notjanitor( $a )
{
return ( $a != 'janitor' );
}
function get_random_string( $len = 16 )
{
$str = mt_rand( 1000000, 9999999 );
$str = hash( 'sha256', $str );
return substr( $str, -$len );
}
function derefer_url($url) {
return 'https://www.4chan.org/derefer?url=' . rawurlencode($url);
}
function access_check()
{
global $access;
$user = $_COOKIE['4chan_auser'];
$pass = $_COOKIE['apass'];
if( !$user || !$pass ) return;
$query = mysql_global_call( "SELECT allow,password_expired,level,flags,username,password,signed_agreement FROM mod_users WHERE username='%s' LIMIT 1", $user );
if (!mysql_num_rows($query)) {
return '';
}
list($allow, $expired, $level, $flags, $username, $password, $signed_agreement) = mysql_fetch_row($query);
$admin_salt = file_get_contents('/www/keys/2014_admin.salt');
if (!$admin_salt) {
die('Internal Server Error (s0)');
}
$hashed_admin_password = hash('sha256', $username . $password . $admin_salt);
if ($hashed_admin_password !== $pass) {
return '';
}
if( $expired ) {
die( 'Your password has expired; check IRC for instructions on changing it.' );
}
if ($signed_agreement == 0 && basename($_SERVER['SELF_PATH']) !== 'agreement.php') {
die('You must agree to the 4chan Volunteer Moderator Agreement in order to access moderation tools. Please check your e-mail for more information.');
}
if( $allow ) {
if( $level == 'janitor' ) {
$a = $access['janitor'];
$a['board'] = array_filter( explode( ',', $allow ), '_grep_notjanitor' );
if( in_array( "all", $a['board'] ) )
unset( $a['board'] );
return $a;
} elseif( $level == 'manager' ) {
return $access['manager'];
} elseif( $level == 'admin' ) {
return $access['admin'];
} elseif( $level == 'mod' ) {
if (is_array($access['mod'])) {
$flags = explode(',', $flags);
$access['mod']['is_developer'] = in_array('developer', $flags);
}
return $access['mod'];
} else {
die( 'oh no you are not a right user!' );
}
} else {
return '';
}
}
//based on team pages' valid(), need to merge with above!
//this sets different globals and respects deny
function access_check2( $func = 0 )
{
global $is_admin, $user, $pass;
$is_admin = 0;
$user = "";
$pass = "";
if( isset( $_COOKIE['4chan_auser'] ) && isset( $_COOKIE['4chan_apass'] ) ) {
$user = $_COOKIE['4chan_auser'];
$pass = $_COOKIE['4chan_apass'];
}
if( isset( $user ) && $user && $pass ) {
$result = mysql_global_call( "SELECT allow,deny,password_expired FROM " . SQLLOGMOD . " WHERE username='%s' and password='%s' limit 1", $user, $pass );
if( mysql_num_rows( $result ) != 0 ) {
list( $allowed, $denied, $expired ) = mysql_fetch_array( $result );
if( $expired ) {
die( 'Your password has expired; check IRC for instructions on changing it.' );
}
if( $func == "unban" ) {
$deny_arr = explode( ",", $denied );
if( in_array( "unban", $deny_arr ) ) die( "You do not have access to unban users." );
}
$allow_arr = explode( ",", $allowed );
if( in_array( "admin", $allow_arr ) || in_array( "manager", $allow_arr ) ) $is_admin = 1;
} else {
die( "Please login via admin panel first. (admin user not found)" );
}
if( $user && !$pass ) {
die( "Please login via admin panel first. (no pass specified)" );
} elseif( !$user && $pass ) {
die( "Please login via admin panel first. (no user specified)" );
}
} else {
die( "Please login via admin panel first." );
}
}
function form_post_values( $names )
{
$a = array();
foreach( $names as $n ) {
$v = $_REQUEST[$n];
if( $v ) $a[$n] = $v;
}
return $a;
}
//rebuild the bans for board $boards
function rebuild_bans( $boards )
{
// run in background
$cmd = "nohup /usr/local/bin/suid_run_global bin/rebuildbans $boards >/dev/null 2>&1 &";
// print "<br>Rebuilding bans in $boards<br>";
exec( $cmd );
}
//add list of bans to the file for $boards
function append_bans( $boards, $bans )
{
$str = is_array( $bans ) ? implode( ",", $bans ) : $bans;
$cmd = "nohup /usr/local/bin/suid_run_global bin/appendban $boards $str >/dev/null 2>&1 &";
// print "<br>Added new bans to $boards<br>";
exec( $cmd );
}
// IPs that can't be banned because they're known good proxy servers
// e.g. cloudflare, singapore
function whitelisted_ip( $ip = 0 )
{
list( $ips ) = post_filter_get( "ipwhitelist" );
if( $ip === 0 ) $ip = $_SERVER["REMOTE_ADDR"];
return find_ipxff_in( ip2long( $ip ), 0, $ips );
}
// add a global ban (indefinite for now)
// returns true if it was new (not already inserted)
function add_ban( $ip, $reason, $days = -1, $zonly = false, $origname = 'Anonymous', &$error, $no = 0, $pass = '', $no_reverse = false )
{
global $user;
if( ip2long( $ip ) === false ) {
$error = "invalid IP address";
return false;
}
if( whitelisted_ip( $ip ) ) {
$error = "IP is whitelisted";
return false;
}
// FIXME add unique index to banned_users instead
$prev = mysql_global_call( "SELECT COUNT(*)>0 FROM " . SQLLOGBAN . " WHERE active=1 AND global=1 AND host='%s'", $ip );
list( $nprev ) = mysql_fetch_array( $prev );
if( $nprev > 0 ) return false;
if ($no_reverse) {
$rev = $ip;
}
else {
$rev = gethostbyaddr( $ip );
}
$tripcode = '';
$name_bits = explode('</span> <span class="postertrip">!', $origname);
if ($name_bits[1]) {
$tripcode = preg_replace('/<[^>]+>/', '', $name_bits[1]);
}
$origname = str_replace( '</span> <span class="postertrip">!', ' #', $origname );
$origname = preg_replace( '/<[^>]+>/', '', $origname ); // remove all remaining html crap
$board = defined( 'BOARD_DIR' ) ? BOARD_DIR : "";
if( $days == -1 )
$length = "00000000000000";
else
$length = date( "Ymd", time() + $days * ( 24 * 60 * 60 ) ) . '000000';
echo "Banned $ip (" . htmlspecialchars( $rev ) . ")<br>\n";
if (!isset($user)) {
$banned_by = $_COOKIE['4chan_auser'];
}
else {
$banned_by = $user;
}
mysql_global_do( "INSERT INTO " . SQLLOGBAN . " (global,board,host,reverse,reason,admin,zonly,length,name,tripcode,4pass_id,post_num,admin_ip) values (%d,'%s','%s','%s','%s','%s',%d,'%s','%s','%s','%s',%d,'%s')", !$zonly, $board, $ip, $rev, "$reason", $banned_by, $zonly, $length, $origname, $tripcode, $pass, $no, $_SERVER['REMOTE_ADDR'] );
return true;
}
function is_real_board( $board )
{
// no board
if( $board === "-" || $board === '' ) return true;
$res = mysql_global_call( "select count(*) from boardlist where dir='%s'", $board );
$row = mysql_fetch_row( $res );
return ( $row[0] > 0 );
}
function remote_delete_things( $board, $nos, $tool = null )
{
// see reports/actions.php, action_delete()
$url = "https://sys.int/$board/";
if( $board != 'f' ) // XXX dumb. :( XXX
$url .= 'imgboard.php';
else
$url .= 'up.php';
// Build the appropriate POST and cookie...
$post = array();
$post['mode'] = 'usrdel';
$post['onlyimgdel'] = ''; // never delete only img
if ($tool) {
$post['tool'] = $tool;
}
// note multiple post number deletions
foreach( $nos as $no )
$post[$no] = 'delete';
$post['remote_addr'] = $_SERVER['REMOTE_ADDR'];
rpc_start_request($url, $post, $_COOKIE, true);
return "";
}
function clear_cookies()
{
if( strstr( $_SERVER["HTTP_HOST"], ".4chan.org" ) ) {
setcookie( "4chan_auser", "", time() - 3600, "/", ".4chan.org", true );
setcookie( "4chan_apass", "", time() - 3600, "/", ".4chan.org", true );
setcookie( "4chan_aflags", "", time() - 3600, "/", ".4chan.org", true );
} elseif( strstr( $_SERVER["HTTP_HOST"], ".4channel.org" ) ) {
setcookie( "4chan_auser", "", time() - 24 * 3600, "/", ".4channel.org", true );
setcookie( "4chan_apass", "", time() - 24 * 3600, "/", ".4channel.org", true );
} else {
setcookie( "4chan_auser", "", time() - 24 * 3600, "/", true );
setcookie( "4chan_apass", "", time() - 24 * 3600, "/", true );
setcookie( "4chan_aflags", "", time() - 24 * 3600, "/", true );
}
setcookie( 'extra_path', '', 1, '/', '.4chan.org' );
}
// record and autoban failed logins. assumes admin or imgboard.php as caller
function admin_login_fail()
{
$ip = ip2long( $_SERVER["REMOTE_ADDR"] );
clear_cookies();
mysql_global_call( "insert into user_actions (ip,board,action,time) values (%d,'%s','fail_login',now())", $ip, BOARD_DIR );
$query = mysql_global_call( "select count(*)>%d from user_actions where ip=%d and action='fail_login' and time >= subdate(now(), interval 1 hour)", LOGIN_FAIL_HOURLY, $ip );
if( mysql_result( $query, 0, 0 ) ) {
auto_ban_poster( "", -1, 1, "failed to login to /" . BOARD_DIR . "/admin.php " . LOGIN_FAIL_HOURLY . " times", "Repeated admin login failures." );
}
error( S_WRONGPASS );
}
// delete all posts everywhere by the poster's IP
// for autobans
function del_all_posts( $ip = false )
{
$q = mysql_global_call( "select sql_cache dir from boardlist" );
$boards = mysql_column_array( $q );
$host = $ip ? $ip : $_SERVER['REMOTE_ADDR'];
foreach( $boards as $b ) {
$q = mysql_board_call( "select no from `%s` where host='%s'", $b, $host );
$posts = mysql_column_array( $q );
if( !count( $posts ) ) continue;
remote_delete_things( $b, $posts );
}
}
function auto_ban_poster($nametrip, $banlength, $global, $reason, $pubreason = '', $is_filter = false, $pwd = null, $pass_id = null) {
if (!$nametrip) {
$nametrip = S_ANONAME;
}
if (strpos($nametrip, '</span> <span class="postertrip">!') !== false) {
$nameparts = explode('</span> <span class="postertrip">!', $nametrip);
$nametrip = "{$nameparts[0]} #{$nameparts[1]}";
}
$host = $_SERVER['REMOTE_ADDR'];
$reverse = mysql_real_escape_string(gethostbyaddr($host));
$nametrip = mysql_real_escape_string($nametrip);
$global = ($global ? 1 : 0);
$board = defined( 'BOARD_DIR' ) ? BOARD_DIR : '';
$reason = mysql_real_escape_string($reason);
$pubreason = mysql_real_escape_string($pubreason);
if ($pubreason) {
$pubreason .= "<>";
}
if ($pass_id) {
$pass_id = mysql_real_escape_string($pass_id);
}
else {
$pass_id = '';
}
if ($pwd) {
$pwd = mysql_real_escape_string($pwd);
}
else {
$pwd = '';
}
// check for whitelisted ban
if( whitelisted_ip() ) return;
//if they're already banned on this board, don't insert again
//since this is just a spam post
//i don't think it matters if the active ban is global=0 and this one is global=1
/*
if ($banlength == -1) {
$existingq = mysql_global_do("select count(*)>0 from " . SQLLOGBAN . " where host='$host' and active=1 AND global = 1 AND length = 0");
}
else {
$existingq = mysql_global_do("select count(*)>0 from " . SQLLOGBAN . " where host='$host' and active=1 and (board='$board' or global=1)");
}
$existingban = mysql_result( $existingq, 0, 0 );
if( $existingban > 0 ) {
delete_uploaded_files();
die();
}
*/
/*
if( $banlength == 0 ) { // warning
// check for recent warnings to punish spammers
$autowarnq = mysql_global_call( "SELECT COUNT(*) FROM " . SQLLOGBAN . " WHERE host='$host' AND admin='Auto-ban' AND now > DATE_SUB(NOW(),INTERVAL 3 DAY) AND reason like '%$reason'" );
$autowarncount = mysql_result( $autowarnq, 0, 0 );
if( $autowarncount > 3 ) {
$banlength = 14;
}
}
*/
if ($banlength == -1) { // permanent
$length = '0000' . '00' . '00'; // YYYY/MM/DD
}
else {
$banlength = (int)$banlength;
if ($banlength < 0) {
$banlength = 0;
}
$length = date('Ymd', time() + $banlength * (24 * 60 * 60));
}
$length .= "00" . "00" . "00"; // H:M:S
$sql = "INSERT INTO " . SQLLOGBAN . " (board,global,name,host,reason,length,admin,reverse,post_time,4pass_id,password) VALUES('$board','$global','$nametrip','$host','{$pubreason}Auto-ban: $reason','$length','Auto-ban','$reverse',NOW(),'$pass_id','$pwd')";
$res = mysql_global_call($sql);
if (!$res) {
die(S_SQLFAIL);
}
//append_bans( $global ? "global" : $board, array($host) );
//$child = stripos($pubreason, 'child') !== false || stripos($reason, 'child') !== false;
//if ($global && $child && !$is_filter) {
// del_all_posts();
//}
}
function cloudflare_purge_url_old($file,$secondary = false)
{
global $purges;
if (!defined('CLOUDFLARE_API_TOKEN')) {
internal_error_log('cf', "tried purging but token isn't set");
return null;
}
$post = array(
"tkn" => CLOUDFLARE_API_TOKEN,
"email" => CLOUDFLARE_EMAIL,
"a" => "zone_file_purge",
"z" => $secondary ? CLOUDFLARE_ZONE_2 : CLOUDFLARE_ZONE,
"url" => $file
);
//quick_log_to("/www/perhost/cf-purge.log", print_r($post, true));
$ch = rpc_start_request("https://www.cloudflare.com/api_json.html", $post, array(), false);
return $ch;
}
function write_to_event_log($event, $ip, $args = []) {
$sql = <<<SQL
INSERT INTO event_log(`type`, ip, board, thread_id, post_id, arg_num,
arg_str, pwd, req_sig, ua_sig, meta)
VALUES('%s', '%s', '%s', '%d', '%d', '%d',
'%s', '%s', '%s', '%s', '%s')
SQL;
return mysql_global_call($sql, $event, $ip,
$args['board'], $args['thread_id'], $args['post_id'], $args['arg_num'],
$args['arg_str'], $args['pwd'], $args['req_sig'], $args['ua_sig'], $args['meta']
);
}
function log_staff_event($event, $username, $ip, $pwd, $board, $post) {
$json_post = [];
if ($post['sub'] !== '') {
$json_post['sub'] = $post['sub'];
}
if ($post['name'] !== '') {
$json_post['name'] = $post['name'];
}
if ($post['com'] !== '') {
$json_post['com'] = $post['com'];
}
if ($post['fsize'] > 0) {
$json_post['file'] = $post["filename"].$post["ext"];
$json_post['md5'] = $post["md5"];
}
$json_post = json_encode($json_post, JSON_PARTIAL_OUTPUT_ON_ERROR);
return write_to_event_log($event, $ip, [
'board' => $board,
'thread_id' => $post['resto'] ? $post['resto'] : $post['no'],
'post_id' => $post['no'],
'arg_str' => $username,
'pwd' => $pwd,
'meta' => $json_post
]);
}
function cloudflare_purge_url($files, $zone2 = false) {
// 4cdn = ca66ca34d08802412ae32ee20b7e98af (zone2)
// 4chan = 363d1b9b6be563ffd5143c8cfcc29d52
$url = 'https://api.cloudflare.com/client/v4/zones/'
. ($zone2 ? 'ca66ca34d08802412ae32ee20b7e98af' : '363d1b9b6be563ffd5143c8cfcc29d52')
. '/purge_cache';
$opts = array(
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => array(
'Authorization: Bearer iTf0pQMTvn0zSHAN9vg5S1m_tiwmPKYDjepq8za9',
'Content-Type: application/json'
)
);
// Multiple files
if (is_array($files)) {
// Batching
if (count($files) > 30) {
$files = array_chunk($files, 30);
foreach ($files as $batch) {
$opts[CURLOPT_POSTFIELDS] = '{"files":' . json_encode($batch, JSON_UNESCAPED_SLASHES) . '}';
//print_r($opts[CURLOPT_POSTFIELDS]);
rpc_start_request_with_options($url, $opts);
}
}
else {
$opts[CURLOPT_POSTFIELDS] = '{"files":' . json_encode($files, JSON_UNESCAPED_SLASHES) . '}';
//print_r($opts[CURLOPT_POSTFIELDS]);
rpc_start_request_with_options($url, $opts);
}
}
// Single file
else {
$opts[CURLOPT_POSTFIELDS] = '{"files":["' . $files . '"]}';
//print_r($opts[CURLOPT_POSTFIELDS]);
rpc_start_request_with_options($url, $opts);
}
}
function cloudflare_purge_by_basename($board, $basename) {
preg_match("/([0-9]+)[sm]?\\.([a-z]{3,4})/", $basename, $m);
$tim = $m[1];
$ext = $m[2];
cloudflare_purge_url("https://i.4cdn.org/$board/$tim.$ext", true);
cloudflare_purge_url("https://i.4cdn.org/$board/${tim}s.jpg", true);
cloudflare_purge_url("https://i.4cdn.org/$board/${tim}m.jpg", true);
}

155
lib/ads-test.php Normal file
View file

@ -0,0 +1,155 @@
<?
// pick a random row from tablename and return the img column
// FIXME this is a duplicate of the function below it...
// about 4 tables in global are the same thing with different schema
function rid($tablename,$usetext=0) {
if ($usetext) {
$fields = "img,link";
} else {
$fields = "img";
}
$ret = mysql_global_call("select $fields from `%s` join (select floor(1+rand()*(select max(id) from `%s`)) as id) as randid using (id)", $tablename, $tablename);
if ($usetext) {
return mysql_fetch_row($ret);
} else {
return mysql_result($ret,0,0);
}
}
//file format:
//url (escaped for putting in a href)
//desc (not escaped)
//repeat
function text_link_ad($file) {
global $text_ads;
global $text_ads_n;
if (!isset($text_ads[$file])) {
$ads = @file($file, FILE_IGNORE_NEW_LINES);
shuffle($ads);
$text_ads[$file] = $ads;
$num_ads = count($ads);
$n = 0;
$text_ads_n[$file] = $n;
} else {
$ads = $text_ads[$file];
$n = $text_ads_n[$file];
$num_ads = count($ads);
}
if (!$ads) return "";
list($url,$desc) = explode("<>",$ads[$n]);
if (!$url) return "";
$text = "<strong><a href=\"$url\" rel=\"nofollow\" target=\"_blank\">".htmlspecialchars($desc)."</a></strong>";
$n++;
if ($n == $num_ads)
$n = 0;
$text_ads_n[$file] = $n;
return $text;
}
//duplicate of rid.php
//dir - absolute path from web root to dir inc. trailing slash
//urlroot - what to append the name to to get the url
function rid_in_directory($dir,$urlroot) {
global $document_root;
$realdir = "/www/global/imgtop/dontblockthis/".$dir;
$ft = "$realdir/files.txt";
$names = file_array_cached($ft);
if (!$names) {
$arr = scandir($realdir);
foreach ($arr as $fi) {
if (preg_match("/\.(jpg|gif|png)$/", $fi)) {
$names[] = $fi;
}
}
file_put_contents($ft, join($names, "\n"));
}
return $urlroot.$names[rand(0, count($names)-1)];
}
// Takes a dir and a filename and uses file() to parse it
// then returns a random value.
function rand_from_flatfile( $dir, $filename )
{
$file = $dir . $filename;
$names = file_array_cached( $file );
return $names[ rand( 0, count($names)-1 ) ];
}
function form_ads(&$dat) {
$error = false; // unused, errors have ads too
$dat .= "<div style='position:relative'>";
/*if(!$error && FIXED_AD == 1) {
$dat.='<a href="'.FIXED_LINK.'" target="_blank"><img src="//static.4chan.org/support/'.FIXED_IMG.'" width="120" height="240" border="0" style="position: absolute; top: '.$gtop.'px; right: 20px"></a>';
}*/
if(FIXED_LEFT_AD == 1) {
if(defined('FIXED_LEFT_TXT') && FIXED_LEFT_TXT) {
$dat.= ad_text_for(FIXED_LEFT_TXT);
}
else if(defined('FIXED_LEFT_TABLE')) {
list($ldimg,$ldhref) = rid(FIXED_LEFT_TABLE,1);
$dat.='<a href="'.$ldhref.'" target="_blank"><img src="'.$ldimg.'" width=120 height=240 border="0" style="position:absolute;left:10%"></a>';
}
}
if(FIXED_RIGHT_AD == 1) {
if(defined('FIXED_RIGHT_TXT') && FIXED_RIGHT_TXT) {
$dat.= ad_text_for(FIXED_RIGHT_TXT);
}
else if(defined('FIXED_RIGHT_TABLE')) {
list($ldimg,$ldhref) = rid(FIXED_RIGHT_TABLE,1);
$dat.='<a href="'.$ldhref.'" target="_blank"><img src="'.$ldimg.'" border="0" style="position:absolute;right:10%"></a>';
}
}
$dat .= "</div>";
}
function ad_text_for($path) {
$txt = @file_get_contents_cached($path);
if (!$txt) return $txt;
return preg_replace_callback("@RANDOM@", "rand", $txt);
}
function global_msg_txt() {
static $globalmsgtxt, $globalmsgdate;
if (!$globalmsgdate) {
if (file_exists(GLOBAL_MSG_FILE)) {
$globalmsgtxt = file_get_contents(GLOBAL_MSG_FILE);
$globalmsgdate = filemtime(GLOBAL_MSG_FILE);
if ($globalmsgtxt) {
$globalmsgtxt = str_replace('{{4CHAN_DOMAIN}}', MAIN_DOMAIN, $globalmsgtxt);
}
}
else {
$globalmsgtxt = null;
$globalmsgdate = 1;
}
}
return array($globalmsgtxt, $globalmsgdate);
}
?>

145
lib/ads.php Normal file
View file

@ -0,0 +1,145 @@
<?
// pick a random row from tablename and return the img column
// FIXME this is a duplicate of the function below it...
// about 4 tables in global are the same thing with different schema
function rid($tablename,$usetext=0) {
if ($usetext) {
$fields = "img,link";
} else {
$fields = "img";
}
$ret = mysql_global_call("select $fields from `%s` join (select floor(1+rand()*(select max(id) from `%s`)) as id) as randid using (id)", $tablename, $tablename);
if ($usetext) {
return mysql_fetch_row($ret);
} else {
return mysql_result($ret,0,0);
}
}
//file format:
//url (escaped for putting in a href)
//desc (not escaped)
//repeat
function text_link_ad($file) {
global $text_ads;
global $text_ads_n;
if (!isset($text_ads[$file])) {
$ads = @file($file, FILE_IGNORE_NEW_LINES);
shuffle($ads);
$text_ads[$file] = $ads;
$num_ads = count($ads);
$n = 0;
$text_ads_n[$file] = $n;
} else {
$ads = $text_ads[$file];
$n = $text_ads_n[$file];
$num_ads = count($ads);
}
if (!$ads) return "";
list($url,$desc) = explode("<>",$ads[$n]);
if (!$url) return "";
$text = "<strong><a href=\"$url\" rel=\"nofollow\" target=\"_blank\">".htmlspecialchars($desc)."</a></strong>";
$n++;
if ($n == $num_ads)
$n = 0;
$text_ads_n[$file] = $n;
return $text;
}
//duplicate of rid.php
//dir - absolute path from web root to dir inc. trailing slash
//urlroot - what to append the name to to get the url
function rid_in_directory($dir,$urlroot) {
global $document_root;
$realdir = "/www/global/imgtop/dontblockthis/".$dir;
$ft = "$realdir/files.txt";
$names = file_array_cached($ft);
if (!$names) {
$arr = scandir($realdir);
foreach ($arr as $fi) {
if (preg_match("/\.(jpg|gif|png)$/", $fi)) {
$names[] = $fi;
}
}
file_put_contents($ft, join($names, "\n"));
}
return $urlroot.$names[rand(0, count($names)-1)];
}
// Takes a dir and a filename and uses file() to parse it
// then returns a random value.
function rand_from_flatfile( $dir, $filename )
{
$file = $dir . $filename;
$names = file_array_cached( $file );
return $names[ rand( 0, count($names)-1 ) ];
}
function form_ads(&$dat) {
$error = false; // unused, errors have ads too
$dat .= "<div style='position:relative'>";
/*if(!$error && FIXED_AD == 1) {
$dat.='<a href="'.FIXED_LINK.'" target="_blank"><img src="//static.4chan.org/support/'.FIXED_IMG.'" width="120" height="240" border="0" style="position: absolute; top: '.$gtop.'px; right: 20px"></a>';
}*/
if(FIXED_LEFT_AD == 1) {
if(defined('FIXED_LEFT_TXT') && FIXED_LEFT_TXT) {
$dat.= ad_text_for(FIXED_LEFT_TXT);
}
else if(defined('FIXED_LEFT_TABLE')) {
list($ldimg,$ldhref) = rid(FIXED_LEFT_TABLE,1);
$dat.='<a href="'.$ldhref.'" target="_blank"><img src="'.$ldimg.'" width=120 height=240 border="0" style="position:absolute;left:10%"></a>';
}
}
if(FIXED_RIGHT_AD == 1) {
if(defined('FIXED_RIGHT_TXT') && FIXED_RIGHT_TXT) {
$dat.= ad_text_for(FIXED_RIGHT_TXT);
}
else if(defined('FIXED_RIGHT_TABLE')) {
list($ldimg,$ldhref) = rid(FIXED_RIGHT_TABLE,1);
$dat.='<a href="'.$ldhref.'" target="_blank"><img src="'.$ldimg.'" border="0" style="position:absolute;right:10%"></a>';
}
}
$dat .= "</div>";
}
function ad_text_for($path) {
$txt = @file_get_contents_cached($path);
if (!$txt) return $txt;
return preg_replace_callback("@RANDOM@", "rand", $txt);
}
function global_msg_txt() {
static $globalmsgtxt, $globalmsgdate;
if (!$globalmsgdate) {
$globalmsgtxt = @file_get_contents(GLOBAL_MSG_FILE);
$globalmsgdate = @filemtime(GLOBAL_MSG_FILE);
}
return array($globalmsgtxt, $globalmsgdate);
}
?>

171
lib/archives.php Normal file
View file

@ -0,0 +1,171 @@
<?php
function return_archive_link( $board, $resno, $admin = false, $url_only = false, $thread_id = 0 )
{
switch( $board ) {
case 'a':
case 'aco':
case 'an':
case 'c':
case 'co':
case 'd':
case 'fit':
case 'g':
case 'his':
case 'int':
case 'k':
case 'm':
case 'mu':
case 'mlp':
case 'qa':
case 'r9k':
case 'tg':
case 'trash':
case 'vr':
case 'wsg':
$url = "https://desuarchive.org/$board/post/$resno";
break;
case 'h':
case 'hc':
case 'hm':
case 'i':
case 'lgbt':
case 'r':
case 's':
case 'soc':
case 't':
case 'u':
$url = "http://archiveofsins.com/$board/post/$resno";
break;
case 'asp':
case 'cm':
case 'y':
case 'b':
case 'ck':
case 'gd':
case 'gif':
case 'po':
case 'xs':
$url = "http://archived.moe/$board/post/$resno";
break;
case 'bant':
case 'news':
case 'out':
case 'p':
case 'pw':
case 'e':
case 'n':
case 'qst':
case 'toy':
case 'vip':
case 'vt':
case 'vp':
case 'w':
case 'wg':
case 'wsr':
$url = "https://archive.palanq.win/$board/post/$resno";
break;
case 'v':
case 'vrpg':
case 'vmg':
case 'vm':
case 'vg':
case 'vst':
$url = "https://arch.b4k.dev/$board/post/$resno";
break;
case 'adv':
case 'f':
case 'hr':
case 'o':
case 'pol':
case 's4s':
case 'sp':
case 'trv':
case 'tv':
case 'x':
$url = "https://archive.4plebs.org/$board/post/$resno";
break;
case '3':
case 'biz':
case 'cgl':
case 'diy':
case 'fa':
case 'ic':
case 'sci':
case 'jp':
case 'lit':
$url = "https://warosu.org/$board/?task=post&ghost=&post=$resno";
break;
/*
if ($thread_id) {
$url = "https://yuki.la/$board/$thread_id";
if ($resno) {
$url .= '#' . $resno;
}
break;
}
*/
// Return a link to the deletion log
default:
if ($url_only) {
return false;
}
if (!$admin) {
return '/' . $board . '/' . $resno;
}
/*
$u = $_COOKIE['4chan_auser'];
if (has_level('manager')) {
$allow = mysql_global_call("SELECT admin,id FROM `" . SQLLOGDEL . "` WHERE postno=%d AND board='%s'", $resno, $board);
if (!mysql_num_rows($allow)) {
return 'Could not find admin/post.';
}
$res = mysql_fetch_assoc( $allow );
$admin = $res['admin'];
$id = $res['id'];
$allow = mysql_global_call("SELECT allow FROM `" . SQLLOGMOD . "` WHERE username = '%s'", $admin);
$res = mysql_fetch_assoc($allow);
$file = (strpos( $res['allow'], 'janitor') !== false) ? 'log_janitors' : 'admin/log_moderators';
}
else {
$allow = mysql_global_call("SELECT allow FROM `" . SQLLOGMOD . "` WHERE username='%s' AND allow LIKE '%s'", $admin, '%janitor%');
if (!mysql_num_rows($allow)) {
return 'None Available.';
}
$allow = mysql_global_call("SELECT admin,id FROM `" . SQLLOGDEL . "` WHERE postno=%d AND board='%s'", $resno, $board);
if (!mysql_num_rows($allow)) {
return 'Could not find post.';
}
$res = mysql_fetch_assoc($allow);
$id = $res['id'];
$file = 'log_janitors';
}
*/
return '<a href="https://team.4chan.org/stafflog#board=' . $board . ',post=' . $resno . '" rel="noreferrer" target="_blank">/' . $board . '/' . $resno . '</a>';
}
if ($url_only) {
return $url;
}
$url = rawurlencode($url);
return '<a href="https://www.4chan.org/derefer?url=' .
$url . '" target="_blank">/' . $board . '/' . $resno . '</a>';
}

594
lib/auth-test.php Normal file
View file

@ -0,0 +1,594 @@
<?php
/** User authentication / flag stuff */
$auth = array(
'level' => false,
'flags' => false,
'allow' => false,
'deny' => false,
'guest' => true,
);
$levelorder = array(
1 => 'janitor',
10 => 'mod',
20 => 'manager',
50 => 'admin'
);
$levelorderf = array(
'janitor' => 1,
'mod' => 10,
'manager' => 20,
'admin' => 50
);
if (!defined('SQLLOGMOD')) {
define("SQLLOGMOD", "mod_users");
define('PASS_TIMEOUT', 1800);
define('LOGIN_FAIL_HOURLY', 5);
}
function csrf_tag() {
if (isset($_COOKIE['_tkn'])) {
return '<input type="hidden" value="' . htmlspecialchars($_COOKIE['_tkn'], ENT_QUOTES) . '" name="_tkn">';
}
else {
return '';
}
}
function csrf_attr() {
if (isset($_COOKIE['_tkn'])) {
return 'data-tkn="' . htmlspecialchars($_COOKIE['_tkn'], ENT_QUOTES) . '"';
}
else {
return '';
}
}
function auth_encrypt($data) {
$key = file_get_contents('/www/keys/2015_enc.key');
if (!$key) {
return false;
}
$iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC);
$iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);
$encrypted = mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $key, $data, MCRYPT_MODE_CBC, $iv);
if ($encrypted === false) {
return false;
}
return $iv . $encrypted;
}
function auth_decrypt($data) {
$key = file_get_contents('/www/keys/2015_enc.key');
if (!$key) {
return false;
}
$iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC);
$iv_dec = substr($data, 0, $iv_size);
$data = substr($data, $iv_size);
$data = mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $key, $data, MCRYPT_MODE_CBC, $iv_dec);
if ($data === false) {
return false;
}
return rtrim($data, "\0");
}
function verify_one_time_pwd($username, $otp) {
if (!$otp) {
return false;
}
$query = "SELECT auth_secret FROM mod_users WHERE username = '%s' LIMIT 1";
$res = mysql_global_call($query, $username);
if (!$res) {
return false;
}
$enc_secret = mysql_fetch_row($res)[0];
if (!$enc_secret) {
return false;
}
require_once 'lib/GoogleAuthenticator.php';
$ga = new PHPGangsta_GoogleAuthenticator();
$dec_secret = auth_decrypt($enc_secret);
if ($dec_secret === false) {
return false;
}
if ($ga->verifyCode($dec_secret, $otp, 2)) {
return true;
}
return false;
}
/**
* Returns a hash containing implicit levels for the current authed level
* ex: will return array('janitor' => true, 'mod' => true)
* if the current level is 'mod'
*/
function get_level_map($level = null) {
global $auth, $levelorderf;
$map = array();
if (!$level) {
$level = $auth['level'];
}
if (!$level) {
return $map;
}
$level_value = (int)$levelorderf[$level];
foreach ($levelorderf as $k => $v) {
if ($v <= $level_value) {
$map[$k] = true;
}
}
return $map;
}
function has_level( $level = 'mod', $board = false )
{
if( is_local_auth() ) return YES;
global $auth, $levelorder, $levelorderf;
static $ourlevel = -1;
//if( !$board && defined( 'BOARD_DIR' ) ) $board = BOARD_DIR;
//if( !access_board($board) ) return false;
if( $ourlevel < 0 ) $ourlevel = $levelorderf[$auth['level']];
if (!isset($levelorderf[$level])) {
return false;
}
if( $levelorderf[$level] <= $ourlevel ) return true;
return false;
}
function has_flag( $flag, $board = false )
{
if( is_local_auth() ) return YES;
global $auth;
if( $auth['guest'] ) return false;
if( !access_board( $board ) ) return false;
if( in_array( $flag, $auth['flags'] ) ) return true;
return false;
}
function access_board( $board )
{
if( is_local_auth() ) return YES;
global $auth;
if( $auth['guest'] ) return false;
$can_do = false;
// See if we have access to this board or all
if( in_array( 'all', $auth['allow'] ) || in_array( $board, $auth['allow'] ) ) $can_do = true;
// Are we denied on this board?
if( $board && in_array( $board, $auth['deny'] ) ) $can_do = false;
// If we're not using a board, are we denied for no-board stuff?
if( !$board && in_array( 'noboard', $auth['deny'] ) ) $can_do = false;
return $can_do;
}
function is_user()
{
if( is_local_auth() ) return YES;
global $auth;
if( $auth['guest'] ) return false;
if( $auth['level'] ) return true;
return false;
}
function auth_user($skip_agreement = false) {
global $auth;
$user = $_COOKIE['4chan_auser'];
$pass = $_COOKIE['apass'];
if( !$user || !$pass ) return false;
$query = mysql_global_call("SELECT * FROM `%s` WHERE `username` = '%s' LIMIT 1", SQLLOGMOD, $user);
if (!mysql_num_rows($query)) {
return false;
}
$fetch = mysql_fetch_assoc($query);
$admin_salt = file_get_contents('/www/keys/2014_admin.salt');
if (!$admin_salt) {
die('Internal Server Error (s0)');
}
$hashed_admin_password = hash('sha256', $fetch['username'] . $fetch['password'] . $admin_salt);
if ($hashed_admin_password !== $pass) {
return false;
}
if ($fetch['password_expired'] == 1) {
die('Your password has expired; check IRC for instructions on changing it.');
}
if (!$skip_agreement) {
if ($fetch['signed_agreement'] == 0 && basename($_SERVER['SELF_PATH']) !== 'agreement.php' && basename($_SERVER['SELF_PATH']) !== 'agreement_genkey.php') {
die('You must agree to the 4chan Volunteer Moderator Agreement in order to access moderation tools. Please check your e-mail for more information.');
}
}
$auth['level'] = $fetch['level'];
$auth['flags'] = explode( ',', $fetch['flags'] );
$auth['allow'] = explode( ',', $fetch['allow'] );
$auth['deny'] = explode( ',', $fetch['deny'] );
$auth['guest'] = false;
$flags = array();
if( has_level( 'admin' ) ) {
$flags['forcedanonname'] = 2;
}
if( has_level( 'manager' ) || has_flag( 'html' ) ) {
$flags['html'] = 1;
}
$flags = array_flip( $flags );
$flags = implode( ',', $flags );
$ips_array = json_decode($fetch['ips'], true);
if (json_last_error() !== JSON_ERROR_NONE) {
die('Database Error (1-0)');
}
$ips_array[$_SERVER['REMOTE_ADDR']] = $_SERVER['REQUEST_TIME'];
if (count($ips_array) > 512) {
asort($ips_array);
array_shift($ips_array);
}
$ips_array = json_encode($ips_array);
if (json_last_error() !== JSON_ERROR_NONE) {
die('Database Error (1-1)');
}
if (mb_strlen($_SERVER['HTTP_USER_AGENT']) > 128) {
$ua = mb_substr($_SERVER['HTTP_USER_AGENT'], 0, 128);
}
else {
$ua = $_SERVER['HTTP_USER_AGENT'];
}
mysql_global_call("UPDATE `%s` SET ips = '$ips_array', last_ua = '%s' WHERE id = %d LIMIT 1", SQLLOGMOD, $ua, $fetch['id']);
return true;
}
// OLD auth
/*
function auth_user( $login = false )
{
global $auth;
if( $login ) {
$user = $_POST['userlogin'];
$pass = $_POST['passlogin'];
} else {
$user = $_COOKIE['4chan_auser'];
$pass = $_COOKIE['4chan_apass'];
}
if( !$user || !$pass ) return false;
$query = mysql_global_call( "SELECT * FROM `%s` WHERE `username` = '%s' LIMIT 1", SQLLOGMOD, $user );
if( !mysql_num_rows( $query ) ) return false;
$fetch = mysql_fetch_assoc( $query );
if( $fetch['password_expired'] == 1 ) {
die( 'Your password has expired; check IRC for instructions on changing it.' );
}
if ($login) {
if( !password_verify($pass, $fetch['password'])) return false;
$pass = $fetch['password'];
} else {
if ($pass != $fetch['password']) return false;
}
$auth['level'] = $fetch['level'];
$auth['flags'] = explode( ',', $fetch['flags'] );
$auth['allow'] = explode( ',', $fetch['allow'] );
$auth['deny'] = explode( ',', $fetch['deny'] );
$auth['guest'] = false;
$flags = array();
if( has_level( 'admin' ) && $user == 'moot' ) {
$flags['forcedanonname'] = 2;
}
if( has_level( 'manager' ) || has_flag( 'html' ) ) {
$flags['html'] = 1;
}
$flags = array_flip( $flags );
$flags = implode( ',', $flags );
$ips_array = json_decode($fetch['ips'], true);
if (json_last_error() !== JSON_ERROR_NONE) {
die('Database Error (1-0)');
}
$ips_array[$_SERVER['REMOTE_ADDR']] = $_SERVER['REQUEST_TIME'];
$ips_array = json_encode($ips_array);
if (json_last_error() !== JSON_ERROR_NONE) {
die('Database Error (1-1)');
}
if ($login) {
$login_query = ", last_login = now()";
}
else {
if (!isset($_COOKIE['apass'])) {
return false;
}
$admin_salt = file_get_contents('/www/keys/2014_admin.salt');
if (!$admin_salt) {
die('Internal Server Error (s0)');
}
$hashed_admin_cookie = $_COOKIE['apass'];
$hashed_admin_password = hash('sha256', $fetch['username'] . $fetch['password'] . $admin_salt);
if ($hashed_admin_password !== $hashed_admin_cookie) {
return false;
}
$login_query = '';
}
mysql_global_do("UPDATE `%s` SET ips = '$ips_array' $login_query WHERE id = %d", SQLLOGMOD, $fetch['id']);
if( !isset( $_COOKIE['4chan_auser'] ) || !isset( $_COOKIE['4chan_apass'] ) ) {
if( strstr( $_SERVER["HTTP_HOST"], ".4chan.org" ) ) {
setcookie( "4chan_auser", $user, time() + 30 * 24 * 3600, "/", ".4chan.org", true, true );
setcookie( "4chan_apass", $pass, time() + 30 * 24 * 3600, "/", ".4chan.org", true, true );
setcookie( "4chan_aflags", $flags, time() + 30 * 24 * 3600, "/", ".4chan.org", true );
$jspath = $auth['level'] == 'janitor' ? JANITOR_JS_PATH : ADMIN_JS_PATH;
if( !isset( $_COOKIE['extra_path'] ) || !in_array( $_COOKIE['extra_path'], array(JANITOR_JS_PATH, ADMIN_JS_PATH) ) ) {
setcookie( 'extra_path', $jspath, time() + ( 30 * 24 * 3600 ), '/', '.4chan.org' );
}
} elseif( strstr( $_SERVER["HTTP_HOST"], ".4channel.org" ) ) {
setcookie( "4chan_auser", $user, time() + 30 * 24 * 3600, "/", ".4channel.org", true, true );
setcookie( "4chan_apass", $pass, time() + 30 * 24 * 3600, "/", ".4channel.org", true, true );
} else {
die( 'Not 4chan.org' );
}
}
return true;
}
*/
function is_local_auth()
{
if (!isset($_SERVER['REMOTE_ADDR'])) {
return true;
}
// local rpc can do anything
$longip = ip2long( $_SERVER['REMOTE_ADDR'] );
if(
cidrtest( $longip, "10.0.0.0/24" ) ||
cidrtest( $longip, "204.152.204.0/24" ) ||
cidrtest( $longip, "127.0.0.0/24" )
) {
return YES;
}
return false;
}
function can_delete( $resno )
{
if( !has_level( 'janitor' ) ) return false;
if( has_level( 'janitor' ) && access_board( BOARD_DIR ) ) return true;
//if( !access_board(BOARD_DIR) ) return false;
$query = mysql_global_do( "SELECT COUNT(*) from reports WHERE board='%s' AND no=%d AND cat=2", BOARD_DIR, $resno );
$illegal_count = mysql_result( $query, 0, 0 );
mysql_free_result( $query );
return $illegal_count >= 3;
}
function start_auth_captcha($use_alt_captcha = false)
{
if (valid_captcha_bypass() !== true) {
if ($use_alt_captcha) {
start_recaptcha_verify_alt();
}
else {
start_recaptcha_verify();
}
}
}
function clear_pass_cookies() {
setcookie('pass_id', null, 1, '/', 'sys.4chan.org', true, true);
setcookie('pass_id', null, 1, '/', '.4chan.org', true, true);
setcookie('pass_enabled', null, 1, '/', '.4chan.org');
}
function valid_captcha_bypass()
{
global $captcha_bypass, $passid, $rangeban_bypass;
$captcha_bypass = false;
$rangeban_bypass = false;
$passid = '';
if (is_local_auth() || has_level('janitor')) {
$captcha_bypass = true;
$rangeban_bypass = true;
return true;
}
if (CAPTCHA != 1) {
$captcha_bypass = true;
}
$time = $_SERVER['REQUEST_TIME'];
$host = $_SERVER['REMOTE_ADDR'];
// check for 4chan pass
$pass_cookie = isset( $_COOKIE['pass_id'] ) ? $_COOKIE['pass_id'] : '';
if (strlen($pass_cookie) == 10) {
setcookie('pass_id', '0', 1, '/', '.4chan.org', true, true);
setcookie('pass_enabled', '0', 1, '/', '.4chan.org');
error(S_PASSFORMATCHANGED);
}
if ($pass_cookie) {
$pass_parts = explode('.', $pass_cookie);
$pass_user = $pass_parts[0];
$pass_session = $pass_parts[1];
if (!$pass_user || !$pass_session) {
error(S_INVALIDPASS);
}
// The column is case insensitive but all passes should be uppercase to avoid ban bypassing exploits.
$pass_user = strtoupper($pass_user);
$admin_salt = file_get_contents('/www/keys/2014_admin.salt');
if (!$admin_salt) {
die('Internal Server Error (s0)');
}
$passq = mysql_global_call("SELECT user_hash, session_id, last_ip, last_used, last_country, status, pending_id, UNIX_TIMESTAMP(expiration_date) as expiration_date FROM pass_users WHERE pin != '' AND user_hash = '%s'", $pass_user);
if( !$passq ) error( S_INVALIDPASS );
$res = mysql_fetch_assoc($passq);
if (!$res || !$res['session_id']) {
clear_pass_cookies();
error(S_INVALIDPASS);
}
$hashed_pass_session = substr(hash('sha256', $res['session_id'] . $admin_salt), 0, 32);
if ($hashed_pass_session !== $pass_session) {
clear_pass_cookies();
error(S_INVALIDPASS);
}
if ((int)$res['expiration_date'] <= $time) {
clear_pass_cookies();
error(sprintf(S_PASSEXPIRED, $res['pending_id']));
}
if ($res['status'] != 0) {
clear_pass_cookies();
error(S_PASSDISABLED);
}
$lastused = strtotime( $res['last_used'] );
$lastip_mask = ip2long( $res['last_ip'] ) & ( ~255 );
$ip_mask = ip2long( $host ) & ( ~255 );
if( $lastip_mask !== 0 && ( $time - $lastused ) < PASS_TIMEOUT && $lastip_mask != $ip_mask ) {
// old strict code, above is to match last octet
//if( ( $time - $lastused ) < PASS_TIMEOUT && $res['last_ip'] != $host && $res['last_ip'] != '0.0.0.0' ) {
clear_pass_cookies();
error( S_PASSINUSE );
}
$update_country = '';
if ($res['last_ip'] !== $host) {
$geo_data = GeoIP2::get_country($host);
if ($geo_data && isset($geo_data['country_code'])) {
$country_code = $geo_data['country_code'];
}
else {
$country_code = 'XX';
}
$update_country = ", last_country = '" . mysql_real_escape_string($country_code) . "'";
}
$passid = $pass_user;
$captcha_bypass = true;
$rangeban_bypass = true;
mysql_global_call( "UPDATE pass_users SET last_used = NOW(), last_ip = '%s' $update_country WHERE user_hash = '%s' AND status = 0 LIMIT 1", $host, $res['user_hash'], $host );
}
return $captcha_bypass;
}
// some code paths might think current admin name is 4chan_auser cookie
// when that's not set (e.g. local requests), assert out here
function validate_admin_cookies()
{
if (!$_COOKIE['4chan_auser']) {
error('Internal error (internal request missing name)');
}
}

594
lib/auth.php Normal file
View file

@ -0,0 +1,594 @@
<?php
/** User authentication / flag stuff */
$auth = array(
'level' => false,
'flags' => false,
'allow' => false,
'deny' => false,
'guest' => true,
);
$levelorder = array(
1 => 'janitor',
10 => 'mod',
20 => 'manager',
50 => 'admin'
);
$levelorderf = array(
'janitor' => 1,
'mod' => 10,
'manager' => 20,
'admin' => 50
);
if (!defined('SQLLOGMOD')) {
define("SQLLOGMOD", "mod_users");
define('PASS_TIMEOUT', 1800);
define('LOGIN_FAIL_HOURLY', 5);
}
function csrf_tag() {
if (isset($_COOKIE['_tkn'])) {
return '<input type="hidden" value="' . htmlspecialchars($_COOKIE['_tkn'], ENT_QUOTES) . '" name="_tkn">';
}
else {
return '';
}
}
function csrf_attr() {
if (isset($_COOKIE['_tkn'])) {
return 'data-tkn="' . htmlspecialchars($_COOKIE['_tkn'], ENT_QUOTES) . '"';
}
else {
return '';
}
}
function auth_encrypt($data) {
$key = file_get_contents('/www/keys/2015_enc.key');
if (!$key) {
return false;
}
$iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC);
$iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);
$encrypted = mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $key, $data, MCRYPT_MODE_CBC, $iv);
if ($encrypted === false) {
return false;
}
return $iv . $encrypted;
}
function auth_decrypt($data) {
$key = file_get_contents('/www/keys/2015_enc.key');
if (!$key) {
return false;
}
$iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC);
$iv_dec = substr($data, 0, $iv_size);
$data = substr($data, $iv_size);
$data = mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $key, $data, MCRYPT_MODE_CBC, $iv_dec);
if ($data === false) {
return false;
}
return rtrim($data, "\0");
}
function verify_one_time_pwd($username, $otp) {
if (!$otp) {
return false;
}
$query = "SELECT auth_secret FROM mod_users WHERE username = '%s' LIMIT 1";
$res = mysql_global_call($query, $username);
if (!$res) {
return false;
}
$enc_secret = mysql_fetch_row($res)[0];
if (!$enc_secret) {
return false;
}
require_once 'lib/GoogleAuthenticator.php';
$ga = new PHPGangsta_GoogleAuthenticator();
$dec_secret = auth_decrypt($enc_secret);
if ($dec_secret === false) {
return false;
}
if ($ga->verifyCode($dec_secret, $otp, 2)) {
return true;
}
return false;
}
/**
* Returns a hash containing implicit levels for the current authed level
* ex: will return array('janitor' => true, 'mod' => true)
* if the current level is 'mod'
*/
function get_level_map($level = null) {
global $auth, $levelorderf;
$map = array();
if (!$level) {
$level = $auth['level'];
}
if (!$level) {
return $map;
}
$level_value = (int)$levelorderf[$level];
foreach ($levelorderf as $k => $v) {
if ($v <= $level_value) {
$map[$k] = true;
}
}
return $map;
}
function has_level( $level = 'mod', $board = false )
{
if( is_local_auth() ) return YES;
global $auth, $levelorder, $levelorderf;
static $ourlevel = -1;
//if( !$board && defined( 'BOARD_DIR' ) ) $board = BOARD_DIR;
//if( !access_board($board) ) return false;
if( $ourlevel < 0 ) $ourlevel = $levelorderf[$auth['level']];
if (!isset($levelorderf[$level])) {
return false;
}
if( $levelorderf[$level] <= $ourlevel ) return true;
return false;
}
function has_flag( $flag, $board = false )
{
if( is_local_auth() ) return YES;
global $auth;
if( $auth['guest'] ) return false;
if( !access_board( $board ) ) return false;
if( in_array( $flag, $auth['flags'] ) ) return true;
return false;
}
function access_board( $board )
{
if( is_local_auth() ) return YES;
global $auth;
if( $auth['guest'] ) return false;
$can_do = false;
// See if we have access to this board or all
if( in_array( 'all', $auth['allow'] ) || in_array( $board, $auth['allow'] ) ) $can_do = true;
// Are we denied on this board?
if( $board && in_array( $board, $auth['deny'] ) ) $can_do = false;
// If we're not using a board, are we denied for no-board stuff?
if( !$board && in_array( 'noboard', $auth['deny'] ) ) $can_do = false;
return $can_do;
}
function is_user()
{
if( is_local_auth() ) return YES;
global $auth;
if( $auth['guest'] ) return false;
if( $auth['level'] ) return true;
return false;
}
function auth_user($skip_agreement = false) {
global $auth;
$user = $_COOKIE['4chan_auser'];
$pass = $_COOKIE['apass'];
if( !$user || !$pass ) return false;
$query = mysql_global_call("SELECT * FROM `%s` WHERE `username` = '%s' LIMIT 1", SQLLOGMOD, $user);
if (!mysql_num_rows($query)) {
return false;
}
$fetch = mysql_fetch_assoc($query);
$admin_salt = file_get_contents('/www/keys/2014_admin.salt');
if (!$admin_salt) {
die('Internal Server Error (s0)');
}
$hashed_admin_password = hash('sha256', $fetch['username'] . $fetch['password'] . $admin_salt);
if ($hashed_admin_password !== $pass) {
return false;
}
if ($fetch['password_expired'] == 1) {
die('Your password has expired; check IRC for instructions on changing it.');
}
if (!$skip_agreement) {
if ($fetch['signed_agreement'] == 0 && basename($_SERVER['SELF_PATH']) !== 'agreement.php' && basename($_SERVER['SELF_PATH']) !== 'agreement_genkey.php') {
die('You must agree to the 4chan Volunteer Moderator Agreement in order to access moderation tools. Please check your e-mail for more information.');
}
}
$auth['level'] = $fetch['level'];
$auth['flags'] = explode( ',', $fetch['flags'] );
$auth['allow'] = explode( ',', $fetch['allow'] );
$auth['deny'] = explode( ',', $fetch['deny'] );
$auth['guest'] = false;
$flags = array();
if( has_level( 'admin' ) ) {
$flags['forcedanonname'] = 2;
}
if( has_level( 'manager' ) || has_flag( 'html' ) ) {
$flags['html'] = 1;
}
$flags = array_flip( $flags );
$flags = implode( ',', $flags );
$ips_array = json_decode($fetch['ips'], true);
if (json_last_error() !== JSON_ERROR_NONE) {
die('Database Error (1-0)');
}
$ips_array[$_SERVER['REMOTE_ADDR']] = $_SERVER['REQUEST_TIME'];
if (count($ips_array) > 512) {
asort($ips_array);
array_shift($ips_array);
}
$ips_array = json_encode($ips_array);
if (json_last_error() !== JSON_ERROR_NONE) {
die('Database Error (1-1)');
}
if (mb_strlen($_SERVER['HTTP_USER_AGENT']) > 128) {
$ua = mb_substr($_SERVER['HTTP_USER_AGENT'], 0, 128);
}
else {
$ua = $_SERVER['HTTP_USER_AGENT'];
}
mysql_global_call("UPDATE `%s` SET ips = '$ips_array', last_ua = '%s' WHERE id = %d LIMIT 1", SQLLOGMOD, $ua, $fetch['id']);
return true;
}
// OLD auth
/*
function auth_user( $login = false )
{
global $auth;
if( $login ) {
$user = $_POST['userlogin'];
$pass = $_POST['passlogin'];
} else {
$user = $_COOKIE['4chan_auser'];
$pass = $_COOKIE['4chan_apass'];
}
if( !$user || !$pass ) return false;
$query = mysql_global_call( "SELECT * FROM `%s` WHERE `username` = '%s' LIMIT 1", SQLLOGMOD, $user );
if( !mysql_num_rows( $query ) ) return false;
$fetch = mysql_fetch_assoc( $query );
if( $fetch['password_expired'] == 1 ) {
die( 'Your password has expired; check IRC for instructions on changing it.' );
}
if ($login) {
if( !password_verify($pass, $fetch['password'])) return false;
$pass = $fetch['password'];
} else {
if ($pass != $fetch['password']) return false;
}
$auth['level'] = $fetch['level'];
$auth['flags'] = explode( ',', $fetch['flags'] );
$auth['allow'] = explode( ',', $fetch['allow'] );
$auth['deny'] = explode( ',', $fetch['deny'] );
$auth['guest'] = false;
$flags = array();
if( has_level( 'admin' ) && $user == 'moot' ) {
$flags['forcedanonname'] = 2;
}
if( has_level( 'manager' ) || has_flag( 'html' ) ) {
$flags['html'] = 1;
}
$flags = array_flip( $flags );
$flags = implode( ',', $flags );
$ips_array = json_decode($fetch['ips'], true);
if (json_last_error() !== JSON_ERROR_NONE) {
die('Database Error (1-0)');
}
$ips_array[$_SERVER['REMOTE_ADDR']] = $_SERVER['REQUEST_TIME'];
$ips_array = json_encode($ips_array);
if (json_last_error() !== JSON_ERROR_NONE) {
die('Database Error (1-1)');
}
if ($login) {
$login_query = ", last_login = now()";
}
else {
if (!isset($_COOKIE['apass'])) {
return false;
}
$admin_salt = file_get_contents('/www/keys/2014_admin.salt');
if (!$admin_salt) {
die('Internal Server Error (s0)');
}
$hashed_admin_cookie = $_COOKIE['apass'];
$hashed_admin_password = hash('sha256', $fetch['username'] . $fetch['password'] . $admin_salt);
if ($hashed_admin_password !== $hashed_admin_cookie) {
return false;
}
$login_query = '';
}
mysql_global_do("UPDATE `%s` SET ips = '$ips_array' $login_query WHERE id = %d", SQLLOGMOD, $fetch['id']);
if( !isset( $_COOKIE['4chan_auser'] ) || !isset( $_COOKIE['4chan_apass'] ) ) {
if( strstr( $_SERVER["HTTP_HOST"], ".4chan.org" ) ) {
setcookie( "4chan_auser", $user, time() + 30 * 24 * 3600, "/", ".4chan.org", true, true );
setcookie( "4chan_apass", $pass, time() + 30 * 24 * 3600, "/", ".4chan.org", true, true );
setcookie( "4chan_aflags", $flags, time() + 30 * 24 * 3600, "/", ".4chan.org", true );
$jspath = $auth['level'] == 'janitor' ? JANITOR_JS_PATH : ADMIN_JS_PATH;
if( !isset( $_COOKIE['extra_path'] ) || !in_array( $_COOKIE['extra_path'], array(JANITOR_JS_PATH, ADMIN_JS_PATH) ) ) {
setcookie( 'extra_path', $jspath, time() + ( 30 * 24 * 3600 ), '/', '.4chan.org' );
}
} elseif( strstr( $_SERVER["HTTP_HOST"], ".4channel.org" ) ) {
setcookie( "4chan_auser", $user, time() + 30 * 24 * 3600, "/", ".4channel.org", true, true );
setcookie( "4chan_apass", $pass, time() + 30 * 24 * 3600, "/", ".4channel.org", true, true );
} else {
die( 'Not 4chan.org' );
}
}
return true;
}
*/
function is_local_auth()
{
if (!isset($_SERVER['REMOTE_ADDR'])) {
return true;
}
// local rpc can do anything
$longip = ip2long( $_SERVER['REMOTE_ADDR'] );
if(
cidrtest( $longip, "10.0.0.0/24" ) ||
cidrtest( $longip, "204.152.204.0/24" ) ||
cidrtest( $longip, "127.0.0.0/24" )
) {
return YES;
}
return false;
}
function can_delete( $resno )
{
if( !has_level( 'janitor' ) ) return false;
if( has_level( 'janitor' ) && access_board( BOARD_DIR ) ) return true;
//if( !access_board(BOARD_DIR) ) return false;
$query = mysql_global_do( "SELECT COUNT(*) from reports WHERE board='%s' AND no=%d AND cat=2", BOARD_DIR, $resno );
$illegal_count = mysql_result( $query, 0, 0 );
mysql_free_result( $query );
return $illegal_count >= 3;
}
function start_auth_captcha($use_alt_captcha = false)
{
if (valid_captcha_bypass() !== true) {
if ($use_alt_captcha) {
start_recaptcha_verify_alt();
}
else {
start_recaptcha_verify();
}
}
}
function clear_pass_cookies() {
setcookie('pass_id', null, 1, '/', 'sys.4chan.org', true, true);
setcookie('pass_id', null, 1, '/', '.4chan.org', true, true);
setcookie('pass_enabled', null, 1, '/', '.4chan.org');
}
function valid_captcha_bypass()
{
global $captcha_bypass, $passid, $rangeban_bypass;
$captcha_bypass = false;
$rangeban_bypass = false;
$passid = '';
if (is_local_auth() || has_level('janitor')) {
$captcha_bypass = true;
$rangeban_bypass = true;
return true;
}
if (CAPTCHA != 1) {
$captcha_bypass = true;
}
$time = $_SERVER['REQUEST_TIME'];
$host = $_SERVER['REMOTE_ADDR'];
// check for 4chan pass
$pass_cookie = isset( $_COOKIE['pass_id'] ) ? $_COOKIE['pass_id'] : '';
if (strlen($pass_cookie) == 10) {
setcookie('pass_id', '0', 1, '/', '.4chan.org', true, true);
setcookie('pass_enabled', '0', 1, '/', '.4chan.org');
error(S_PASSFORMATCHANGED);
}
if ($pass_cookie) {
$pass_parts = explode('.', $pass_cookie);
$pass_user = $pass_parts[0];
$pass_session = $pass_parts[1];
if (!$pass_user || !$pass_session) {
error(S_INVALIDPASS);
}
// The column is case insensitive but all passes should be uppercase to avoid ban bypassing exploits.
$pass_user = strtoupper($pass_user);
$admin_salt = file_get_contents('/www/keys/2014_admin.salt');
if (!$admin_salt) {
die('Internal Server Error (s0)');
}
$passq = mysql_global_call("SELECT user_hash, session_id, last_ip, last_used, last_country, status, pending_id, UNIX_TIMESTAMP(expiration_date) as expiration_date FROM pass_users WHERE pin != '' AND user_hash = '%s'", $pass_user);
if( !$passq ) error( S_INVALIDPASS );
$res = mysql_fetch_assoc($passq);
if (!$res || !$res['session_id']) {
clear_pass_cookies();
error(S_INVALIDPASS);
}
$hashed_pass_session = substr(hash('sha256', $res['session_id'] . $admin_salt), 0, 32);
if ($hashed_pass_session !== $pass_session) {
clear_pass_cookies();
error(S_INVALIDPASS);
}
if ((int)$res['expiration_date'] <= $time) {
clear_pass_cookies();
error(sprintf(S_PASSEXPIRED, $res['pending_id']));
}
if ($res['status'] != 0) {
clear_pass_cookies();
error(S_PASSDISABLED);
}
$lastused = strtotime( $res['last_used'] );
$lastip_mask = ip2long( $res['last_ip'] ) & ( ~255 );
$ip_mask = ip2long( $host ) & ( ~255 );
if( $lastip_mask !== 0 && ( $time - $lastused ) < PASS_TIMEOUT && $lastip_mask != $ip_mask ) {
// old strict code, above is to match last octet
//if( ( $time - $lastused ) < PASS_TIMEOUT && $res['last_ip'] != $host && $res['last_ip'] != '0.0.0.0' ) {
clear_pass_cookies();
error( S_PASSINUSE );
}
$update_country = '';
if ($res['last_ip'] !== $host) {
$geo_data = GeoIP2::get_country($host);
if ($geo_data && isset($geo_data['country_code'])) {
$country_code = $geo_data['country_code'];
}
else {
$country_code = 'XX';
}
$update_country = ", last_country = '" . mysql_real_escape_string($country_code) . "'";
}
$passid = $pass_user;
$captcha_bypass = true;
$rangeban_bypass = true;
mysql_global_call( "UPDATE pass_users SET last_used = NOW(), last_ip = '%s' $update_country WHERE user_hash = '%s' AND status = 0 LIMIT 1", $host, $res['user_hash'], $host );
}
return $captcha_bypass;
}
// some code paths might think current admin name is 4chan_auser cookie
// when that's not set (e.g. local requests), assert out here
function validate_admin_cookies()
{
if (!$_COOKIE['4chan_auser']) {
error('Internal error (internal request missing name)');
}
}

132
lib/board_flags_lgbt.php Normal file
View file

@ -0,0 +1,132 @@
<?php
// Flag code to name mapping
function get_board_flags_array() {
static $board_flags = array(
'AAP' => 'AAP',
'ACE' => 'Asexual',
'ACH' => 'Achillean',
'AFB' => 'AFAB',
'AGP' => 'AGP',
'AGR' => 'Agender',
'ALL' => 'LGBT',
'ALY' => 'Ally',
'AMB' => 'AMAB',
'AND' => 'Androgynous',
'ARO' => 'Aromantic',
'BCH' => 'Butch',
'BI' => 'Bisexual',
'BOY' => 'Boymoder',
'BR' => 'Bear',
'CHR' => 'Chaser',
'CIS' => 'Cis',
'DOM' => 'Dom',
'DRO' => 'Demiromantic',
'DSX' => 'Demisexual',
'FBY' => 'Femboy',
'FFB' => 'FtM Femboy',
'FR' => 'FtM Repressor',
'GAY' => 'Gay',
'GFL' => 'Genderfluid',
'GQR' => 'Genderqueer',
'HFB' => 'HRT Femboy',
'HON' => 'Hon',
'HST' => 'HSTS',
'INT' => 'Intersex',
'LAB' => 'Labrys',
'LES' => 'Lesbian',
'MBT' => 'MtF Butch',
'MR' => 'MtF Repressor',
'NB' => 'Nonbinary',
'OG' => 'Original',
'PAN' => 'Pansexual',
'PBI' => 'Prison Bi',
'PG' => 'Prison Gay',
'PLY' => 'Poly',
'PNR' => 'Pooner',
'PRG' => 'Progress',
'QES' => 'Questioning',
'QR' => 'Queer',
'REP' => 'Repressor',
'SPH' => 'Sapphic',
'STR' => 'Straight',
'SUB' => 'Sub',
'SW' => 'Switch',
'TF' => 'Transfem',
'TKH' => 'Twinkhon',
'TMA' => 'Transmasc',
'TNK' => 'Twink',
'TRN' => 'Transgender',
'UKR' => 'Woke'
);
return $board_flags;
}
// Flag names as they appear in the selection menu
function get_board_flags_selector() {
static $board_flags = array(
'AAP' => 'AAP',
'ACE' => 'Asexual',
'ACH' => 'Achillean',
'AFB' => 'AFAB',
'AGP' => 'AGP',
'AGR' => 'Agender',
'ALL' => 'LGBT',
'ALY' => 'Ally',
'AMB' => 'AMAB',
'AND' => 'Androgynous',
'ARO' => 'Aromantic',
'BCH' => 'Butch',
'BI' => 'Bisexual',
'BOY' => 'Boymoder',
'BR' => 'Bear',
'CHR' => 'Chaser',
'CIS' => 'Cis',
'DOM' => 'Dom',
'DRO' => 'Demiromantic',
'DSX' => 'Demisexual',
'FBY' => 'Femboy',
'FFB' => 'FtM Femboy',
'FR' => 'FtM Repressor',
'GAY' => 'Gay',
'GFL' => 'Genderfluid',
'GQR' => 'Genderqueer',
'HFB' => 'HRT Femboy',
'HON' => 'Hon',
'HST' => 'HSTS',
'INT' => 'Intersex',
'LAB' => 'Labrys',
'LES' => 'Lesbian',
'MBT' => 'MtF Butch',
'MR' => 'MtF Repressor',
'NB' => 'Nonbinary',
'OG' => 'Original',
'PAN' => 'Pansexual',
'PBI' => 'Prison Bi',
'PG' => 'Prison Gay',
'PLY' => 'Poly',
'PNR' => 'Pooner',
'PRG' => 'Progress',
'QES' => 'Questioning',
'QR' => 'Queer',
'REP' => 'Repressor',
'SPH' => 'Sapphic',
'STR' => 'Straight',
'SUB' => 'Sub',
'SW' => 'Switch',
'TF' => 'Transfem',
'TKH' => 'Twinkhon',
'TMA' => 'Transmasc',
'TNK' => 'Twink',
'TRN' => 'Transgender',
'UKR' => 'Woke'
);
return $board_flags;
}
function board_flag_code_to_name($code) {
$board_flags = get_board_flags_array();
return isset($board_flags[$code]) ? $board_flags[$code] : 'None';
}

102
lib/board_flags_mlp.php Normal file
View file

@ -0,0 +1,102 @@
<?php
// Flag code to name mapping
function get_board_flags_array() {
static $board_flags = array(
'4CC' => '4cc /mlp/',
'ADA' => 'Adagio Dazzle',
'AN' => 'Anon',
'ANF' => 'Anonfilly',
'APB' => 'Apple Bloom',
'AJ' => 'Applejack',
'AB' => 'Aria Blaze',
'AU' => 'Autumn Blaze',
'BB' => 'Bon Bon',
'BM' => 'Big Mac',
'BP' => 'Berry Punch',
'BS' => 'Babs Seed',
'CL' => 'Changeling',
'CO' => 'Coco Pommel',
'CG' => 'Cozy Glow',
'CHE' => 'Cheerilee',
'CB' => 'Cherry Berry',
'DAY' => 'Daybreaker',
'DD' => 'Daring Do',
'DER' => 'Derpy Hooves',
'DT' => 'Diamond Tiara',
'DIS' => 'Discord',
'EQA' => 'EqG Applejack',
'EQF' => 'EqG Fluttershy',
'EQP' => 'EqG Pinkie Pie',
'EQR' => 'EqG Rainbow Dash',
'EQT' => 'EqG Trixie',
'EQI' => 'EqG Twilight Sparkle',
'EQS' => 'EqG Sunset Shimmer',
'ERA' => 'EqG Rarity',
'FAU' => 'Fausticorn',
'FLE' => 'Fleur de lis',
'FL' => 'Fluttershy',
'GI' => 'Gilda',
'HT' => 'Hitch Trailblazer',
'IZ' => 'Izzy Moonbow',
'LI' => 'Limestone',
'LT' => 'Lord Tirek',
'LY' => 'Lyra Heartstrings',
'MA' => 'Marble',
'MAU' => 'Maud',
'MIN' => 'Minuette',
'NI' => 'Nightmare Moon',
'NUR' => 'Nurse Redheart',
'OCT' => 'Octavia',
'PAR' => 'Parasprite',
'PC' => 'Princess Cadance',
'PCE' => 'Princess Celestia',
'PI' => 'Pinkie Pie',
'PLU' => 'Princess Luna',
'PM' => 'Pinkamena',
'PP' => 'Pipp Petals',
'QC' => 'Queen Chrysalis',
'RAR' => 'Rarity',
'RD' => 'Rainbow Dash',
'RLU' => 'Roseluck',
'S1L' => 'S1 Luna',
'SCO' => 'Scootaloo',
'SHI' => 'Shining Armor',
'SIL' => 'Silver Spoon',
'SON' => 'Sonata Dusk',
'SP' => 'Spike',
'SPI' => 'Spitfire',
'SS' => 'Sunny Starscout',
'STA' => 'Star Dancer',
'STL' => 'Starlight Glimmer',
'SPT' => 'Sprout',
'SUN' => 'Sunburst',
'SUS' => 'Sunset Shimmer',
'SWB' => 'Sweetie Belle',
'TFA' => 'TFH Arizona',
'TFO' => 'TFH Oleander',
'TFP' => 'TFH Paprika',
'TFS' => 'TFH Shanty',
'TFT' => 'TFH Tianhuo',
'TFV' => 'TFH Velvet',
'TP' => 'TFH Pom',
'TS' => 'Tempest Shadow',
'TWI' => 'Twilight Sparkle',
'TX' => 'Trixie',
'VS' => 'Vinyl Scratch',
'ZE' => 'Zecora',
'ZS' => 'Zipp Storm'
);
return $board_flags;
}
// Flag names as they appear in the selection menu
function get_board_flags_selector() {
return get_board_flags_array();
}
function board_flag_code_to_name($code) {
$board_flags = get_board_flags_array();
return isset($board_flags[$code]) ? $board_flags[$code] : 'None';
}

72
lib/board_flags_pol.php Normal file
View file

@ -0,0 +1,72 @@
<?php
// Flag code to name mapping
function get_board_flags_array() {
static $board_flags = array(
'AC' => 'Anarcho-Capitalist',
'AN' => 'Anarchist',
'BL' => 'Black Lives Matter',
'CF' => 'Confederate',
'CM' => 'Commie',
'CT' => 'Catalonia',
'DM' => 'Democrat',
'EU' => 'European',
'FC' => 'Fascist',
'GN' => 'Gadsden',
'GY' => 'LGBT',
'JH' => 'Jihadi',
'KN' => 'Kekistani',
'MF' => 'Muslim',
'NB' => 'National Bolshevik',
'NT' => 'NATO',
'NZ' => 'Nazi',
'PC' => 'Hippie',
'PR' => 'Pirate',
'RE' => 'Republican',
'TM' => 'DEUS VULT',
'MZ' => 'Task Force Z',
'TR' => 'Tree Hugger',
'UN' => 'United Nations',
'WP' => 'White Supremacist'
);
return $board_flags;
}
// Flag names as they appear in the selection menu
function get_board_flags_selector() {
static $board_flags = array(
'AC' => 'Anarcho-Capitalist',
'AN' => 'Anarchist',
'BL' => 'Black Nationalist',
'CF' => 'Confederate',
'CM' => 'Communist',
'CT' => 'Catalonia',
'DM' => 'Democrat',
'EU' => 'European',
'FC' => 'Fascist',
'GN' => 'Gadsden',
'GY' => 'Gay',
'JH' => 'Jihadi',
'KN' => 'Kekistani',
'MF' => 'Muslim',
'NB' => 'National Bolshevik',
'NT' => 'NATO',
'NZ' => 'Nazi',
'PC' => 'Hippie',
'PR' => 'Pirate',
'RE' => 'Republican',
'MZ' => 'Task Force Z',
'TM' => 'Templar',
'TR' => 'Tree Hugger',
'UN' => 'United Nations',
'WP' => 'White Supremacist'
);
return $board_flags;
}
function board_flag_code_to_name($code) {
$board_flags = get_board_flags_array();
return isset($board_flags[$code]) ? $board_flags[$code] : 'None';
}

26
lib/board_flags_test.php Normal file
View file

@ -0,0 +1,26 @@
<?php
// Flag code to name mapping
function get_board_flags_array() {
static $board_flags = array(
'FL1' => 'Flag 1',
'FL2' => 'Flag 2'
);
return $board_flags;
}
// Flag names as they appear in the selection menu
function get_board_flags_selector() {
static $board_flags = array(
'FL1' => 'Flag 1',
'FL2' => 'Flag 2'
);
return $board_flags;
}
function board_flag_code_to_name($code) {
$board_flags = get_board_flags_array();
return isset($board_flags[$code]) ? $board_flags[$code] : 'None';
}

686
lib/captcha-test.php Normal file
View file

@ -0,0 +1,686 @@
<?
require_once "lib/rpc.php";
require_once 'lib/ini.php';
load_ini_file('captcha_config.ini');
$recaptcha_public_key = RECAPTCHA_API_KEY_PUBLIC;
$recaptcha_private_key = RECAPTCHA_API_KEY_PRIVATE;
$hcaptcha_public_key = HCAPTCHA_API_KEY_PUBLIC;
$hcaptcha_private_key = HCAPTCHA_API_KEY_PRIVATE;
// Parameter formats and other checks must much the formatting in /captcha
function is_twister_captcha_valid($memcached, $ip, $userpwd, $board = '!', $thread_id = 0, &$unsolved_count = null) {
if (!$memcached) {
return false;
}
if (defined('TEST_BOARD') && TEST_BOARD) {
require_once 'lib/twister_captcha-test.php';
}
else {
require_once 'lib/twister_captcha.php';
}
if (!isset($_POST['t-challenge']) || !$_POST['t-challenge']) {
return false;
}
if (!isset($_POST['t-response']) || !$_POST['t-response']) {
return false;
}
if (strlen($_POST['t-response']) > 24) {
return false;
}
if (strlen($_POST['t-challenge']) > 255) {
return false;
}
// User agent
if (!isset($_SERVER['HTTP_USER_AGENT'])) {
$user_agent = '!';
}
else {
$user_agent = md5($_SERVER['HTTP_USER_AGENT']);
}
// Password
$password = '!';
if ($userpwd && !$userpwd->isNew()) {
$password = $userpwd->getPwd();
}
// ---
list($uniq_id, $challenge_hash) = explode('.', $_POST['t-challenge']);
$long_ip = ip2long($ip);
if (!$uniq_id || !$challenge_hash || !$long_ip) {
return false;
}
$challenge_key = "ch$long_ip";
$params = [$ip, $password, $user_agent, $board, $thread_id];
$response = TwisterCaptcha::normalizeReponseStr($_POST['t-response']);
$is_valid = TwisterCaptcha::verifyChallengeHash($challenge_hash, $uniq_id, $response, $params);
if ($is_valid) {
$active_uniq_id = $memcached->get($challenge_key);
// Delete challenge
$memcached->delete($challenge_key);
if (!$active_uniq_id || $uniq_id !== $active_uniq_id) {
return false;
}
// Return and decrement the unsolved session count
$us = decrement_twister_captcha_session($memcached, $ip, $unsolved_count !== null);
if ($unsolved_count !== null) {
$unsolved_count = $us;
}
return true;
}
// Delete challenge
$memcached->delete($challenge_key);
return false;
}
// Decrements the unsolved count by 2 and returns the old count
function decrement_twister_captcha_session($memcached, $ip, $return_old = true) {
if (!$memcached) {
return false;
}
$long_ip = ip2long($ip);
if (!$long_ip) {
return false;
}
$key = "us$long_ip";
if ($return_old) {
$val = $memcached->get($key);
if ($val === false) {
$val = 0;
}
}
else {
$val = 0;
}
$memcached->decrement($key, 2);
return $val;
}
// FIXME: The IP arg isn't used for now
function set_twister_captcha_credits($memcached, $ip, $userpwd, $current_time) {
if (!$memcached || !$userpwd) {
return false;
}
$current_time = (int)$current_time;
if ($current_time <= 0) {
return false;
}
//$long_ip = ip2long($ip);
//if (!$long_ip) {
//return false;
//}
$credits = 0;
// Config
// Stage 1 should match the check in use_twister_captcha_credit()
// and captcha.php for optimisation purposes
// Stage 1
$noop_known_ttl_1 = 4320; // required user lifetime (3 days, in minutes)
$noop_post_count_1 = 5; // required post count
$noop_credits_1 = 1; // credits given
$noop_duration_1 = 3600; // duration of the credits (1 hour, in seconds)
// Stage 2
$noop_known_ttl_2 = 21600; // 15 days
$noop_post_count_2 = 20;
$noop_credits_2 = 2;
$noop_duration_2 = 7200; // 2 hours
// Stage 3
$noop_known_ttl_3 = 129600; // 90 days
$noop_post_count_3 = 100;
$noop_credits_3 = 3;
$noop_duration_3 = 10800; // 3 hours
// ---
// The IP changed too recently
if ($userpwd->ipLifetime() < 60) {
return false;
}
// Stage 3
if ($userpwd->isUserKnown($noop_known_ttl_3) && $userpwd->postCount() >= $noop_post_count_3) {
$credits = $noop_credits_3;
$duration = $noop_duration_3;
}
// Stage 2
else if ($userpwd->isUserKnown($noop_known_ttl_2) && $userpwd->postCount() >= $noop_post_count_2) {
$credits = $noop_credits_2;
$duration = $noop_duration_2;
}
// Stage 1
else if ($userpwd->isUserKnown($noop_known_ttl_1) && $userpwd->postCount() >= $noop_post_count_1) {
$credits = $noop_credits_1;
$duration = $noop_duration_1;
}
else {
return false;
}
if (!$credits || $credits > 3) {
return false;
}
$expiration_ts = $current_time + $duration;
// Require no more than 5 actions in the past 8 minutes
/*
$query = <<<SQL
SELECT SQL_NO_CACHE COUNT(*) FROM user_actions
WHERE ip = $long_ip
AND time >= DATE_SUB(NOW(), INTERVAL 8 MINUTE)
SQL;
$res = mysql_global_call($query);
if (!$res) {
return false;
}
$count = (int)mysql_fetch_row($res)[0];
if ($count > 5) {
return false;
}
*/
// Set credits
$pwd = $userpwd->getPwd();
if (!$pwd) {
return false;
}
$key = "cr-$pwd";
$val = "$credits.$expiration_ts";
$res = $memcached->replace($key, $val, $expiration_ts);
if ($res === false) {
if ($memcached->getResultCode() === Memcached::RES_NOTSTORED) {
return $memcached->set($key, $val, $expiration_ts);
}
else {
return false;
}
}
return true;
}
// FIXME: The IP arg isn't used for now
function use_twister_captcha_credit($memcached, $ip, $userpwd) {
if (!$memcached || !$userpwd) {
return false;
}
//$long_ip = ip2long($ip);
//if (!$long_ip) {
//return false;
//}
// Must match the check in set_twister_captcha_credits()
$noop_known_ttl_1 = 4320; // required user lifetime (3 days, in minutes)
$noop_post_count_1 = 5; // required post count
if (!$userpwd->isUserKnown($noop_known_ttl_1) || $userpwd->postCount() < $noop_post_count_1) {
return false;
}
$pwd = $userpwd->getPwd();
if (!$pwd) {
return false;
}
$key = "cr-$pwd";
$credits = $memcached->get($key);
if ($credits === false) {
return false;
}
list($count, $ts) = explode('.', $credits);
$count = (int)$count;
$ts = (int)$ts;
// No credits left
if ($count <= 0 || $ts <= 0) {
$memcached->delete($key);
return false;
}
$count -= 1;
$res = $memcached->replace($key, "$count.$ts", $ts);
if ($res === false && $memcached->getResultCode() !== Memcached::RES_NOTSTORED) {
return false;
}
return true;
}
function twister_captcha_form() {
return '<div id="t-root"></div>';
}
function log_failed_captcha($ip, $userpwd, $board, $thread_id, $is_quiet, $meta = null) {
$data = [
'board' => $board,
'thread_id' => $thread_id,
];
if ($userpwd) {
$data['arg_num'] = $userpwd->pwdLifetime();
$data['pwd'] = $userpwd->getPwd();
}
if ($meta) {
$data['meta'] = $meta;
}
if ($is_quiet) {
$type = 'failed_captcha_quiet';
}
else {
$type = 'failed_captcha';
}
write_to_event_log($type, $ip, $data);
}
function h_captcha_form($autoload = false, $cb = 'onRecaptchaLoaded', $dark = false) {
global $hcaptcha_public_key;
$js_tag = '<script src="https://hcaptcha.com/1/api.js'
. (!$autoload ? "?onload=$cb&amp;render=explicit" : '') . '" async defer></script>';
if ($autoload) {
$attrs = ' class="h-captcha" data-sitekey="' . $hcaptcha_public_key . '"';
if ($dark) {
$attrs .= ' data-theme="dark"';
}
}
else {
$attrs = '';
}
$container_tag = '<script>window.hcaptchaKey = "' . $hcaptcha_public_key
. '";</script><div id="g-recaptcha"'
. $attrs . '></div>';
return $js_tag.$container_tag;
}
// Moves css out of the form for html validation
function captcha_form($autoload = false, $cb = 'onRecaptchaLoaded', $dark = false) {
global $recaptcha_public_key;
$js_tag = '<script src="https://www.google.com/recaptcha/api.js'
. (!$autoload ? "?onload=$cb&amp;render=explicit" : '') . '" async defer></script>';
if ($autoload) {
$attrs = ' class="g-recaptcha" data-sitekey="' . $recaptcha_public_key . '"';
if ($dark) {
$attrs .= ' data-theme="dark"';
}
}
else {
$attrs = '';
}
$container_tag = '<script>window.recaptchaKey = "' . $recaptcha_public_key
. '";</script><div id="g-recaptcha"'
. $attrs . '></div>';
$noscript_tag =<<<HTML
<noscript>
<div style="width: 302px;">
<div style="width: 302px; position: relative;">
<div style="width: 302px; height: 422px;">
<iframe src="https://www.google.com/recaptcha/api/fallback?k=$recaptcha_public_key" frameborder="0" scrolling="no" style="width: 302px; height:422px; border-style: none;"></iframe>
</div>
<div style="width: 300px; height: 60px; border-style: none;bottom: 12px; left: 25px; margin: 0px; padding: 0px; right: 25px;background: #f9f9f9; border: 1px solid #c1c1c1; border-radius: 3px;">
<textarea id="g-recaptcha-response" name="g-recaptcha-response" class="g-recaptcha-response" style="width: 250px; height: 40px; border: 1px solid #c1c1c1;margin: 10px 25px; padding: 0px; resize: none;"></textarea>
</div>
</div>
</div>
</noscript>
HTML;
if (defined('NOSCRIPT_CAPTCHA_ONLY') && NOSCRIPT_CAPTCHA_ONLY == 1) {
return $container_tag.$noscript_tag;
}
return $js_tag.$container_tag.$noscript_tag;
}
// Legacy captcha
// Uses recaptcha v2 for noscript captcha as the v1 seems to be broken currently.
function captcha_form_alt() {
global $recaptcha_public_key;
$html = <<<HTML
<div id="captchaContainerAlt"></div>
<script>
function onAltCaptchaClick() {
Recaptcha.reload('t');
}
function onAltCaptchaReady() {
var el;
if (el = document.getElementById('recaptcha_image')) {
el.title = 'Reload';
el.addEventListener('click', onAltCaptchaClick, false);
}
}
if (!window.passEnabled) {
var el = document.createElement('script');
el.type = 'text/javascript';
el.src = '//www.google.com/recaptcha/api/js/recaptcha_ajax.js';
el.onload = function() {
Recaptcha.create('$recaptcha_public_key',
'captchaContainerAlt',
{
theme: 'clean',
tabindex: 3,
callback: onAltCaptchaReady
}
);
}
document.head.appendChild(el);
}</script>
<noscript>
<div style="width: 302px;">
<div style="width: 302px; position: relative;">
<div style="width: 302px; height: 422px;">
<iframe src="https://www.google.com/recaptcha/api/fallback?k=$recaptcha_public_key" frameborder="0" scrolling="no" style="width: 302px; height:422px; border-style: none;"></iframe>
</div>
<div style="width: 300px; height: 60px; border-style: none;bottom: 12px; left: 25px; margin: 0px; padding: 0px; right: 25px;background: #f9f9f9; border: 1px solid #c1c1c1; border-radius: 3px;">
<textarea id="g-recaptcha-response" name="g-recaptcha-response" class="g-recaptcha-response" style="width: 250px; height: 40px; border: 1px solid #c1c1c1;margin: 10px 25px; padding: 0px; resize: none;"></textarea>
</div>
</div>
</div>
</noscript>
HTML;
return $html;
}
function recaptcha_ban($n, $time, $return_error = 0, $length = 1)
{
auto_ban_poster($name, $length, 1, "failed verification $n times per $time", "Possible spambot; repeatedly sent incorrect CAPTCHA verification.");
if( $return_error == 1 ) {
return S_GENERICERROR;
}
error(S_GENERICERROR);
}
/**
* Works for both recaptcha and hcaptcha
*/
function recaptcha_bad_captcha($return_error = false, $codes = null) {
$error = S_BADCAPTCHA;
if (is_array($codes)) {
if (in_array('missing-input-response', $codes)) {
$error = S_NOCAPTCHA;
}
if ($return_error) {
return $error;
}
else {
error($error);
}
}
else {
if ($return_error) {
return $error;
}
else {
error($error);
}
}
}
// -----------
// hCaptcha
// -----------
function start_hcaptcha_verify($return_error = false) {
global $hcaptcha_private_key, $hcaptcha_ch;
$response = $_POST["h-captcha-response"];
if (!$response) {
if ($return_error == false) {
error(S_NOCAPTCHA);
}
return S_NOCAPTCHA;
}
$response = urlencode($response);
$rlen = strlen($response);
if ($rlen > 32768) {
return recaptcha_bad_captcha($return_error);
}
$api_url = 'https://hcaptcha.com/siteverify';
$post = array(
'secret' => $hcaptcha_private_key,
'response' => $response
);
$hcaptcha_ch = rpc_start_captcha_request($api_url, $post, null, false);
}
function end_hcaptcha_verify($return_error = false) {
global $hcaptcha_ch;
if (!$hcaptcha_ch) {
return;
}
$ret = rpc_finish_request($hcaptcha_ch, $error, $httperror);
// BAD
// 413 Request Too Large is bad; it was caused intentionally by the user.
if ($httperror == 413) {
return recaptcha_bad_captcha($return_error);
}
// BAD
if ($ret == null) {
return recaptcha_bad_captcha($return_error);
}
$resp = json_decode($ret, true);
// BAD
// Malformed JSON response from Google
if (json_last_error() !== JSON_ERROR_NONE) {
return recaptcha_bad_captcha($return_error);
}
// GOOD
if ($resp['success']) {
return $resp;
}
// BAD
return recaptcha_bad_captcha($return_error, $resp['error-codes']);
}
// -----------
// reCaptcha V2
// -----------
// FIXME $challenge_field is no longer used
function start_recaptcha_verify($return_error = false, $challenge_field = '') {
global $recaptcha_private_key, $recaptcha_ch;
$response = $_POST["g-recaptcha-response"];
if (!$response) {
if ($return_error == false) {
error(S_NOCAPTCHA);
}
return S_NOCAPTCHA;
}
$response = urlencode($response);
$rlen = strlen($response);
if ($rlen > 4096) {
return recaptcha_bad_captcha($return_error);
}
$api_url = 'https://www.google.com/recaptcha/api/siteverify';
$post = array(
'secret' => $recaptcha_private_key,
'response' => $response
);
$recaptcha_ch = rpc_start_captcha_request($api_url, $post, null, false);
}
function end_recaptcha_verify($return_error = false) {
global $recaptcha_ch;
if (!$recaptcha_ch) {
return;
}
$ret = rpc_finish_request($recaptcha_ch, $error, $httperror);
// BAD
// 413 Request Too Large is bad; it was caused intentionally by the user.
if ($httperror == 413) {
return recaptcha_bad_captcha($return_error);
}
// BAD
if ($ret == null) {
return recaptcha_bad_captcha($return_error);
}
$resp = json_decode($ret, true);
// BAD
// Malformed JSON response from Google
if (json_last_error() !== JSON_ERROR_NONE) {
return recaptcha_bad_captcha($return_error);
}
// GOOD
if ($resp['success']) {
return $resp;
}
// BAD
return recaptcha_bad_captcha($return_error, $resp['error-codes']);
}
// -----------
// reCaptcha V1
// -----------
function start_recaptcha_verify_alt($return_error = false, $challenge_field = '') {
global $recaptcha_private_key, $recaptcha_ch;
$challenge = ( $challenge_field == '' ) ? $_POST["recaptcha_challenge_field"] : $challenge_field;
$response = $_POST["recaptcha_response_field"];
if (!$challenge || !$response) {
if( $return_error == false ) {
error(S_NOCAPTCHA);
}
return S_NOCAPTCHA;
}
$num_words = 1 + preg_match_all('/\\s/', $response);
$rlen = strlen($response);
if ($num_words > 3 || $rlen > 128) {
return recaptcha_bad_captcha($return_error);
}
$post = array(
"privatekey" => $recaptcha_private_key,
"challenge" => $challenge,
"remoteip" => $_SERVER["REMOTE_ADDR"],
"response" => $response
);
$recaptcha_ch = rpc_start_request("https://www.google.com/recaptcha/api/verify", $post, null, false);
}
function end_recaptcha_verify_alt($return_error = false) {
global $recaptcha_ch;
if (!$recaptcha_ch) return;
$ret = rpc_finish_request($recaptcha_ch, $error, $httperror);
if ($httperror == 413) {
return recaptcha_bad_captcha($return_error);
}
if ($ret) {
$lines = explode("\n", $ret);
if ($lines[0] === "true") {
// GOOD
return;
}
}
// BAD
return recaptcha_bad_captcha($return_error);
}
?>

686
lib/captcha.php Normal file
View file

@ -0,0 +1,686 @@
<?
require_once "lib/rpc.php";
require_once 'lib/ini.php';
load_ini_file('captcha_config.ini');
$recaptcha_public_key = RECAPTCHA_API_KEY_PUBLIC;
$recaptcha_private_key = RECAPTCHA_API_KEY_PRIVATE;
$hcaptcha_public_key = HCAPTCHA_API_KEY_PUBLIC;
$hcaptcha_private_key = HCAPTCHA_API_KEY_PRIVATE;
// Parameter formats and other checks must much the formatting in /captcha
function is_twister_captcha_valid($memcached, $ip, $userpwd, $board = '!', $thread_id = 0, &$unsolved_count = null) {
if (!$memcached) {
return false;
}
if (defined('TEST_BOARD') && TEST_BOARD) {
require_once 'lib/twister_captcha-test.php';
}
else {
require_once 'lib/twister_captcha.php';
}
if (!isset($_POST['t-challenge']) || !$_POST['t-challenge']) {
return false;
}
if (!isset($_POST['t-response']) || !$_POST['t-response']) {
return false;
}
if (strlen($_POST['t-response']) > 24) {
return false;
}
if (strlen($_POST['t-challenge']) > 255) {
return false;
}
// User agent
if (!isset($_SERVER['HTTP_USER_AGENT'])) {
$user_agent = '!';
}
else {
$user_agent = md5($_SERVER['HTTP_USER_AGENT']);
}
// Password
$password = '!';
if ($userpwd && !$userpwd->isNew()) {
$password = $userpwd->getPwd();
}
// ---
list($uniq_id, $challenge_hash) = explode('.', $_POST['t-challenge']);
$long_ip = ip2long($ip);
if (!$uniq_id || !$challenge_hash || !$long_ip) {
return false;
}
$challenge_key = "ch$long_ip";
$params = [$ip, $password, $user_agent, $board, $thread_id];
$response = TwisterCaptcha::normalizeReponseStr($_POST['t-response']);
$is_valid = TwisterCaptcha::verifyChallengeHash($challenge_hash, $uniq_id, $response, $params);
if ($is_valid) {
$active_uniq_id = $memcached->get($challenge_key);
// Delete challenge
$memcached->delete($challenge_key);
if (!$active_uniq_id || $uniq_id !== $active_uniq_id) {
return false;
}
// Return and decrement the unsolved session count
$us = decrement_twister_captcha_session($memcached, $ip, $unsolved_count !== null);
if ($unsolved_count !== null) {
$unsolved_count = $us;
}
return true;
}
// Delete challenge
$memcached->delete($challenge_key);
return false;
}
// Decrements the unsolved count by 2 and returns the old count
function decrement_twister_captcha_session($memcached, $ip, $return_old = true) {
if (!$memcached) {
return false;
}
$long_ip = ip2long($ip);
if (!$long_ip) {
return false;
}
$key = "us$long_ip";
if ($return_old) {
$val = $memcached->get($key);
if ($val === false) {
$val = 0;
}
}
else {
$val = 0;
}
$memcached->decrement($key, 2);
return $val;
}
// FIXME: The IP arg isn't used for now
function set_twister_captcha_credits($memcached, $ip, $userpwd, $current_time) {
if (!$memcached || !$userpwd) {
return false;
}
$current_time = (int)$current_time;
if ($current_time <= 0) {
return false;
}
//$long_ip = ip2long($ip);
//if (!$long_ip) {
//return false;
//}
$credits = 0;
// Config
// Stage 1 should match the check in use_twister_captcha_credit()
// and captcha.php for optimisation purposes
// Stage 1
$noop_known_ttl_1 = 4320; // required user lifetime (3 days, in minutes)
$noop_post_count_1 = 5; // required post count
$noop_credits_1 = 1; // credits given
$noop_duration_1 = 3600; // duration of the credits (1 hour, in seconds)
// Stage 2
$noop_known_ttl_2 = 21600; // 15 days
$noop_post_count_2 = 20;
$noop_credits_2 = 2;
$noop_duration_2 = 7200; // 2 hours
// Stage 3
$noop_known_ttl_3 = 129600; // 90 days
$noop_post_count_3 = 100;
$noop_credits_3 = 3;
$noop_duration_3 = 10800; // 3 hours
// ---
// The IP changed too recently
if ($userpwd->ipLifetime() < 60) {
return false;
}
// Stage 3
if ($userpwd->isUserKnown($noop_known_ttl_3) && $userpwd->postCount() >= $noop_post_count_3) {
$credits = $noop_credits_3;
$duration = $noop_duration_3;
}
// Stage 2
else if ($userpwd->isUserKnown($noop_known_ttl_2) && $userpwd->postCount() >= $noop_post_count_2) {
$credits = $noop_credits_2;
$duration = $noop_duration_2;
}
// Stage 1
else if ($userpwd->isUserKnown($noop_known_ttl_1) && $userpwd->postCount() >= $noop_post_count_1) {
$credits = $noop_credits_1;
$duration = $noop_duration_1;
}
else {
return false;
}
if (!$credits || $credits > 3) {
return false;
}
$expiration_ts = $current_time + $duration;
// Require no more than 5 actions in the past 8 minutes
/*
$query = <<<SQL
SELECT SQL_NO_CACHE COUNT(*) FROM user_actions
WHERE ip = $long_ip
AND time >= DATE_SUB(NOW(), INTERVAL 8 MINUTE)
SQL;
$res = mysql_global_call($query);
if (!$res) {
return false;
}
$count = (int)mysql_fetch_row($res)[0];
if ($count > 5) {
return false;
}
*/
// Set credits
$pwd = $userpwd->getPwd();
if (!$pwd) {
return false;
}
$key = "cr-$pwd";
$val = "$credits.$expiration_ts";
$res = $memcached->replace($key, $val, $expiration_ts);
if ($res === false) {
if ($memcached->getResultCode() === Memcached::RES_NOTSTORED) {
return $memcached->set($key, $val, $expiration_ts);
}
else {
return false;
}
}
return true;
}
// FIXME: The IP arg isn't used for now
function use_twister_captcha_credit($memcached, $ip, $userpwd) {
if (!$memcached || !$userpwd) {
return false;
}
//$long_ip = ip2long($ip);
//if (!$long_ip) {
//return false;
//}
// Must match the check in set_twister_captcha_credits()
$noop_known_ttl_1 = 4320; // required user lifetime (3 days, in minutes)
$noop_post_count_1 = 5; // required post count
if (!$userpwd->isUserKnown($noop_known_ttl_1) || $userpwd->postCount() < $noop_post_count_1) {
return false;
}
$pwd = $userpwd->getPwd();
if (!$pwd) {
return false;
}
$key = "cr-$pwd";
$credits = $memcached->get($key);
if ($credits === false) {
return false;
}
list($count, $ts) = explode('.', $credits);
$count = (int)$count;
$ts = (int)$ts;
// No credits left
if ($count <= 0 || $ts <= 0) {
$memcached->delete($key);
return false;
}
$count -= 1;
$res = $memcached->replace($key, "$count.$ts", $ts);
if ($res === false && $memcached->getResultCode() !== Memcached::RES_NOTSTORED) {
return false;
}
return true;
}
function twister_captcha_form() {
return '<div id="t-root"></div>';
}
function log_failed_captcha($ip, $userpwd, $board, $thread_id, $is_quiet, $meta = null) {
$data = [
'board' => $board,
'thread_id' => $thread_id,
];
if ($userpwd) {
$data['arg_num'] = $userpwd->pwdLifetime();
$data['pwd'] = $userpwd->getPwd();
}
if ($meta) {
$data['meta'] = $meta;
}
if ($is_quiet) {
$type = 'failed_captcha_quiet';
}
else {
$type = 'failed_captcha';
}
write_to_event_log($type, $ip, $data);
}
function h_captcha_form($autoload = false, $cb = 'onRecaptchaLoaded', $dark = false) {
global $hcaptcha_public_key;
$js_tag = '<script src="https://hcaptcha.com/1/api.js'
. (!$autoload ? "?onload=$cb&amp;render=explicit" : '') . '" async defer></script>';
if ($autoload) {
$attrs = ' class="h-captcha" data-sitekey="' . $hcaptcha_public_key . '"';
if ($dark) {
$attrs .= ' data-theme="dark"';
}
}
else {
$attrs = '';
}
$container_tag = '<script>window.hcaptchaKey = "' . $hcaptcha_public_key
. '";</script><div id="g-recaptcha"'
. $attrs . '></div>';
return $js_tag.$container_tag;
}
// Moves css out of the form for html validation
function captcha_form($autoload = false, $cb = 'onRecaptchaLoaded', $dark = false) {
global $recaptcha_public_key;
$js_tag = '<script src="https://www.google.com/recaptcha/api.js'
. (!$autoload ? "?onload=$cb&amp;render=explicit" : '') . '" async defer></script>';
if ($autoload) {
$attrs = ' class="g-recaptcha" data-sitekey="' . $recaptcha_public_key . '"';
if ($dark) {
$attrs .= ' data-theme="dark"';
}
}
else {
$attrs = '';
}
$container_tag = '<script>window.recaptchaKey = "' . $recaptcha_public_key
. '";</script><div id="g-recaptcha"'
. $attrs . '></div>';
$noscript_tag =<<<HTML
<noscript>
<div style="width: 302px;">
<div style="width: 302px; position: relative;">
<div style="width: 302px; height: 422px;">
<iframe src="https://www.google.com/recaptcha/api/fallback?k=$recaptcha_public_key" frameborder="0" scrolling="no" style="width: 302px; height:422px; border-style: none;"></iframe>
</div>
<div style="width: 300px; height: 60px; border-style: none;bottom: 12px; left: 25px; margin: 0px; padding: 0px; right: 25px;background: #f9f9f9; border: 1px solid #c1c1c1; border-radius: 3px;">
<textarea id="g-recaptcha-response" name="g-recaptcha-response" class="g-recaptcha-response" style="width: 250px; height: 40px; border: 1px solid #c1c1c1;margin: 10px 25px; padding: 0px; resize: none;"></textarea>
</div>
</div>
</div>
</noscript>
HTML;
if (defined('NOSCRIPT_CAPTCHA_ONLY') && NOSCRIPT_CAPTCHA_ONLY == 1) {
return $container_tag.$noscript_tag;
}
return $js_tag.$container_tag.$noscript_tag;
}
// Legacy captcha
// Uses recaptcha v2 for noscript captcha as the v1 seems to be broken currently.
function captcha_form_alt() {
global $recaptcha_public_key;
$html = <<<HTML
<div id="captchaContainerAlt"></div>
<script>
function onAltCaptchaClick() {
Recaptcha.reload('t');
}
function onAltCaptchaReady() {
var el;
if (el = document.getElementById('recaptcha_image')) {
el.title = 'Reload';
el.addEventListener('click', onAltCaptchaClick, false);
}
}
if (!window.passEnabled) {
var el = document.createElement('script');
el.type = 'text/javascript';
el.src = '//www.google.com/recaptcha/api/js/recaptcha_ajax.js';
el.onload = function() {
Recaptcha.create('$recaptcha_public_key',
'captchaContainerAlt',
{
theme: 'clean',
tabindex: 3,
callback: onAltCaptchaReady
}
);
}
document.head.appendChild(el);
}</script>
<noscript>
<div style="width: 302px;">
<div style="width: 302px; position: relative;">
<div style="width: 302px; height: 422px;">
<iframe src="https://www.google.com/recaptcha/api/fallback?k=$recaptcha_public_key" frameborder="0" scrolling="no" style="width: 302px; height:422px; border-style: none;"></iframe>
</div>
<div style="width: 300px; height: 60px; border-style: none;bottom: 12px; left: 25px; margin: 0px; padding: 0px; right: 25px;background: #f9f9f9; border: 1px solid #c1c1c1; border-radius: 3px;">
<textarea id="g-recaptcha-response" name="g-recaptcha-response" class="g-recaptcha-response" style="width: 250px; height: 40px; border: 1px solid #c1c1c1;margin: 10px 25px; padding: 0px; resize: none;"></textarea>
</div>
</div>
</div>
</noscript>
HTML;
return $html;
}
function recaptcha_ban($n, $time, $return_error = 0, $length = 1)
{
auto_ban_poster($name, $length, 1, "failed verification $n times per $time", "Possible spambot; repeatedly sent incorrect CAPTCHA verification.");
if( $return_error == 1 ) {
return S_GENERICERROR;
}
error(S_GENERICERROR);
}
/**
* Works for both recaptcha and hcaptcha
*/
function recaptcha_bad_captcha($return_error = false, $codes = null) {
$error = S_BADCAPTCHA;
if (is_array($codes)) {
if (in_array('missing-input-response', $codes)) {
$error = S_NOCAPTCHA;
}
if ($return_error) {
return $error;
}
else {
error($error);
}
}
else {
if ($return_error) {
return $error;
}
else {
error($error);
}
}
}
// -----------
// hCaptcha
// -----------
function start_hcaptcha_verify($return_error = false) {
global $hcaptcha_private_key, $hcaptcha_ch;
$response = $_POST["h-captcha-response"];
if (!$response) {
if ($return_error == false) {
error(S_NOCAPTCHA);
}
return S_NOCAPTCHA;
}
$response = urlencode($response);
$rlen = strlen($response);
if ($rlen > 32768) {
return recaptcha_bad_captcha($return_error);
}
$api_url = 'https://hcaptcha.com/siteverify';
$post = array(
'secret' => $hcaptcha_private_key,
'response' => $response
);
$hcaptcha_ch = rpc_start_captcha_request($api_url, $post, null, false);
}
function end_hcaptcha_verify($return_error = false) {
global $hcaptcha_ch;
if (!$hcaptcha_ch) {
return;
}
$ret = rpc_finish_request($hcaptcha_ch, $error, $httperror);
// BAD
// 413 Request Too Large is bad; it was caused intentionally by the user.
if ($httperror == 413) {
return recaptcha_bad_captcha($return_error);
}
// BAD
if ($ret == null) {
return recaptcha_bad_captcha($return_error);
}
$resp = json_decode($ret, true);
// BAD
// Malformed JSON response from Google
if (json_last_error() !== JSON_ERROR_NONE) {
return recaptcha_bad_captcha($return_error);
}
// GOOD
if ($resp['success']) {
return $resp;
}
// BAD
return recaptcha_bad_captcha($return_error, $resp['error-codes']);
}
// -----------
// reCaptcha V2
// -----------
// FIXME $challenge_field is no longer used
function start_recaptcha_verify($return_error = false, $challenge_field = '') {
global $recaptcha_private_key, $recaptcha_ch;
$response = $_POST["g-recaptcha-response"];
if (!$response) {
if ($return_error == false) {
error(S_NOCAPTCHA);
}
return S_NOCAPTCHA;
}
$response = urlencode($response);
$rlen = strlen($response);
if ($rlen > 4096) {
return recaptcha_bad_captcha($return_error);
}
$api_url = 'https://www.google.com/recaptcha/api/siteverify';
$post = array(
'secret' => $recaptcha_private_key,
'response' => $response
);
$recaptcha_ch = rpc_start_captcha_request($api_url, $post, null, false);
}
function end_recaptcha_verify($return_error = false) {
global $recaptcha_ch;
if (!$recaptcha_ch) {
return;
}
$ret = rpc_finish_request($recaptcha_ch, $error, $httperror);
// BAD
// 413 Request Too Large is bad; it was caused intentionally by the user.
if ($httperror == 413) {
return recaptcha_bad_captcha($return_error);
}
// BAD
if ($ret == null) {
return recaptcha_bad_captcha($return_error);
}
$resp = json_decode($ret, true);
// BAD
// Malformed JSON response from Google
if (json_last_error() !== JSON_ERROR_NONE) {
return recaptcha_bad_captcha($return_error);
}
// GOOD
if ($resp['success']) {
return $resp;
}
// BAD
return recaptcha_bad_captcha($return_error, $resp['error-codes']);
}
// -----------
// reCaptcha V1
// -----------
function start_recaptcha_verify_alt($return_error = false, $challenge_field = '') {
global $recaptcha_private_key, $recaptcha_ch;
$challenge = ( $challenge_field == '' ) ? $_POST["recaptcha_challenge_field"] : $challenge_field;
$response = $_POST["recaptcha_response_field"];
if (!$challenge || !$response) {
if( $return_error == false ) {
error(S_NOCAPTCHA);
}
return S_NOCAPTCHA;
}
$num_words = 1 + preg_match_all('/\\s/', $response);
$rlen = strlen($response);
if ($num_words > 3 || $rlen > 128) {
return recaptcha_bad_captcha($return_error);
}
$post = array(
"privatekey" => $recaptcha_private_key,
"challenge" => $challenge,
"remoteip" => $_SERVER["REMOTE_ADDR"],
"response" => $response
);
$recaptcha_ch = rpc_start_request("https://www.google.com/recaptcha/api/verify", $post, null, false);
}
function end_recaptcha_verify_alt($return_error = false) {
global $recaptcha_ch;
if (!$recaptcha_ch) return;
$ret = rpc_finish_request($recaptcha_ch, $error, $httperror);
if ($httperror == 413) {
return recaptcha_bad_captcha($return_error);
}
if ($ret) {
$lines = explode("\n", $ret);
if ($lines[0] === "true") {
// GOOD
return;
}
}
// BAD
return recaptcha_bad_captcha($return_error);
}
?>

345
lib/db.php Normal file
View file

@ -0,0 +1,345 @@
<?php
// generic db functions
require_once 'config/config_db.php';
require_once 'lib/util.php';
// define the error message strings in case this wasn't used in a file that
// uses the full yotsuba_config system...
if(!defined('S_SQLCONF')) {
define('S_SQLCONF', 'MySQL connection error');
define('S_SQLDBSF', 'MySQL database error');
}
//paranoid since i don't know when it fails, when it does fail
function mysql_try_connect($host,$usr,$pass,$db,$pconnect=true) {
global $mysql_connect_opts;
$tries = 1;
do {
$con = $pconnect ? @mysql_pconnect($host,$usr,$pass,$mysql_connect_opts) : @mysql_connect($host,$usr,$pass,1,$mysql_connect_opts);
$failed = 1; $pconnect = false;
if ($con) {
if (mysql_select_db($db, $con)) {
$failed = 0;
}
}
} while ($failed && $tries--);
if ($failed)
mysql_internal_err(NULL, "while connecting to $host", "", true);
return $con;
}
function mysql_internal_close($con) {
mysql_query("UNLOCK TABLES", $con);
mysql_close($con);
}
function mysql_board_lock($local=false) {
global $board_lock_level, $con;
if ($board_lock_level > 0) {
mysql_internal_err($con, "recursively locked table", "lock tables ".BOARD_DIR);
return;
}
$board_lock_level++;
mysql_query("lock tables ".BOARD_DIR." read".($local ? " local" : ""), $con);
}
function mysql_board_unlock($ignore_error=false) {
global $board_lock_level, $con;
if ($board_lock_level == 0) {
if (!$ignore_error)
mysql_internal_err($con, "not already locked", "unlock tables");
return;
}
$board_lock_level--;
mysql_query("unlock tables", $con);
}
function mysql_clear_locks() {
mysql_board_unlock(true);
}
function mysql_global_connect($pconnect=true) {
global $gcon;
$gcon = mysql_try_connect(SQLHOST_GLOBAL, SQLUSER_GLOBAL, SQLPASS_GLOBAL, SQLDB_GLOBAL, $pconnect);
return $gcon;
}
// assumes BOARD_DIR is set
function mysql_check_connections() {
global $gcon, $con;
$gcon_res = mysql_ping($gcon);
$con_res = mysql_ping($con);
if ($gcon_res == false || $con_res == false) {
mysql_internal_close($con);
mysql_internal_close($gcon);
$con = null;
$gcon = null;
mysql_board_connect(BOARD_DIR, false);
mysql_global_connect(false);
}
}
//really bad error system...
function mysql_internal_err($conn, $priverr, $query="", $die=false) {
global $mysql_never_die;
global $mysql_suppress_err;
$err = sprintf("%s error: %s - %d - %s%s", $query ? "query" : "connection",
$priverr, mysql_errno($conn), mysql_error($conn), $query ? " query: $query" : "");
if (SQL_DEBUG && ini_get('display_errors')) echo $err."\n";
internal_error_log("SQL", $err);
if ($die && !$mysql_never_die) die($query ? S_SQLDBSF : S_SQLCONF);
}
//pconnect - call _pconnect, not safe if tables are locked
function mysql_board_connect($board="", $pconnect=true) {
global $con;
global $did_add_lockfunc;
if (!defined('SQLHOST')) {
$db = 1; // db is always 1 now
$host = "db-ena.int"; // and always "db-ena" (this should never happen because we define SQLHOST)
$db = "img$db";
define('SQLHOST', $host);
define('SQLDB', $db);
if ($board) define('BOARD_DIR', $board);
} else {
$host = SQLHOST;
$db = SQLDB;
}
$con = mysql_try_connect($host, SQLUSER, SQLPASS, $db, $pconnect);
//if (SQL_DEBUG && ini_get('display_errors')) echo "connected to ".$host." SQLHOST is ".SQLHOST."\n";
if (!$did_add_lockfunc) {
$did_add_lockfunc = 1;
register_shutdown_function("mysql_clear_locks");
}
return $con;
}
function mysql_do_query($query, $con) {
global $mysql_unbuffered_reads;
global $mysql_suppress_err;
global $mysql_query_log;
global $mysql_debug_buf;
static $querylog_fd;
$querylog = (defined('QUERY_LOG') && constant('QUERY_LOG')) || $mysql_query_log == true;
$is_select = stripos($query, "SELECT")===0;
$time = 0;
if ($querylog) {
$time = microtime(true);
if (!$querylog_fd) {
$querylog_fd = fopen("/www/perhost/querylog.log", "a");
flock($querylog_fd, LOCK_EX);
}
fprintf($querylog_fd, "%d query: %s\n", getmypid(), $query);
}
if ($mysql_unbuffered_reads)
$ret = @mysql_unbuffered_query($query, $con);
else
$ret = @mysql_query($query, $con);
if ($ret && $querylog) {
$elapsed = microtime(true) - $time;
if (!$mysql_unbuffered_reads) {
$nr = @mysql_num_rows($ret);
if (!$nr) $nr = @mysql_affected_rows($ret);
} else
$nr = "?";
fprintf($querylog_fd, "%d rows, %f sec\n", $nr, $elapsed);
}
if (isset($mysql_debug_buf)) {
if (!$mysql_unbuffered_reads) {
$nr = @mysql_num_rows($ret);
if (!$nr) $nr = @mysql_affected_rows($ret);
if (!$nr) $nr = 0;
} else
$nr = "?";
$mysql_debug_buf .= "Query: $query\nRows: $nr\n";
}
if ($ret === FALSE) {
mysql_internal_err($con, "in do_query", $query);
}
return $ret;
}
function try_escape_string($string, $con, $recon_func, $tries=0)
{
$res = mysql_real_escape_string($string, $con);
if ($res === FALSE) {
mysql_internal_err($con, "in escape_string", $string, $tries == 0);
}
return $res;
}
//note for use of db query functions
//old-style calls (escaping done manually beforehand)
//must connect manually too
//for read queries
function mysql_global_call() {
global $gcon;
if(!$gcon) mysql_global_connect();
$args = func_get_args();
$format = array_shift($args);
if (count($args)) {
foreach($args as &$arg)
$arg = try_escape_string($arg, $gcon, "mysql_global_connect" );
$query = vsprintf($format, $args);
} else $query = $format;
return mysql_do_query( $query, $gcon );
}
function mysql_global_error() {
global $gcon;
if(!$gcon) mysql_global_connect();
return mysql_error($gcon);
}
//for r/w queries (historical, doesn't actually matter)
//TODO: remove
function mysql_global_do() {
global $gcon;
if(!$gcon) mysql_global_connect();
$args = func_get_args();
$format = array_shift($args);
if (count($args)) {
foreach($args as &$arg)
$arg = try_escape_string($arg, $gcon, "mysql_global_connect" );
$query = vsprintf($format, $args);
} else $query = $format;
return mysql_do_query( $query, $gcon );
}
function mysql_global_insert_id() {
global $gcon;
return mysql_insert_id($gcon);
}
function mysql_board_call() {
global $con;
if (!$con) mysql_board_connect();
$args = func_get_args();
$format = array_shift($args);
if (count($args)) {
foreach($args as &$arg)
$arg = mysql_real_escape_string($arg, $con);
$query = vsprintf($format, $args);
} else $query = $format;
return mysql_do_query( $query, $con );
}
function mysql_global_escape($string) {
global $gcon;
if(!$gcon) mysql_global_connect();
return mysql_real_escape_string($string, $gcon);
}
function mysql_board_error() {
global $con;
if(!$con) mysql_board_connect();
return mysql_error($con);
}
function mysql_board_escape($string) {
global $con;
if (!$con) mysql_board_connect();
return mysql_real_escape_string($string, $con);
}
function mysql_board_get_post($board,$no) {
global $con;
mysql_board_connect($board);
$query = mysql_board_call("SELECT HIGH_PRIORITY * from `%s` WHERE no=%d",$board,$no);
$array = mysql_fetch_assoc($query);
mysql_close($con);
$con = NULL;
return $array;
}
function mysql_board_get_post_lazy( $board, $no )
{
global $con;
mysql_board_connect($board);
$query = mysql_board_call("SELECT * from `%s` WHERE no=%d",$board,$no);
$array = mysql_fetch_assoc($query);
mysql_close($con);
$con = NULL;
return $array;
}
function mysql_board_insert_id() {
global $con;
return mysql_insert_id($con);
}
function mysql_global_row($table, $col, $val)
{
$q = mysql_global_call("select * from `%s` where $col='%s'", $table, $val);
$r = mysql_fetch_assoc($q);
mysql_free_result($q);
return $r;
}
function mysql_board_row($table, $col, $val)
{
$q = mysql_board_call("select * from `%s` where $col='%s'", $table, $val);
$r = mysql_fetch_assoc($q);
mysql_free_result($q);
return $r;
}
// answer must be one column
function mysql_column_array($q) {
$ret = array();
while ($row = mysql_fetch_row($q))
$ret[] = $row[0];
return $ret;
}
// turn "" into NULL
// incompatible with escaping :(
function mysql_nullify($s) {
return ($s || $s === '0') ? "'$s'" : "''"; //"NULL";
}
?>

308
lib/db_pdo.php Normal file
View file

@ -0,0 +1,308 @@
<?php
/**
* MySQLi DB Handler
* Drag and drop replacement for db.php!
*/
include_once 'config/config_db.php';
include_once 'lib/util.php';
if( !defined( 'S_SQLCONF' ) ) {
define( 'S_SQLCONF', 'MySQL connection error' );
define( 'S_SQLDBSF', 'MySQL database error' );
}
/**
* @param PDO $con
* @param PDO $gcon
*/
$con = $gcon = null;
$has_set_unbuffered = false;
function mysql_try_connect( $host, $user, $pass, $db, $pconnect = true )
{
global $mysql_connect_opts;
$tries = 1;
do {
$pconnect = ( $pconnect ) ? array(PDO::ATTR_PERSISTENT => true) : null;
$con = @new PDO( "mysql:host=$host;dbname=$db", $user, $pass, $pconnect );
$failed = 1; $pconnect = false;
if( $con ) $failed = 0;
} while( $failed && $tries-- );
if( $failed ) {
mysql_internal_err( NULL, "while connecting to $host", "", true );
}
// Set attributes
$con->setAttribute( PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC );
$con->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );
$con->setAttribute( PDO::ATTR_ORACLE_NULLS, PDO::NULL_TO_STRING );
//$con->setAttribute( PDO::ATTR_EMULATE_PREPARES, false );
$con->setAttribute( PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, true );
return $con;
}
/**
* @param PDO $conn
*/
function mysql_internal_err($conn, $priverr, $query="", $die=false) {
global $mysql_never_die;
global $mysql_suppress_err;
if ($mysql_suppress_err) return;
$errInfo = $conn->errorInfo();
$errInfo = $errInfo[2];
$err = sprintf("%s error: %s - %d - %s%s", $query ? "query" : "connection",
$priverr, $conn->errorCode(), $errInfo, $query ? " query: $query" : "");
//internal_error_log("SQL", $err);
echo $err;
if ($die && !$mysql_never_die) die($query ? S_SQLDBSF : S_SQLCONF);
}
/**
* @param PDO $con
*/
function mysql_internal_close( $con )
{
$con->exec('UNLOCK TABLES');
unset($con);
}
function mysql_board_lock( $local = false )
{
global $board_lock_level, $con;
if( $board_lock_level > 0 ) {
mysql_internal_err( $con, "recursively locked table", "lock tables " . BOARD_DIR );
return;
}
$board_lock_level++;
$local = $local ? ' local' : '';
$con->exec("LOCK TABLE " . BOARD_DIR . " READ $local");
}
function mysql_board_unlock( $ignore_error = false )
{
global $board_lock_level, $con;
if( $board_lock_level == 0 ) {
if( !$ignore_error ) {
mysql_internal_err( $con, "not already locked", "UNLOCK TABLES" );
}
return;
}
$board_lock_level--;
$con->exec('UNLOCK TABLES');
}
function mysql_clear_locks()
{
mysql_board_unlock(true);
}
/** CONNECTIONS **/
function mysql_global_connect()
{
global $gcon;
$gcon = mysql_try_connect(
SQLHOST_GLOBAL,
SQLUSER_GLOBAL,
SQLPASS_GLOBAL,
SQLDB_GLOBAL
);
return $gcon;
}
function mysql_board_connect( $board = '', $pconnect = true )
{
global $con;
if( !defined( 'SQLHOST' ) || ( constant( 'BOARD_DIR' ) != $board ) ) {
if( !$board ) {
if( !defined( 'BOARD_DIR' ) ) {
mysql_internal_err( null, 'no board defined to connect to' );
} else {
$board = BOARD_DIR;
}
$db = 1;
$host = "db-ena.int";
$db = "img$db";
define( 'SQLHOST', $host );
define( 'SQLDB', $db );
define( 'BOARD_DIR', $board );
}
} else {
$host = SQLHOST;
$db = SQLDB;
}
$con = mysql_try_connect( $host, SQLUSER, SQLPASS, $db );
register_shutdown_function( 'mysql_clear_locks' );
return $con;
}
/**
* @param PDOStatement $query
* @param PDOStatement $res
* @param PDO $con
*/
function mysql_do_query( $query, $con, $querystr, $recon_func, $tries = 0 )
{
global $mysql_unbuffered_reads, $mysql_suppress_err, $has_set_unbuffered;
$querylog = defined( 'QUERY_LOG' ) && QUERY_LOG;
$is_select = strpos( $querystr, 'SELECT' ) === 0;
if( $querylog ) $time = microtime(true);
if( $mysql_unbuffered_reads ) $query->closeCursor(); // close anything open
$con->setAttribute( PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, (bool)!$mysql_unbuffered_reads );
$res = $query->execute();
if( $res && $querylog ) {
global $querylog_fd;
$elapsed = microtime(true);
$nr = $mysql_unbuffered_reads ? '?' : $query->rowCount();
if( !$querylog_fd ) {
$querylog_fd = fopen( '/www/perhost/querylog.log', 'a' );
flock( $querylog_fd, LOCK_EX );
}
fprintf($querylog_fd, "%d query: %s\n%d rows, %f sec\n", getmypid(), $query, $nr, $elapsed / 1000000.);
}
if( $mysql_suppress_err ) return $query;
if( $res === false || ( !$mysql_unbuffered_reads && $is_select && $res === false ) ) {
if( $is_select && $tries > 0 ) {
mysql_internal_close( $con );
$con = $recon_func();
return mysql_do_query( $query, $con, $querystr, $recon_func, $tries-1 );
} else {
error_log( 'do_query res = ' . gettype($res) . ': ' . $res );
mysql_internal_err( $con, 'in do_query', $querystr, $tries == 0 );
}
}
return $query;
}
function mysql_global_call()
{
global $gcon;
if( !$gcon ) mysql_global_connect();
$args = func_get_args();
$querystr = array_shift($args);
$query = $gcon->prepare($querystr);
if( count( $args ) ) {
$i = 1;
foreach( $args as $arg ) {
if( $arg == null ) $arg = '';
$type = is_int($arg) || ctype_digit($arg) ? PDO::PARAM_INT : PDO::PARAM_STR;
$query->bindValue( $i, $arg, $type );
$i++;
}
}
return mysql_do_query( $query, $gcon, $querystr, 'mysql_global_connect' );
}
function mysql_board_call()
{
global $con;
if( !$con ) mysql_board_connect();
$args = func_get_args();
$querystr = array_shift($args);
$query = $con->prepare($querystr);
if( count( $args ) ) {
$i = 1;
foreach( $args as $arg ) {
if( $arg == null ) $arg = '';
$type = is_int($arg) || ctype_digit($arg) ? PDO::PARAM_INT : PDO::PARAM_STR;
$query->bindValue( $i, $arg, $type );
$i++;
}
}
return mysql_do_query( $query, $con, $querystr, 'mysql_global_connect' );
}
function mysql_board_get_post( $board, $no )
{
global $con;
mysql_board_connect( $board );
/**
* @param PDOStatement $query
*/
$query = mysql_board_call( "SELECT HIGH_PRIORITY * FROM $board WHERE no=?", $no );
$arr = $query->fetch();
$con = null;
return $arr;
}
/**
* Utility function to make $query->fetch() on SELECT COUNT() not a pain in the ass
*
* @param PDOStatement $query
*/
function mysql_fetch_count( $query )
{
return $query->fetchColumn();
}
/**
* Utility function mimicking mysql_free_result()
*
* @param PDOStatement $query
*/
function mysql_close_result( $query )
{
$query->closeCursor();
}
function mysql_column_array( $query, $field )
{
$ret = array();
while( $row = $query->fetch() ) {
$ret[] = $row[$field];
}
return $ret;
}
function mysql_board_insert_id()
{
global $con;
return $con->lastInsertId();
}

115
lib/geoip2-test.php Normal file
View file

@ -0,0 +1,115 @@
<?php
//require_once('MaxMind-DB-Reader/autoload.php');
final class GeoIP2 {
private static
$db_file_country = '/usr/local/share/GeoIP2/GeoLite2-City.mmdb',
$db_file_asn = '/usr/local/share/GeoIP2/GeoLite2-ASN.mmdb'
;
private static
$mmdb_country = null,
$mmdb_asn = null
;
private function __construct() {}
private static function load_db($file) {
try {
return new MaxMind\Db\Reader($file);
} catch (Exception $e) {
return false;
}
}
// geoip_record_by_name
public static function get_country($ip) {
if (!$ip) {
return null;
}
if (self::$mmdb_country === null) {
self::$mmdb_country = self::load_db(self::$db_file_country);
}
if (!self::$mmdb_country) {
return null;
}
try {
$entry = self::$mmdb_country->get($ip);
} catch (Exception $e) {
return null;
}
$data = array();
// Continent
if (isset($entry['continent']['code'])) {
$data['continent_code'] = $entry['continent']['code'];
}
// Country
if (isset($entry['country']['iso_code'])) {
$data['country_code'] = $entry['country']['iso_code'];
$data['country_name'] = $entry['country']['names']['en'];
// State for US
if ($data['country_code'] === 'US' && isset($entry['subdivisions'][0]['iso_code'])) {
$data['state_code'] = $entry['subdivisions'][0]['iso_code'];
$data['state_name'] = $entry['subdivisions'][0]['names']['en'];
}
// FIXME: subdivisions for UK during sport events
else if ($data['country_code'] === 'GB' && isset($entry['subdivisions'][0]['iso_code'])) {
$data['sub_code'] = $entry['subdivisions'][0]['iso_code'];
}
}
if (isset($entry['city']['names']['en'])) {
$data['city_name'] = $entry['city']['names']['en'];
}
if (empty($data)) {
return null;
}
return $data;
}
public static function get_asn($ip) {
if (!$ip) {
return null;
}
if (self::$mmdb_asn === null) {
self::$mmdb_asn = self::load_db(self::$db_file_asn);
}
if (!self::$mmdb_asn) {
return null;
}
try {
$entry = self::$mmdb_asn->get($ip);
} catch (Exception $e) {
return null;
}
$data = array();
if (isset($entry['autonomous_system_number'])) {
$data['asn'] = $entry['autonomous_system_number'];
}
if (isset($entry['autonomous_system_organization'])) {
$data['aso'] = $entry['autonomous_system_organization'];
}
if (empty($data)) {
return null;
}
return $data;
}
}

115
lib/geoip2.php Normal file
View file

@ -0,0 +1,115 @@
<?php
//require_once('MaxMind-DB-Reader/autoload.php');
final class GeoIP2 {
private static
$db_file_country = '/usr/local/share/GeoIP2/GeoLite2-City.mmdb',
$db_file_asn = '/usr/local/share/GeoIP2/GeoLite2-ASN.mmdb'
;
private static
$mmdb_country = null,
$mmdb_asn = null
;
private function __construct() {}
private static function load_db($file) {
try {
return new MaxMind\Db\Reader($file);
} catch (Exception $e) {
return false;
}
}
// geoip_record_by_name
public static function get_country($ip) {
if (!$ip) {
return null;
}
if (self::$mmdb_country === null) {
self::$mmdb_country = self::load_db(self::$db_file_country);
}
if (!self::$mmdb_country) {
return null;
}
try {
$entry = self::$mmdb_country->get($ip);
} catch (Exception $e) {
return null;
}
$data = array();
// Continent
if (isset($entry['continent']['code'])) {
$data['continent_code'] = $entry['continent']['code'];
}
// Country
if (isset($entry['country']['iso_code'])) {
$data['country_code'] = $entry['country']['iso_code'];
$data['country_name'] = $entry['country']['names']['en'];
// State for US
if ($data['country_code'] === 'US' && isset($entry['subdivisions'][0]['iso_code'])) {
$data['state_code'] = $entry['subdivisions'][0]['iso_code'];
$data['state_name'] = $entry['subdivisions'][0]['names']['en'];
}
// FIXME: subdivisions for UK during sport events
else if ($data['country_code'] === 'GB' && isset($entry['subdivisions'][0]['iso_code'])) {
$data['sub_code'] = $entry['subdivisions'][0]['iso_code'];
}
}
if (isset($entry['city']['names']['en'])) {
$data['city_name'] = $entry['city']['names']['en'];
}
if (empty($data)) {
return null;
}
return $data;
}
public static function get_asn($ip) {
if (!$ip) {
return null;
}
if (self::$mmdb_asn === null) {
self::$mmdb_asn = self::load_db(self::$db_file_asn);
}
if (!self::$mmdb_asn) {
return null;
}
try {
$entry = self::$mmdb_asn->get($ip);
} catch (Exception $e) {
return null;
}
$data = array();
if (isset($entry['autonomous_system_number'])) {
$data['asn'] = $entry['autonomous_system_number'];
}
if (isset($entry['autonomous_system_organization'])) {
$data['aso'] = $entry['autonomous_system_organization'];
}
if (empty($data)) {
return null;
}
return $data;
}
}

6
lib/global_constants.php Normal file
View file

@ -0,0 +1,6 @@
<?php
// If captcha is already defined we have included global_config.ini already.
if( defined( 'CAPTCHA' ) ) return;
define( 'ONLY_PARSE_INI', true );
require_once 'yotsuba_config.php';

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,48 @@
<?php
/**
* Converts HTMLPurifier_ConfigSchema_Interchange to our runtime
* representation used to perform checks on user configuration.
*/
class HTMLPurifier_ConfigSchema_Builder_ConfigSchema
{
/**
* @param HTMLPurifier_ConfigSchema_Interchange $interchange
* @return HTMLPurifier_ConfigSchema
*/
public function build($interchange)
{
$schema = new HTMLPurifier_ConfigSchema();
foreach ($interchange->directives as $d) {
$schema->add(
$d->id->key,
$d->default,
$d->type,
$d->typeAllowsNull
);
if ($d->allowed !== null) {
$schema->addAllowedValues(
$d->id->key,
$d->allowed
);
}
foreach ($d->aliases as $alias) {
$schema->addAlias(
$alias->key,
$d->id->key
);
}
if ($d->valueAliases !== null) {
$schema->addValueAliases(
$d->id->key,
$d->valueAliases
);
}
}
$schema->postProcess();
return $schema;
}
}
// vim: et sw=4 sts=4

View file

@ -0,0 +1,144 @@
<?php
/**
* Converts HTMLPurifier_ConfigSchema_Interchange to an XML format,
* which can be further processed to generate documentation.
*/
class HTMLPurifier_ConfigSchema_Builder_Xml extends XMLWriter
{
/**
* @type HTMLPurifier_ConfigSchema_Interchange
*/
protected $interchange;
/**
* @type string
*/
private $namespace;
/**
* @param string $html
*/
protected function writeHTMLDiv($html)
{
$this->startElement('div');
$purifier = HTMLPurifier::getInstance();
$html = $purifier->purify($html);
$this->writeAttribute('xmlns', 'http://www.w3.org/1999/xhtml');
$this->writeRaw($html);
$this->endElement(); // div
}
/**
* @param mixed $var
* @return string
*/
protected function export($var)
{
if ($var === array()) {
return 'array()';
}
return var_export($var, true);
}
/**
* @param HTMLPurifier_ConfigSchema_Interchange $interchange
*/
public function build($interchange)
{
// global access, only use as last resort
$this->interchange = $interchange;
$this->setIndent(true);
$this->startDocument('1.0', 'UTF-8');
$this->startElement('configdoc');
$this->writeElement('title', $interchange->name);
foreach ($interchange->directives as $directive) {
$this->buildDirective($directive);
}
if ($this->namespace) {
$this->endElement();
} // namespace
$this->endElement(); // configdoc
$this->flush();
}
/**
* @param HTMLPurifier_ConfigSchema_Interchange_Directive $directive
*/
public function buildDirective($directive)
{
// Kludge, although I suppose having a notion of a "root namespace"
// certainly makes things look nicer when documentation is built.
// Depends on things being sorted.
if (!$this->namespace || $this->namespace !== $directive->id->getRootNamespace()) {
if ($this->namespace) {
$this->endElement();
} // namespace
$this->namespace = $directive->id->getRootNamespace();
$this->startElement('namespace');
$this->writeAttribute('id', $this->namespace);
$this->writeElement('name', $this->namespace);
}
$this->startElement('directive');
$this->writeAttribute('id', $directive->id->toString());
$this->writeElement('name', $directive->id->getDirective());
$this->startElement('aliases');
foreach ($directive->aliases as $alias) {
$this->writeElement('alias', $alias->toString());
}
$this->endElement(); // aliases
$this->startElement('constraints');
if ($directive->version) {
$this->writeElement('version', $directive->version);
}
$this->startElement('type');
if ($directive->typeAllowsNull) {
$this->writeAttribute('allow-null', 'yes');
}
$this->text($directive->type);
$this->endElement(); // type
if ($directive->allowed) {
$this->startElement('allowed');
foreach ($directive->allowed as $value => $x) {
$this->writeElement('value', $value);
}
$this->endElement(); // allowed
}
$this->writeElement('default', $this->export($directive->default));
$this->writeAttribute('xml:space', 'preserve');
if ($directive->external) {
$this->startElement('external');
foreach ($directive->external as $project) {
$this->writeElement('project', $project);
}
$this->endElement();
}
$this->endElement(); // constraints
if ($directive->deprecatedVersion) {
$this->startElement('deprecated');
$this->writeElement('version', $directive->deprecatedVersion);
$this->writeElement('use', $directive->deprecatedUse->toString());
$this->endElement(); // deprecated
}
$this->startElement('description');
$this->writeHTMLDiv($directive->description);
$this->endElement(); // description
$this->endElement(); // directive
}
}
// vim: et sw=4 sts=4

View file

@ -0,0 +1,11 @@
<?php
/**
* Exceptions related to configuration schema
*/
class HTMLPurifier_ConfigSchema_Exception extends HTMLPurifier_Exception
{
}
// vim: et sw=4 sts=4

View file

@ -0,0 +1,47 @@
<?php
/**
* Generic schema interchange format that can be converted to a runtime
* representation (HTMLPurifier_ConfigSchema) or HTML documentation. Members
* are completely validated.
*/
class HTMLPurifier_ConfigSchema_Interchange
{
/**
* Name of the application this schema is describing.
* @type string
*/
public $name;
/**
* Array of Directive ID => array(directive info)
* @type HTMLPurifier_ConfigSchema_Interchange_Directive[]
*/
public $directives = array();
/**
* Adds a directive array to $directives
* @param HTMLPurifier_ConfigSchema_Interchange_Directive $directive
* @throws HTMLPurifier_ConfigSchema_Exception
*/
public function addDirective($directive)
{
if (isset($this->directives[$i = $directive->id->toString()])) {
throw new HTMLPurifier_ConfigSchema_Exception("Cannot redefine directive '$i'");
}
$this->directives[$i] = $directive;
}
/**
* Convenience function to perform standard validation. Throws exception
* on failed validation.
*/
public function validate()
{
$validator = new HTMLPurifier_ConfigSchema_Validator();
return $validator->validate($this);
}
}
// vim: et sw=4 sts=4

View file

@ -0,0 +1,89 @@
<?php
/**
* Interchange component class describing configuration directives.
*/
class HTMLPurifier_ConfigSchema_Interchange_Directive
{
/**
* ID of directive.
* @type HTMLPurifier_ConfigSchema_Interchange_Id
*/
public $id;
/**
* Type, e.g. 'integer' or 'istring'.
* @type string
*/
public $type;
/**
* Default value, e.g. 3 or 'DefaultVal'.
* @type mixed
*/
public $default;
/**
* HTML description.
* @type string
*/
public $description;
/**
* Whether or not null is allowed as a value.
* @type bool
*/
public $typeAllowsNull = false;
/**
* Lookup table of allowed scalar values.
* e.g. array('allowed' => true).
* Null if all values are allowed.
* @type array
*/
public $allowed;
/**
* List of aliases for the directive.
* e.g. array(new HTMLPurifier_ConfigSchema_Interchange_Id('Ns', 'Dir'))).
* @type HTMLPurifier_ConfigSchema_Interchange_Id[]
*/
public $aliases = array();
/**
* Hash of value aliases, e.g. array('alt' => 'real'). Null if value
* aliasing is disabled (necessary for non-scalar types).
* @type array
*/
public $valueAliases;
/**
* Version of HTML Purifier the directive was introduced, e.g. '1.3.1'.
* Null if the directive has always existed.
* @type string
*/
public $version;
/**
* ID of directive that supercedes this old directive.
* Null if not deprecated.
* @type HTMLPurifier_ConfigSchema_Interchange_Id
*/
public $deprecatedUse;
/**
* Version of HTML Purifier this directive was deprecated. Null if not
* deprecated.
* @type string
*/
public $deprecatedVersion;
/**
* List of external projects this directive depends on, e.g. array('CSSTidy').
* @type array
*/
public $external = array();
}
// vim: et sw=4 sts=4

View file

@ -0,0 +1,58 @@
<?php
/**
* Represents a directive ID in the interchange format.
*/
class HTMLPurifier_ConfigSchema_Interchange_Id
{
/**
* @type string
*/
public $key;
/**
* @param string $key
*/
public function __construct($key)
{
$this->key = $key;
}
/**
* @return string
* @warning This is NOT magic, to ensure that people don't abuse SPL and
* cause problems for PHP 5.0 support.
*/
public function toString()
{
return $this->key;
}
/**
* @return string
*/
public function getRootNamespace()
{
return substr($this->key, 0, strpos($this->key, "."));
}
/**
* @return string
*/
public function getDirective()
{
return substr($this->key, strpos($this->key, ".") + 1);
}
/**
* @param string $id
* @return HTMLPurifier_ConfigSchema_Interchange_Id
*/
public static function make($id)
{
return new HTMLPurifier_ConfigSchema_Interchange_Id($id);
}
}
// vim: et sw=4 sts=4

View file

@ -0,0 +1,226 @@
<?php
class HTMLPurifier_ConfigSchema_InterchangeBuilder
{
/**
* Used for processing DEFAULT, nothing else.
* @type HTMLPurifier_VarParser
*/
protected $varParser;
/**
* @param HTMLPurifier_VarParser $varParser
*/
public function __construct($varParser = null)
{
$this->varParser = $varParser ? $varParser : new HTMLPurifier_VarParser_Native();
}
/**
* @param string $dir
* @return HTMLPurifier_ConfigSchema_Interchange
*/
public static function buildFromDirectory($dir = null)
{
$builder = new HTMLPurifier_ConfigSchema_InterchangeBuilder();
$interchange = new HTMLPurifier_ConfigSchema_Interchange();
return $builder->buildDir($interchange, $dir);
}
/**
* @param HTMLPurifier_ConfigSchema_Interchange $interchange
* @param string $dir
* @return HTMLPurifier_ConfigSchema_Interchange
*/
public function buildDir($interchange, $dir = null)
{
if (!$dir) {
$dir = HTMLPURIFIER_PREFIX . '/HTMLPurifier/ConfigSchema/schema';
}
if (file_exists($dir . '/info.ini')) {
$info = parse_ini_file($dir . '/info.ini');
$interchange->name = $info['name'];
}
$files = array();
$dh = opendir($dir);
while (false !== ($file = readdir($dh))) {
if (!$file || $file[0] == '.' || strrchr($file, '.') !== '.txt') {
continue;
}
$files[] = $file;
}
closedir($dh);
sort($files);
foreach ($files as $file) {
$this->buildFile($interchange, $dir . '/' . $file);
}
return $interchange;
}
/**
* @param HTMLPurifier_ConfigSchema_Interchange $interchange
* @param string $file
*/
public function buildFile($interchange, $file)
{
$parser = new HTMLPurifier_StringHashParser();
$this->build(
$interchange,
new HTMLPurifier_StringHash($parser->parseFile($file))
);
}
/**
* Builds an interchange object based on a hash.
* @param HTMLPurifier_ConfigSchema_Interchange $interchange HTMLPurifier_ConfigSchema_Interchange object to build
* @param HTMLPurifier_StringHash $hash source data
* @throws HTMLPurifier_ConfigSchema_Exception
*/
public function build($interchange, $hash)
{
if (!$hash instanceof HTMLPurifier_StringHash) {
$hash = new HTMLPurifier_StringHash($hash);
}
if (!isset($hash['ID'])) {
throw new HTMLPurifier_ConfigSchema_Exception('Hash does not have any ID');
}
if (strpos($hash['ID'], '.') === false) {
if (count($hash) == 2 && isset($hash['DESCRIPTION'])) {
$hash->offsetGet('DESCRIPTION'); // prevent complaining
} else {
throw new HTMLPurifier_ConfigSchema_Exception('All directives must have a namespace');
}
} else {
$this->buildDirective($interchange, $hash);
}
$this->_findUnused($hash);
}
/**
* @param HTMLPurifier_ConfigSchema_Interchange $interchange
* @param HTMLPurifier_StringHash $hash
* @throws HTMLPurifier_ConfigSchema_Exception
*/
public function buildDirective($interchange, $hash)
{
$directive = new HTMLPurifier_ConfigSchema_Interchange_Directive();
// These are required elements:
$directive->id = $this->id($hash->offsetGet('ID'));
$id = $directive->id->toString(); // convenience
if (isset($hash['TYPE'])) {
$type = explode('/', $hash->offsetGet('TYPE'));
if (isset($type[1])) {
$directive->typeAllowsNull = true;
}
$directive->type = $type[0];
} else {
throw new HTMLPurifier_ConfigSchema_Exception("TYPE in directive hash '$id' not defined");
}
if (isset($hash['DEFAULT'])) {
try {
$directive->default = $this->varParser->parse(
$hash->offsetGet('DEFAULT'),
$directive->type,
$directive->typeAllowsNull
);
} catch (HTMLPurifier_VarParserException $e) {
throw new HTMLPurifier_ConfigSchema_Exception($e->getMessage() . " in DEFAULT in directive hash '$id'");
}
}
if (isset($hash['DESCRIPTION'])) {
$directive->description = $hash->offsetGet('DESCRIPTION');
}
if (isset($hash['ALLOWED'])) {
$directive->allowed = $this->lookup($this->evalArray($hash->offsetGet('ALLOWED')));
}
if (isset($hash['VALUE-ALIASES'])) {
$directive->valueAliases = $this->evalArray($hash->offsetGet('VALUE-ALIASES'));
}
if (isset($hash['ALIASES'])) {
$raw_aliases = trim($hash->offsetGet('ALIASES'));
$aliases = preg_split('/\s*,\s*/', $raw_aliases);
foreach ($aliases as $alias) {
$directive->aliases[] = $this->id($alias);
}
}
if (isset($hash['VERSION'])) {
$directive->version = $hash->offsetGet('VERSION');
}
if (isset($hash['DEPRECATED-USE'])) {
$directive->deprecatedUse = $this->id($hash->offsetGet('DEPRECATED-USE'));
}
if (isset($hash['DEPRECATED-VERSION'])) {
$directive->deprecatedVersion = $hash->offsetGet('DEPRECATED-VERSION');
}
if (isset($hash['EXTERNAL'])) {
$directive->external = preg_split('/\s*,\s*/', trim($hash->offsetGet('EXTERNAL')));
}
$interchange->addDirective($directive);
}
/**
* Evaluates an array PHP code string without array() wrapper
* @param string $contents
*/
protected function evalArray($contents)
{
return eval('return array(' . $contents . ');');
}
/**
* Converts an array list into a lookup array.
* @param array $array
* @return array
*/
protected function lookup($array)
{
$ret = array();
foreach ($array as $val) {
$ret[$val] = true;
}
return $ret;
}
/**
* Convenience function that creates an HTMLPurifier_ConfigSchema_Interchange_Id
* object based on a string Id.
* @param string $id
* @return HTMLPurifier_ConfigSchema_Interchange_Id
*/
protected function id($id)
{
return HTMLPurifier_ConfigSchema_Interchange_Id::make($id);
}
/**
* Triggers errors for any unused keys passed in the hash; such keys
* may indicate typos, missing values, etc.
* @param HTMLPurifier_StringHash $hash Hash to check.
*/
protected function _findUnused($hash)
{
$accessed = $hash->getAccessed();
foreach ($hash as $k => $v) {
if (!isset($accessed[$k])) {
trigger_error("String hash key '$k' not used by builder", E_USER_NOTICE);
}
}
}
}
// vim: et sw=4 sts=4

View file

@ -0,0 +1,248 @@
<?php
/**
* Performs validations on HTMLPurifier_ConfigSchema_Interchange
*
* @note If you see '// handled by InterchangeBuilder', that means a
* design decision in that class would prevent this validation from
* ever being necessary. We have them anyway, however, for
* redundancy.
*/
class HTMLPurifier_ConfigSchema_Validator
{
/**
* @type HTMLPurifier_ConfigSchema_Interchange
*/
protected $interchange;
/**
* @type array
*/
protected $aliases;
/**
* Context-stack to provide easy to read error messages.
* @type array
*/
protected $context = array();
/**
* to test default's type.
* @type HTMLPurifier_VarParser
*/
protected $parser;
public function __construct()
{
$this->parser = new HTMLPurifier_VarParser();
}
/**
* Validates a fully-formed interchange object.
* @param HTMLPurifier_ConfigSchema_Interchange $interchange
* @return bool
*/
public function validate($interchange)
{
$this->interchange = $interchange;
$this->aliases = array();
// PHP is a bit lax with integer <=> string conversions in
// arrays, so we don't use the identical !== comparison
foreach ($interchange->directives as $i => $directive) {
$id = $directive->id->toString();
if ($i != $id) {
$this->error(false, "Integrity violation: key '$i' does not match internal id '$id'");
}
$this->validateDirective($directive);
}
return true;
}
/**
* Validates a HTMLPurifier_ConfigSchema_Interchange_Id object.
* @param HTMLPurifier_ConfigSchema_Interchange_Id $id
*/
public function validateId($id)
{
$id_string = $id->toString();
$this->context[] = "id '$id_string'";
if (!$id instanceof HTMLPurifier_ConfigSchema_Interchange_Id) {
// handled by InterchangeBuilder
$this->error(false, 'is not an instance of HTMLPurifier_ConfigSchema_Interchange_Id');
}
// keys are now unconstrained (we might want to narrow down to A-Za-z0-9.)
// we probably should check that it has at least one namespace
$this->with($id, 'key')
->assertNotEmpty()
->assertIsString(); // implicit assertIsString handled by InterchangeBuilder
array_pop($this->context);
}
/**
* Validates a HTMLPurifier_ConfigSchema_Interchange_Directive object.
* @param HTMLPurifier_ConfigSchema_Interchange_Directive $d
*/
public function validateDirective($d)
{
$id = $d->id->toString();
$this->context[] = "directive '$id'";
$this->validateId($d->id);
$this->with($d, 'description')
->assertNotEmpty();
// BEGIN - handled by InterchangeBuilder
$this->with($d, 'type')
->assertNotEmpty();
$this->with($d, 'typeAllowsNull')
->assertIsBool();
try {
// This also tests validity of $d->type
$this->parser->parse($d->default, $d->type, $d->typeAllowsNull);
} catch (HTMLPurifier_VarParserException $e) {
$this->error('default', 'had error: ' . $e->getMessage());
}
// END - handled by InterchangeBuilder
if (!is_null($d->allowed) || !empty($d->valueAliases)) {
// allowed and valueAliases require that we be dealing with
// strings, so check for that early.
$d_int = HTMLPurifier_VarParser::$types[$d->type];
if (!isset(HTMLPurifier_VarParser::$stringTypes[$d_int])) {
$this->error('type', 'must be a string type when used with allowed or value aliases');
}
}
$this->validateDirectiveAllowed($d);
$this->validateDirectiveValueAliases($d);
$this->validateDirectiveAliases($d);
array_pop($this->context);
}
/**
* Extra validation if $allowed member variable of
* HTMLPurifier_ConfigSchema_Interchange_Directive is defined.
* @param HTMLPurifier_ConfigSchema_Interchange_Directive $d
*/
public function validateDirectiveAllowed($d)
{
if (is_null($d->allowed)) {
return;
}
$this->with($d, 'allowed')
->assertNotEmpty()
->assertIsLookup(); // handled by InterchangeBuilder
if (is_string($d->default) && !isset($d->allowed[$d->default])) {
$this->error('default', 'must be an allowed value');
}
$this->context[] = 'allowed';
foreach ($d->allowed as $val => $x) {
if (!is_string($val)) {
$this->error("value $val", 'must be a string');
}
}
array_pop($this->context);
}
/**
* Extra validation if $valueAliases member variable of
* HTMLPurifier_ConfigSchema_Interchange_Directive is defined.
* @param HTMLPurifier_ConfigSchema_Interchange_Directive $d
*/
public function validateDirectiveValueAliases($d)
{
if (is_null($d->valueAliases)) {
return;
}
$this->with($d, 'valueAliases')
->assertIsArray(); // handled by InterchangeBuilder
$this->context[] = 'valueAliases';
foreach ($d->valueAliases as $alias => $real) {
if (!is_string($alias)) {
$this->error("alias $alias", 'must be a string');
}
if (!is_string($real)) {
$this->error("alias target $real from alias '$alias'", 'must be a string');
}
if ($alias === $real) {
$this->error("alias '$alias'", "must not be an alias to itself");
}
}
if (!is_null($d->allowed)) {
foreach ($d->valueAliases as $alias => $real) {
if (isset($d->allowed[$alias])) {
$this->error("alias '$alias'", 'must not be an allowed value');
} elseif (!isset($d->allowed[$real])) {
$this->error("alias '$alias'", 'must be an alias to an allowed value');
}
}
}
array_pop($this->context);
}
/**
* Extra validation if $aliases member variable of
* HTMLPurifier_ConfigSchema_Interchange_Directive is defined.
* @param HTMLPurifier_ConfigSchema_Interchange_Directive $d
*/
public function validateDirectiveAliases($d)
{
$this->with($d, 'aliases')
->assertIsArray(); // handled by InterchangeBuilder
$this->context[] = 'aliases';
foreach ($d->aliases as $alias) {
$this->validateId($alias);
$s = $alias->toString();
if (isset($this->interchange->directives[$s])) {
$this->error("alias '$s'", 'collides with another directive');
}
if (isset($this->aliases[$s])) {
$other_directive = $this->aliases[$s];
$this->error("alias '$s'", "collides with alias for directive '$other_directive'");
}
$this->aliases[$s] = $d->id->toString();
}
array_pop($this->context);
}
// protected helper functions
/**
* Convenience function for generating HTMLPurifier_ConfigSchema_ValidatorAtom
* for validating simple member variables of objects.
* @param $obj
* @param $member
* @return HTMLPurifier_ConfigSchema_ValidatorAtom
*/
protected function with($obj, $member)
{
return new HTMLPurifier_ConfigSchema_ValidatorAtom($this->getFormattedContext(), $obj, $member);
}
/**
* Emits an error, providing helpful context.
* @throws HTMLPurifier_ConfigSchema_Exception
*/
protected function error($target, $msg)
{
if ($target !== false) {
$prefix = ucfirst($target) . ' in ' . $this->getFormattedContext();
} else {
$prefix = ucfirst($this->getFormattedContext());
}
throw new HTMLPurifier_ConfigSchema_Exception(trim($prefix . ' ' . $msg));
}
/**
* Returns a formatted context string.
* @return string
*/
protected function getFormattedContext()
{
return implode(' in ', array_reverse($this->context));
}
}
// vim: et sw=4 sts=4

View file

@ -0,0 +1,130 @@
<?php
/**
* Fluent interface for validating the contents of member variables.
* This should be immutable. See HTMLPurifier_ConfigSchema_Validator for
* use-cases. We name this an 'atom' because it's ONLY for validations that
* are independent and usually scalar.
*/
class HTMLPurifier_ConfigSchema_ValidatorAtom
{
/**
* @type string
*/
protected $context;
/**
* @type object
*/
protected $obj;
/**
* @type string
*/
protected $member;
/**
* @type mixed
*/
protected $contents;
public function __construct($context, $obj, $member)
{
$this->context = $context;
$this->obj = $obj;
$this->member = $member;
$this->contents =& $obj->$member;
}
/**
* @return HTMLPurifier_ConfigSchema_ValidatorAtom
*/
public function assertIsString()
{
if (!is_string($this->contents)) {
$this->error('must be a string');
}
return $this;
}
/**
* @return HTMLPurifier_ConfigSchema_ValidatorAtom
*/
public function assertIsBool()
{
if (!is_bool($this->contents)) {
$this->error('must be a boolean');
}
return $this;
}
/**
* @return HTMLPurifier_ConfigSchema_ValidatorAtom
*/
public function assertIsArray()
{
if (!is_array($this->contents)) {
$this->error('must be an array');
}
return $this;
}
/**
* @return HTMLPurifier_ConfigSchema_ValidatorAtom
*/
public function assertNotNull()
{
if ($this->contents === null) {
$this->error('must not be null');
}
return $this;
}
/**
* @return HTMLPurifier_ConfigSchema_ValidatorAtom
*/
public function assertAlnum()
{
$this->assertIsString();
if (!ctype_alnum($this->contents)) {
$this->error('must be alphanumeric');
}
return $this;
}
/**
* @return HTMLPurifier_ConfigSchema_ValidatorAtom
*/
public function assertNotEmpty()
{
if (empty($this->contents)) {
$this->error('must not be empty');
}
return $this;
}
/**
* @return HTMLPurifier_ConfigSchema_ValidatorAtom
*/
public function assertIsLookup()
{
$this->assertIsArray();
foreach ($this->contents as $v) {
if ($v !== true) {
$this->error('must be a lookup array');
}
}
return $this;
}
/**
* @param string $msg
* @throws HTMLPurifier_ConfigSchema_Exception
*/
protected function error($msg)
{
throw new HTMLPurifier_ConfigSchema_Exception(ucfirst($this->member) . ' in ' . $this->context . ' ' . $msg);
}
}
// vim: et sw=4 sts=4

View file

@ -0,0 +1,8 @@
Attr.AllowedClasses
TYPE: lookup/null
VERSION: 4.0.0
DEFAULT: null
--DESCRIPTION--
List of allowed class values in the class attribute. By default, this is null,
which means all classes are allowed.
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,12 @@
Attr.AllowedFrameTargets
TYPE: lookup
DEFAULT: array()
--DESCRIPTION--
Lookup table of all allowed link frame targets. Some commonly used link
targets include _blank, _self, _parent and _top. Values should be
lowercase, as validation will be done in a case-sensitive manner despite
W3C's recommendation. XHTML 1.0 Strict does not permit the target attribute
so this directive will have no effect in that doctype. XHTML 1.1 does not
enable the Target module by default, you will have to manually enable it
(see the module documentation for more details.)
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,9 @@
Attr.AllowedRel
TYPE: lookup
VERSION: 1.6.0
DEFAULT: array()
--DESCRIPTION--
List of allowed forward document relationships in the rel attribute. Common
values may be nofollow or print. By default, this is empty, meaning that no
document relationships are allowed.
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,9 @@
Attr.AllowedRev
TYPE: lookup
VERSION: 1.6.0
DEFAULT: array()
--DESCRIPTION--
List of allowed reverse document relationships in the rev attribute. This
attribute is a bit of an edge-case; if you don't know what it is for, stay
away.
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,19 @@
Attr.ClassUseCDATA
TYPE: bool/null
DEFAULT: null
VERSION: 4.0.0
--DESCRIPTION--
If null, class will auto-detect the doctype and, if matching XHTML 1.1 or
XHTML 2.0, will use the restrictive NMTOKENS specification of class. Otherwise,
it will use a relaxed CDATA definition. If true, the relaxed CDATA definition
is forced; if false, the NMTOKENS definition is forced. To get behavior
of HTML Purifier prior to 4.0.0, set this directive to false.
Some rational behind the auto-detection:
in previous versions of HTML Purifier, it was assumed that the form of
class was NMTOKENS, as specified by the XHTML Modularization (representing
XHTML 1.1 and XHTML 2.0). The DTDs for HTML 4.01 and XHTML 1.0, however
specify class as CDATA. HTML 5 effectively defines it as CDATA, but
with the additional constraint that each name should be unique (this is not
explicitly outlined in previous specifications).
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,11 @@
Attr.DefaultImageAlt
TYPE: string/null
DEFAULT: null
VERSION: 3.2.0
--DESCRIPTION--
This is the content of the alt tag of an image if the user had not
previously specified an alt attribute. This applies to all images without
a valid alt attribute, as opposed to %Attr.DefaultInvalidImageAlt, which
only applies to invalid images, and overrides in the case of an invalid image.
Default behavior with null is to use the basename of the src tag for the alt.
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,9 @@
Attr.DefaultInvalidImage
TYPE: string
DEFAULT: ''
--DESCRIPTION--
This is the default image an img tag will be pointed to if it does not have
a valid src attribute. In future versions, we may allow the image tag to
be removed completely, but due to design issues, this is not possible right
now.
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,8 @@
Attr.DefaultInvalidImageAlt
TYPE: string
DEFAULT: 'Invalid image'
--DESCRIPTION--
This is the content of the alt tag of an invalid image if the user had not
previously specified an alt attribute. It has no effect when the image is
valid but there was no alt attribute present.
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,10 @@
Attr.DefaultTextDir
TYPE: string
DEFAULT: 'ltr'
--DESCRIPTION--
Defines the default text direction (ltr or rtl) of the document being
parsed. This generally is the same as the value of the dir attribute in
HTML, or ltr if that is not specified.
--ALLOWED--
'ltr', 'rtl'
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,16 @@
Attr.EnableID
TYPE: bool
DEFAULT: false
VERSION: 1.2.0
--DESCRIPTION--
Allows the ID attribute in HTML. This is disabled by default due to the
fact that without proper configuration user input can easily break the
validation of a webpage by specifying an ID that is already on the
surrounding HTML. If you don't mind throwing caution to the wind, enable
this directive, but I strongly recommend you also consider blacklisting IDs
you use (%Attr.IDBlacklist) or prefixing all user supplied IDs
(%Attr.IDPrefix). When set to true HTML Purifier reverts to the behavior of
pre-1.2.0 versions.
--ALIASES--
HTML.EnableAttrID
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,8 @@
Attr.ForbiddenClasses
TYPE: lookup
VERSION: 4.0.0
DEFAULT: array()
--DESCRIPTION--
List of forbidden class values in the class attribute. By default, this is
empty, which means that no classes are forbidden. See also %Attr.AllowedClasses.
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,5 @@
Attr.IDBlacklist
TYPE: list
DEFAULT: array()
DESCRIPTION: Array of IDs not allowed in the document.
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,9 @@
Attr.IDBlacklistRegexp
TYPE: string/null
VERSION: 1.6.0
DEFAULT: NULL
--DESCRIPTION--
PCRE regular expression to be matched against all IDs. If the expression is
matches, the ID is rejected. Use this with care: may cause significant
degradation. ID matching is done after all other validation.
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,12 @@
Attr.IDPrefix
TYPE: string
VERSION: 1.2.0
DEFAULT: ''
--DESCRIPTION--
String to prefix to IDs. If you have no idea what IDs your pages may use,
you may opt to simply add a prefix to all user-submitted ID attributes so
that they are still usable, but will not conflict with core page IDs.
Example: setting the directive to 'user_' will result in a user submitted
'foo' to become 'user_foo' Be sure to set %HTML.EnableAttrID to true
before using this.
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,14 @@
Attr.IDPrefixLocal
TYPE: string
VERSION: 1.2.0
DEFAULT: ''
--DESCRIPTION--
Temporary prefix for IDs used in conjunction with %Attr.IDPrefix. If you
need to allow multiple sets of user content on web page, you may need to
have a seperate prefix that changes with each iteration. This way,
seperately submitted user content displayed on the same page doesn't
clobber each other. Ideal values are unique identifiers for the content it
represents (i.e. the id of the row in the database). Be sure to add a
seperator (like an underscore) at the end. Warning: this directive will
not work unless %Attr.IDPrefix is set to a non-empty value!
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,31 @@
AutoFormat.AutoParagraph
TYPE: bool
VERSION: 2.0.1
DEFAULT: false
--DESCRIPTION--
<p>
This directive turns on auto-paragraphing, where double newlines are
converted in to paragraphs whenever possible. Auto-paragraphing:
</p>
<ul>
<li>Always applies to inline elements or text in the root node,</li>
<li>Applies to inline elements or text with double newlines in nodes
that allow paragraph tags,</li>
<li>Applies to double newlines in paragraph tags</li>
</ul>
<p>
<code>p</code> tags must be allowed for this directive to take effect.
We do not use <code>br</code> tags for paragraphing, as that is
semantically incorrect.
</p>
<p>
To prevent auto-paragraphing as a content-producer, refrain from using
double-newlines except to specify a new paragraph or in contexts where
it has special meaning (whitespace usually has no meaning except in
tags like <code>pre</code>, so this should not be difficult.) To prevent
the paragraphing of inline text adjacent to block elements, wrap them
in <code>div</code> tags (the behavior is slightly different outside of
the root node.)
</p>
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,12 @@
AutoFormat.Custom
TYPE: list
VERSION: 2.0.1
DEFAULT: array()
--DESCRIPTION--
<p>
This directive can be used to add custom auto-format injectors.
Specify an array of injector names (class name minus the prefix)
or concrete implementations. Injector class must exist.
</p>
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,11 @@
AutoFormat.DisplayLinkURI
TYPE: bool
VERSION: 3.2.0
DEFAULT: false
--DESCRIPTION--
<p>
This directive turns on the in-text display of URIs in &lt;a&gt; tags, and disables
those links. For example, <a href="http://example.com">example</a> becomes
example (<a>http://example.com</a>).
</p>
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,12 @@
AutoFormat.Linkify
TYPE: bool
VERSION: 2.0.1
DEFAULT: false
--DESCRIPTION--
<p>
This directive turns on linkification, auto-linking http, ftp and
https URLs. <code>a</code> tags with the <code>href</code> attribute
must be allowed.
</p>
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,12 @@
AutoFormat.PurifierLinkify.DocURL
TYPE: string
VERSION: 2.0.1
DEFAULT: '#%s'
ALIASES: AutoFormatParam.PurifierLinkifyDocURL
--DESCRIPTION--
<p>
Location of configuration documentation to link to, let %s substitute
into the configuration's namespace and directive names sans the percent
sign.
</p>
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,12 @@
AutoFormat.PurifierLinkify
TYPE: bool
VERSION: 2.0.1
DEFAULT: false
--DESCRIPTION--
<p>
Internal auto-formatter that converts configuration directives in
syntax <a>%Namespace.Directive</a> to links. <code>a</code> tags
with the <code>href</code> attribute must be allowed.
</p>
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,11 @@
AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions
TYPE: lookup
VERSION: 4.0.0
DEFAULT: array('td' => true, 'th' => true)
--DESCRIPTION--
<p>
When %AutoFormat.RemoveEmpty and %AutoFormat.RemoveEmpty.RemoveNbsp
are enabled, this directive defines what HTML elements should not be
removede if they have only a non-breaking space in them.
</p>
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,15 @@
AutoFormat.RemoveEmpty.RemoveNbsp
TYPE: bool
VERSION: 4.0.0
DEFAULT: false
--DESCRIPTION--
<p>
When enabled, HTML Purifier will treat any elements that contain only
non-breaking spaces as well as regular whitespace as empty, and remove
them when %AutoForamt.RemoveEmpty is enabled.
</p>
<p>
See %AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions for a list of elements
that don't have this behavior applied to them.
</p>
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,46 @@
AutoFormat.RemoveEmpty
TYPE: bool
VERSION: 3.2.0
DEFAULT: false
--DESCRIPTION--
<p>
When enabled, HTML Purifier will attempt to remove empty elements that
contribute no semantic information to the document. The following types
of nodes will be removed:
</p>
<ul><li>
Tags with no attributes and no content, and that are not empty
elements (remove <code>&lt;a&gt;&lt;/a&gt;</code> but not
<code>&lt;br /&gt;</code>), and
</li>
<li>
Tags with no content, except for:<ul>
<li>The <code>colgroup</code> element, or</li>
<li>
Elements with the <code>id</code> or <code>name</code> attribute,
when those attributes are permitted on those elements.
</li>
</ul></li>
</ul>
<p>
Please be very careful when using this functionality; while it may not
seem that empty elements contain useful information, they can alter the
layout of a document given appropriate styling. This directive is most
useful when you are processing machine-generated HTML, please avoid using
it on regular user HTML.
</p>
<p>
Elements that contain only whitespace will be treated as empty. Non-breaking
spaces, however, do not count as whitespace. See
%AutoFormat.RemoveEmpty.RemoveNbsp for alternate behavior.
</p>
<p>
This algorithm is not perfect; you may still notice some empty tags,
particularly if a node had elements, but those elements were later removed
because they were not permitted in that context, or tags that, after
being auto-closed by another tag, where empty. This is for safety reasons
to prevent clever code from breaking validation. The general rule of thumb:
if a tag looked empty on the way in, it will get removed; if HTML Purifier
made it empty, it will stay.
</p>
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,11 @@
AutoFormat.RemoveSpansWithoutAttributes
TYPE: bool
VERSION: 4.0.1
DEFAULT: false
--DESCRIPTION--
<p>
This directive causes <code>span</code> tags without any attributes
to be removed. It will also remove spans that had all attributes
removed during processing.
</p>
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,8 @@
CSS.AllowImportant
TYPE: bool
DEFAULT: false
VERSION: 3.1.0
--DESCRIPTION--
This parameter determines whether or not !important cascade modifiers should
be allowed in user CSS. If false, !important will stripped.
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,11 @@
CSS.AllowTricky
TYPE: bool
DEFAULT: false
VERSION: 3.1.0
--DESCRIPTION--
This parameter determines whether or not to allow "tricky" CSS properties and
values. Tricky CSS properties/values can drastically modify page layout or
be used for deceptive practices but do not directly constitute a security risk.
For example, <code>display:none;</code> is considered a tricky property that
will only be allowed if this directive is set to true.
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,12 @@
CSS.AllowedFonts
TYPE: lookup/null
VERSION: 4.3.0
DEFAULT: NULL
--DESCRIPTION--
<p>
Allows you to manually specify a set of allowed fonts. If
<code>NULL</code>, all fonts are allowed. This directive
affects generic names (serif, sans-serif, monospace, cursive,
fantasy) as well as specific font families.
</p>
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,18 @@
CSS.AllowedProperties
TYPE: lookup/null
VERSION: 3.1.0
DEFAULT: NULL
--DESCRIPTION--
<p>
If HTML Purifier's style attributes set is unsatisfactory for your needs,
you can overload it with your own list of tags to allow. Note that this
method is subtractive: it does its job by taking away from HTML Purifier
usual feature set, so you cannot add an attribute that HTML Purifier never
supported in the first place.
</p>
<p>
<strong>Warning:</strong> If another directive conflicts with the
elements here, <em>that</em> directive will win and override.
</p>
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,11 @@
CSS.DefinitionRev
TYPE: int
VERSION: 2.0.0
DEFAULT: 1
--DESCRIPTION--
<p>
Revision identifier for your custom definition. See
%HTML.DefinitionRev for details.
</p>
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,13 @@
CSS.ForbiddenProperties
TYPE: lookup
VERSION: 4.2.0
DEFAULT: array()
--DESCRIPTION--
<p>
This is the logical inverse of %CSS.AllowedProperties, and it will
override that directive or any other directive. If possible,
%CSS.AllowedProperties is recommended over this directive,
because it can sometimes be difficult to tell whether or not you've
forbidden all of the CSS properties you truly would like to disallow.
</p>
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,16 @@
CSS.MaxImgLength
TYPE: string/null
DEFAULT: '1200px'
VERSION: 3.1.1
--DESCRIPTION--
<p>
This parameter sets the maximum allowed length on <code>img</code> tags,
effectively the <code>width</code> and <code>height</code> properties.
Only absolute units of measurement (in, pt, pc, mm, cm) and pixels (px) are allowed. This is
in place to prevent imagecrash attacks, disable with null at your own risk.
This directive is similar to %HTML.MaxImgLength, and both should be
concurrently edited, although there are
subtle differences in the input format (the CSS max is a number with
a unit).
</p>
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,10 @@
CSS.Proprietary
TYPE: bool
VERSION: 3.0.0
DEFAULT: false
--DESCRIPTION--
<p>
Whether or not to allow safe, proprietary CSS values.
</p>
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,9 @@
CSS.Trusted
TYPE: bool
VERSION: 4.2.1
DEFAULT: false
--DESCRIPTION--
Indicates whether or not the user's CSS input is trusted or not. If the
input is trusted, a more expansive set of allowed properties. See
also %HTML.Trusted.
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,14 @@
Cache.DefinitionImpl
TYPE: string/null
VERSION: 2.0.0
DEFAULT: 'Serializer'
--DESCRIPTION--
This directive defines which method to use when caching definitions,
the complex data-type that makes HTML Purifier tick. Set to null
to disable caching (not recommended, as you will see a definite
performance degradation).
--ALIASES--
Core.DefinitionCache
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,13 @@
Cache.SerializerPath
TYPE: string/null
VERSION: 2.0.0
DEFAULT: NULL
--DESCRIPTION--
<p>
Absolute path with no trailing slash to store serialized definitions in.
Default is within the
HTML Purifier library inside DefinitionCache/Serializer. This
path must be writable by the webserver.
</p>
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,11 @@
Cache.SerializerPermissions
TYPE: int
VERSION: 4.3.0
DEFAULT: 0755
--DESCRIPTION--
<p>
Directory permissions of the files and directories created inside
the DefinitionCache/Serializer or other custom serializer path.
</p>
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,18 @@
Core.AggressivelyFixLt
TYPE: bool
VERSION: 2.1.0
DEFAULT: true
--DESCRIPTION--
<p>
This directive enables aggressive pre-filter fixes HTML Purifier can
perform in order to ensure that open angled-brackets do not get killed
during parsing stage. Enabling this will result in two preg_replace_callback
calls and at least two preg_replace calls for every HTML document parsed;
if your users make very well-formed HTML, you can set this directive false.
This has no effect when DirectLex is used.
</p>
<p>
<strong>Notice:</strong> This directive's default turned from false to true
in HTML Purifier 3.2.0.
</p>
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,16 @@
Core.AllowHostnameUnderscore
TYPE: bool
VERSION: 4.6.0
DEFAULT: false
--DESCRIPTION--
<p>
By RFC 1123, underscores are not permitted in host names.
(This is in contrast to the specification for DNS, RFC
2181, which allows underscores.)
However, most browsers do the right thing when faced with
an underscore in the host name, and so some poorly written
websites are written with the expectation this should work.
Setting this parameter to true relaxes our allowed character
check so that underscores are permitted.
</p>
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,12 @@
Core.CollectErrors
TYPE: bool
VERSION: 2.0.0
DEFAULT: false
--DESCRIPTION--
Whether or not to collect errors found while filtering the document. This
is a useful way to give feedback to your users. <strong>Warning:</strong>
Currently this feature is very patchy and experimental, with lots of
possible error messages not yet implemented. It will not cause any
problems, but it may not help your users either.
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,29 @@
Core.ColorKeywords
TYPE: hash
VERSION: 2.0.0
--DEFAULT--
array (
'maroon' => '#800000',
'red' => '#FF0000',
'orange' => '#FFA500',
'yellow' => '#FFFF00',
'olive' => '#808000',
'purple' => '#800080',
'fuchsia' => '#FF00FF',
'white' => '#FFFFFF',
'lime' => '#00FF00',
'green' => '#008000',
'navy' => '#000080',
'blue' => '#0000FF',
'aqua' => '#00FFFF',
'teal' => '#008080',
'black' => '#000000',
'silver' => '#C0C0C0',
'gray' => '#808080',
)
--DESCRIPTION--
Lookup array of color names to six digit hexadecimal number corresponding
to color, with preceding hash mark. Used when parsing colors. The lookup
is done in a case-insensitive manner.
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,14 @@
Core.ConvertDocumentToFragment
TYPE: bool
DEFAULT: true
--DESCRIPTION--
This parameter determines whether or not the filter should convert
input that is a full document with html and body tags to a fragment
of just the contents of a body tag. This parameter is simply something
HTML Purifier can do during an edge-case: for most inputs, this
processing is not necessary.
--ALIASES--
Core.AcceptFullDocuments
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,17 @@
Core.DirectLexLineNumberSyncInterval
TYPE: int
VERSION: 2.0.0
DEFAULT: 0
--DESCRIPTION--
<p>
Specifies the number of tokens the DirectLex line number tracking
implementations should process before attempting to resyncronize the
current line count by manually counting all previous new-lines. When
at 0, this functionality is disabled. Lower values will decrease
performance, and this is only strictly necessary if the counting
algorithm is buggy (in which case you should report it as a bug).
This has no effect when %Core.MaintainLineNumbers is disabled or DirectLex is
not being used.
</p>
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,14 @@
Core.DisableExcludes
TYPE: bool
DEFAULT: false
VERSION: 4.5.0
--DESCRIPTION--
<p>
This directive disables SGML-style exclusions, e.g. the exclusion of
<code>&lt;object&gt;</code> in any descendant of a
<code>&lt;pre&gt;</code> tag. Disabling excludes will allow some
invalid documents to pass through HTML Purifier, but HTML Purifier
will also be less likely to accidentally remove large documents during
processing.
</p>
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,9 @@
Core.EnableIDNA
TYPE: bool
DEFAULT: false
VERSION: 4.4.0
--DESCRIPTION--
Allows international domain names in URLs. This configuration option
requires the PEAR Net_IDNA2 module to be installed. It operates by
punycoding any internationalized host names for maximum portability.
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,15 @@
Core.Encoding
TYPE: istring
DEFAULT: 'utf-8'
--DESCRIPTION--
If for some reason you are unable to convert all webpages to UTF-8, you can
use this directive as a stop-gap compatibility change to let HTML Purifier
deal with non UTF-8 input. This technique has notable deficiencies:
absolutely no characters outside of the selected character encoding will be
preserved, not even the ones that have been ampersand escaped (this is due
to a UTF-8 specific <em>feature</em> that automatically resolves all
entities), making it pretty useless for anything except the most I18N-blind
applications, although %Core.EscapeNonASCIICharacters offers fixes this
trouble with another tradeoff. This directive only accepts ISO-8859-1 if
iconv is not enabled.
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,12 @@
Core.EscapeInvalidChildren
TYPE: bool
DEFAULT: false
--DESCRIPTION--
<p><strong>Warning:</strong> this configuration option is no longer does anything as of 4.6.0.</p>
<p>When true, a child is found that is not allowed in the context of the
parent element will be transformed into text as if it were ASCII. When
false, that element and all internal tags will be dropped, though text will
be preserved. There is no option for dropping the element but preserving
child nodes.</p>
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,7 @@
Core.EscapeInvalidTags
TYPE: bool
DEFAULT: false
--DESCRIPTION--
When true, invalid tags will be written back to the document as plain text.
Otherwise, they are silently dropped.
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,13 @@
Core.EscapeNonASCIICharacters
TYPE: bool
VERSION: 1.4.0
DEFAULT: false
--DESCRIPTION--
This directive overcomes a deficiency in %Core.Encoding by blindly
converting all non-ASCII characters into decimal numeric entities before
converting it to its native encoding. This means that even characters that
can be expressed in the non-UTF-8 encoding will be entity-ized, which can
be a real downer for encodings like Big5. It also assumes that the ASCII
repetoire is available, although this is the case for almost all encodings.
Anyway, use UTF-8!
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,19 @@
Core.HiddenElements
TYPE: lookup
--DEFAULT--
array (
'script' => true,
'style' => true,
)
--DESCRIPTION--
<p>
This directive is a lookup array of elements which should have their
contents removed when they are not allowed by the HTML definition.
For example, the contents of a <code>script</code> tag are not
normally shown in a document, so if script tags are to be removed,
their contents should be removed to. This is opposed to a <code>b</code>
tag, which defines some presentational changes but does not hide its
contents.
</p>
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,10 @@
Core.Language
TYPE: string
VERSION: 2.0.0
DEFAULT: 'en'
--DESCRIPTION--
ISO 639 language code for localizable things in HTML Purifier to use,
which is mainly error reporting. There is currently only an English (en)
translation, so this directive is currently useless.
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,34 @@
Core.LexerImpl
TYPE: mixed/null
VERSION: 2.0.0
DEFAULT: NULL
--DESCRIPTION--
<p>
This parameter determines what lexer implementation can be used. The
valid values are:
</p>
<dl>
<dt><em>null</em></dt>
<dd>
Recommended, the lexer implementation will be auto-detected based on
your PHP-version and configuration.
</dd>
<dt><em>string</em> lexer identifier</dt>
<dd>
This is a slim way of manually overridding the implementation.
Currently recognized values are: DOMLex (the default PHP5
implementation)
and DirectLex (the default PHP4 implementation). Only use this if
you know what you are doing: usually, the auto-detection will
manage things for cases you aren't even aware of.
</dd>
<dt><em>object</em> lexer instance</dt>
<dd>
Super-advanced: you can specify your own, custom, implementation that
implements the interface defined by <code>HTMLPurifier_Lexer</code>.
I may remove this option simply because I don't expect anyone
to use it.
</dd>
</dl>
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,16 @@
Core.MaintainLineNumbers
TYPE: bool/null
VERSION: 2.0.0
DEFAULT: NULL
--DESCRIPTION--
<p>
If true, HTML Purifier will add line number information to all tokens.
This is useful when error reporting is turned on, but can result in
significant performance degradation and should not be used when
unnecessary. This directive must be used with the DirectLex lexer,
as the DOMLex lexer does not (yet) support this functionality.
If the value is null, an appropriate value will be selected based
on other configuration.
</p>
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,11 @@
Core.NormalizeNewlines
TYPE: bool
VERSION: 4.2.0
DEFAULT: true
--DESCRIPTION--
<p>
Whether or not to normalize newlines to the operating
system default. When <code>false</code>, HTML Purifier
will attempt to preserve mixed newline files.
</p>
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,12 @@
Core.RemoveInvalidImg
TYPE: bool
DEFAULT: true
VERSION: 1.3.0
--DESCRIPTION--
<p>
This directive enables pre-emptive URI checking in <code>img</code>
tags, as the attribute validation strategy is not authorized to
remove elements from the document. Revert to pre-1.3.0 behavior by setting to false.
</p>
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,11 @@
Core.RemoveProcessingInstructions
TYPE: bool
VERSION: 4.2.0
DEFAULT: false
--DESCRIPTION--
Instead of escaping processing instructions in the form <code>&lt;? ...
?&gt;</code>, remove it out-right. This may be useful if the HTML
you are validating contains XML processing instruction gunk, however,
it can also be user-unfriendly for people attempting to post PHP
snippets.
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,12 @@
Core.RemoveScriptContents
TYPE: bool/null
DEFAULT: NULL
VERSION: 2.0.0
DEPRECATED-VERSION: 2.1.0
DEPRECATED-USE: Core.HiddenElements
--DESCRIPTION--
<p>
This directive enables HTML Purifier to remove not only script tags
but all of their contents.
</p>
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,11 @@
Filter.Custom
TYPE: list
VERSION: 3.1.0
DEFAULT: array()
--DESCRIPTION--
<p>
This directive can be used to add custom filters; it is nearly the
equivalent of the now deprecated <code>HTMLPurifier-&gt;addFilter()</code>
method. Specify an array of concrete implementations.
</p>
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,14 @@
Filter.ExtractStyleBlocks.Escaping
TYPE: bool
VERSION: 3.0.0
DEFAULT: true
ALIASES: Filter.ExtractStyleBlocksEscaping, FilterParam.ExtractStyleBlocksEscaping
--DESCRIPTION--
<p>
Whether or not to escape the dangerous characters &lt;, &gt; and &amp;
as \3C, \3E and \26, respectively. This is can be safely set to false
if the contents of StyleBlocks will be placed in an external stylesheet,
where there is no risk of it being interpreted as HTML.
</p>
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,29 @@
Filter.ExtractStyleBlocks.Scope
TYPE: string/null
VERSION: 3.0.0
DEFAULT: NULL
ALIASES: Filter.ExtractStyleBlocksScope, FilterParam.ExtractStyleBlocksScope
--DESCRIPTION--
<p>
If you would like users to be able to define external stylesheets, but
only allow them to specify CSS declarations for a specific node and
prevent them from fiddling with other elements, use this directive.
It accepts any valid CSS selector, and will prepend this to any
CSS declaration extracted from the document. For example, if this
directive is set to <code>#user-content</code> and a user uses the
selector <code>a:hover</code>, the final selector will be
<code>#user-content a:hover</code>.
</p>
<p>
The comma shorthand may be used; consider the above example, with
<code>#user-content, #user-content2</code>, the final selector will
be <code>#user-content a:hover, #user-content2 a:hover</code>.
</p>
<p>
<strong>Warning:</strong> It is possible for users to bypass this measure
using a naughty + selector. This is a bug in CSS Tidy 1.3, not HTML
Purifier, and I am working to get it fixed. Until then, HTML Purifier
performs a basic check to prevent this.
</p>
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,16 @@
Filter.ExtractStyleBlocks.TidyImpl
TYPE: mixed/null
VERSION: 3.1.0
DEFAULT: NULL
ALIASES: FilterParam.ExtractStyleBlocksTidyImpl
--DESCRIPTION--
<p>
If left NULL, HTML Purifier will attempt to instantiate a <code>csstidy</code>
class to use for internal cleaning. This will usually be good enough.
</p>
<p>
However, for trusted user input, you can set this to <code>false</code> to
disable cleaning. In addition, you can supply your own concrete implementation
of Tidy's interface to use, although I don't know why you'd want to do that.
</p>
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,74 @@
Filter.ExtractStyleBlocks
TYPE: bool
VERSION: 3.1.0
DEFAULT: false
EXTERNAL: CSSTidy
--DESCRIPTION--
<p>
This directive turns on the style block extraction filter, which removes
<code>style</code> blocks from input HTML, cleans them up with CSSTidy,
and places them in the <code>StyleBlocks</code> context variable, for further
use by you, usually to be placed in an external stylesheet, or a
<code>style</code> block in the <code>head</code> of your document.
</p>
<p>
Sample usage:
</p>
<pre><![CDATA[
<?php
header('Content-type: text/html; charset=utf-8');
echo '<?xml version="1.0" encoding="UTF-8"?>';
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
<title>Filter.ExtractStyleBlocks</title>
<?php
require_once '/path/to/library/HTMLPurifier.auto.php';
require_once '/path/to/csstidy.class.php';
$dirty = '<style>body {color:#F00;}</style> Some text';
$config = HTMLPurifier_Config::createDefault();
$config->set('Filter', 'ExtractStyleBlocks', true);
$purifier = new HTMLPurifier($config);
$html = $purifier->purify($dirty);
// This implementation writes the stylesheets to the styles/ directory.
// You can also echo the styles inside the document, but it's a bit
// more difficult to make sure they get interpreted properly by
// browsers; try the usual CSS armoring techniques.
$styles = $purifier->context->get('StyleBlocks');
$dir = 'styles/';
if (!is_dir($dir)) mkdir($dir);
$hash = sha1($_GET['html']);
foreach ($styles as $i => $style) {
file_put_contents($name = $dir . $hash . "_$i");
echo '<link rel="stylesheet" type="text/css" href="'.$name.'" />';
}
?>
</head>
<body>
<div>
<?php echo $html; ?>
</div>
</b]]><![CDATA[ody>
</html>
]]></pre>
<p>
<strong>Warning:</strong> It is possible for a user to mount an
imagecrash attack using this CSS. Counter-measures are difficult;
it is not simply enough to limit the range of CSS lengths (using
relative lengths with many nesting levels allows for large values
to be attained without actually specifying them in the stylesheet),
and the flexible nature of selectors makes it difficult to selectively
disable lengths on image tags (HTML Purifier, however, does disable
CSS width and height in inline styling). There are probably two effective
counter measures: an explicit width and height set to auto in all
images in your document (unlikely) or the disabling of width and
height (somewhat reasonable). Whether or not these measures should be
used is left to the reader.
</p>
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,16 @@
Filter.YouTube
TYPE: bool
VERSION: 3.1.0
DEFAULT: false
--DESCRIPTION--
<p>
<strong>Warning:</strong> Deprecated in favor of %HTML.SafeObject and
%Output.FlashCompat (turn both on to allow YouTube videos and other
Flash content).
</p>
<p>
This directive enables YouTube video embedding in HTML Purifier. Check
<a href="http://htmlpurifier.org/docs/enduser-youtube.html">this document
on embedding videos</a> for more information on what this filter does.
</p>
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,25 @@
HTML.Allowed
TYPE: itext/null
VERSION: 2.0.0
DEFAULT: NULL
--DESCRIPTION--
<p>
This is a preferred convenience directive that combines
%HTML.AllowedElements and %HTML.AllowedAttributes.
Specify elements and attributes that are allowed using:
<code>element1[attr1|attr2],element2...</code>. For example,
if you would like to only allow paragraphs and links, specify
<code>a[href],p</code>. You can specify attributes that apply
to all elements using an asterisk, e.g. <code>*[lang]</code>.
You can also use newlines instead of commas to separate elements.
</p>
<p>
<strong>Warning</strong>:
All of the constraints on the component directives are still enforced.
The syntax is a <em>subset</em> of TinyMCE's <code>valid_elements</code>
whitelist: directly copy-pasting it here will probably result in
broken whitelists. If %HTML.AllowedElements or %HTML.AllowedAttributes
are set, this directive has no effect.
</p>
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,19 @@
HTML.AllowedAttributes
TYPE: lookup/null
VERSION: 1.3.0
DEFAULT: NULL
--DESCRIPTION--
<p>
If HTML Purifier's attribute set is unsatisfactory, overload it!
The syntax is "tag.attr" or "*.attr" for the global attributes
(style, id, class, dir, lang, xml:lang).
</p>
<p>
<strong>Warning:</strong> If another directive conflicts with the
elements here, <em>that</em> directive will win and override. For
example, %HTML.EnableAttrID will take precedence over *.id in this
directive. You must set that directive to true before you can use
IDs at all.
</p>
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,10 @@
HTML.AllowedComments
TYPE: lookup
VERSION: 4.4.0
DEFAULT: array()
--DESCRIPTION--
A whitelist which indicates what explicit comment bodies should be
allowed, modulo leading and trailing whitespace. See also %HTML.AllowedCommentsRegexp
(these directives are union'ed together, so a comment is considered
valid if any directive deems it valid.)
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,15 @@
HTML.AllowedCommentsRegexp
TYPE: string/null
VERSION: 4.4.0
DEFAULT: NULL
--DESCRIPTION--
A regexp, which if it matches the body of a comment, indicates that
it should be allowed. Trailing and leading spaces are removed prior
to running this regular expression.
<strong>Warning:</strong> Make sure you specify
correct anchor metacharacters <code>^regex$</code>, otherwise you may accept
comments that you did not mean to! In particular, the regex <code>/foo|bar/</code>
is probably not sufficiently strict, since it also allows <code>foobar</code>.
See also %HTML.AllowedComments (these directives are union'ed together,
so a comment is considered valid if any directive deems it valid.)
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,23 @@
HTML.AllowedElements
TYPE: lookup/null
VERSION: 1.3.0
DEFAULT: NULL
--DESCRIPTION--
<p>
If HTML Purifier's tag set is unsatisfactory for your needs, you can
overload it with your own list of tags to allow. If you change
this, you probably also want to change %HTML.AllowedAttributes; see
also %HTML.Allowed which lets you set allowed elements and
attributes at the same time.
</p>
<p>
If you attempt to allow an element that HTML Purifier does not know
about, HTML Purifier will raise an error. You will need to manually
tell HTML Purifier about this element by using the
<a href="http://htmlpurifier.org/docs/enduser-customize.html">advanced customization features.</a>
</p>
<p>
<strong>Warning:</strong> If another directive conflicts with the
elements here, <em>that</em> directive will win and override.
</p>
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,20 @@
HTML.AllowedModules
TYPE: lookup/null
VERSION: 2.0.0
DEFAULT: NULL
--DESCRIPTION--
<p>
A doctype comes with a set of usual modules to use. Without having
to mucking about with the doctypes, you can quickly activate or
disable these modules by specifying which modules you wish to allow
with this directive. This is most useful for unit testing specific
modules, although end users may find it useful for their own ends.
</p>
<p>
If you specify a module that does not exist, the manager will silently
fail to use it, so be careful! User-defined modules are not affected
by this directive. Modules defined in %HTML.CoreModules are not
affected by this directive.
</p>
--# vim: et sw=4 sts=4

View file

@ -0,0 +1,11 @@
HTML.Attr.Name.UseCDATA
TYPE: bool
DEFAULT: false
VERSION: 4.0.0
--DESCRIPTION--
The W3C specification DTD defines the name attribute to be CDATA, not ID, due
to limitations of DTD. In certain documents, this relaxed behavior is desired,
whether it is to specify duplicate names, or to specify names that would be
illegal IDs (for example, names that begin with a digit.) Set this configuration
directive to true to use the relaxed parsing rules.
--# vim: et sw=4 sts=4

Some files were not shown because too many files have changed in this diff Show more