init 4chan
This commit is contained in:
commit
c850337f1e
390 changed files with 195936 additions and 0 deletions
201
lib/GoogleAuthenticator.php
Normal file
201
lib/GoogleAuthenticator.php
Normal 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
612
lib/admin-test.php
Normal 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
603
lib/admin.php
Normal 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
155
lib/ads-test.php
Normal 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
145
lib/ads.php
Normal 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
171
lib/archives.php
Normal 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
594
lib/auth-test.php
Normal 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
594
lib/auth.php
Normal 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
132
lib/board_flags_lgbt.php
Normal 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
102
lib/board_flags_mlp.php
Normal 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
72
lib/board_flags_pol.php
Normal 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
26
lib/board_flags_test.php
Normal 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
686
lib/captcha-test.php
Normal 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&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&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
686
lib/captcha.php
Normal 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&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&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
345
lib/db.php
Normal 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
308
lib/db_pdo.php
Normal 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
115
lib/geoip2-test.php
Normal 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
115
lib/geoip2.php
Normal 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
6
lib/global_constants.php
Normal 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';
|
21873
lib/htmlpurifier/HTMLPurifier.standalone.php
Normal file
21873
lib/htmlpurifier/HTMLPurifier.standalone.php
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Exceptions related to configuration schema
|
||||
*/
|
||||
class HTMLPurifier_ConfigSchema_Exception extends HTMLPurifier_Exception
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
// vim: et sw=4 sts=4
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
BIN
lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema.ser
Normal file
BIN
lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema.ser
Normal file
Binary file not shown.
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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 <a> 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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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><a></a></code> but not
|
||||
<code><br /></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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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><object></code> in any descendant of a
|
||||
<code><pre></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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,11 @@
|
|||
Core.RemoveProcessingInstructions
|
||||
TYPE: bool
|
||||
VERSION: 4.2.0
|
||||
DEFAULT: false
|
||||
--DESCRIPTION--
|
||||
Instead of escaping processing instructions in the form <code><? ...
|
||||
?></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
|
|
@ -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
|
|
@ -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->addFilter()</code>
|
||||
method. Specify an array of concrete implementations.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
|
@ -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 <, > and &
|
||||
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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue