4chan/team/admin/create4chanpass.php
2025-04-17 18:12:08 -05:00

377 lines
9 KiB
PHP

<?php
require_once '../lib/sec.php';
require_once 'lib/admin.php';
require_once 'lib/auth.php';
define('IN_APP', true);
auth_user();
if (!has_level('admin') && !has_flag('developer')) {
APP::denied();
}
/*
if (has_flag('developer')) {
$mysql_suppress_err = false;
ini_set('display_errors', 1);
error_reporting(E_ALL);
}
*/
require_once '../lib/csp.php';
class App {
protected
// Routes
$actions = array(
'index',
'create',
'batches',
'create_batch',
'export_batch'/*,
'create_table'*/
)
;
const MAX_BATCH_SIZE = 5000;
const DATE_FORMAT ='m/d/y H:i';
const EXEC_LIMIT = 300;
const NORMAL_STATUS = 6;
const PRE_STATUS = 7;
const TPL_ROOT = '../views/';
static public function denied() {
require_once(self::TPL_ROOT . 'denied.tpl.php');
die();
}
final protected function success($redirect = null, $no_exit = false) {
$this->redirect = $redirect;
$this->renderHTML('success');
if (!$no_exit) {
die();
}
}
final protected function error($msg) {
$this->message = $msg;
$this->renderHTML('error');
die();
}
/*
public function create_table() {
$query = <<<SQL
CREATE TABLE IF NOT EXISTS `pass_batches` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`batch_id` varchar(255) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`size` int(10) unsigned NOT NULL,
`created_on` int(10) unsigned NOT NULL,
`created_by` varchar(255) NOT NULL,
`description` varchar(512) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
SQL;
mysql_global_call($query);
echo 'Done';
}
*/
/**
* Renders HTML template
*/
private function renderHTML($view) {
require_once(self::TPL_ROOT . $view . '.tpl.php');
}
private function getRandomHexBytes($length = 10) {
$bytes = openssl_random_pseudo_bytes($length);
return bin2hex($bytes);
}
/**
* Index
*/
public function index() {
$this->renderHTML('create4chanpass');
}
/**
* Create 4chan Pass
*/
public function create() {
$fields = array('email' => 'E-mail',
'transaction' => 'Transaction ID', 'otp' => '2FA OTP');
foreach ($fields as $field => $label) {
if (!isset($_POST[$field]) || $_POST[$field] == '') {
$this->error($label . ' cannot be empty.');
}
}
// OTP
if (!verify_one_time_pwd($_COOKIE['4chan_auser'], $_POST['otp'])) {
$this->error('Invalid or expired OTP.');
}
// E-mails
$email = $_POST['email'];
if (!preg_match('/^[^ ]+@[^ ]+/', $email)) {
$this->error('Invalid E-mail');
}
if (isset($_POST['gift_email']) && $_POST['gift_email'] != '') {
$gift_email = $_POST['gift_email'];
if (!preg_match('/^[^ ]+@[^ ]+/', $gift_email)) {
$this->error('Invalid Gift E-mail');
}
$is_gift = true;
}
else {
$gift_email = '';
$is_gift = false;
}
// Transaction
if (!isset($_POST['transaction']) || $_POST['transaction'] === '') {
$this->error('Transaction cannot be empty');
}
$transaction = $_POST['transaction'];
// Price paid in cents
if (isset($_POST['price_paid']) && $_POST['price_paid'] !== '') {
if (!preg_match('/^[0-9]+$/', $_POST['price_paid'])) {
$this->error('Invalid price paid');
}
$price_paid = (int)$_POST['price_paid'];
}
else {
$price_paid = 0;
}
$price_paid_dollars = round($price_paid / 100.0, 2);
if (isset($_POST['preloaded']) && $_POST['preloaded']) {
$status = self::PRE_STATUS;
}
else {
$status = self::NORMAL_STATUS;
}
// ---
$pending_id = $this->getRandomHexBytes(16);
$user_hash = $this->generateUserHash();
$plain_pin = $this->generatePin();
$pin = crypt($plain_pin, substr($user_hash, 4, 9));
$query = <<<SQL
INSERT INTO pass_users(user_hash, transaction_id, pin, purchase_date, status,
last_ip, last_used, email, pending_id, expiration_date, email_sent, gift_email,
price_paid)
VALUES ('%s', '%s', '%s', NOW(), '$status', '0.0.0.0', NOW(), '%s', '%s',
NOW() + INTERVAL 1 YEAR, 1, '%s', %d)
SQL;
$res = mysql_global_call($query,
$user_hash, $transaction, $plain_pin, $email, $pending_id, $gift_email,
$price_paid
);
if (!$res) {
$this->error('Database Error.');
}
$expiry_str = date('m/d/y', strtotime('+1 year'));
require_once('/www/global/yotsuba/payments/stripe_web_request.php');
define('STRIPE_EMAIL_AMOUNT_IN_DOLLARS', $price_paid_dollars);
send_welcome_email($user_hash, $plain_pin, $expiry_str, $email, $gift_email, $is_gift, false);
$this->user_hash = $user_hash;
$this->plain_pin = $plain_pin;
$this->price_paid_dollars = $price_paid_dollars;
$this->renderHTML('create4chanpass');
}
private function generateUserHash() {
return substr(strtoupper(str_shuffle( 'abcdefghijklmnopqrstuvqxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567901234567890')), 0, 10);
}
private function generatePin() {
return mt_rand(100000, 999999);
}
public function batches() {
$query = "SELECT * FROM pass_batches ORDER BY id DESC";
$res = mysql_global_call($query);
if (!$res) {
$this->error('Database Error');
}
$this->batches = array();
while ($row = mysql_fetch_assoc($res)) {
$this->batches[] = $row;
}
$this->renderHTML('create4chanpass-batch');
}
public function create_batch() {
// OTP
if (!verify_one_time_pwd($_COOKIE['4chan_auser'], $_POST['otp'])) {
$this->error('Invalid or expired OTP.');
}
$batch_size = (int)$_POST['batch_size'];
if ($batch_size < 1 || $batch_size > self::MAX_BATCH_SIZE) {
$this->error('Invalid Batch Size');
}
if (isset($_POST['description'])) {
$description = htmlspecialchars($_POST['description'], ENT_QUOTES);
}
else {
$description = '';
}
$created_on = time();
$created_by = htmlspecialchars($_COOKIE['4chan_auser'], ENT_QUOTES);
$batch_id = 'batch_' . $this->getRandomHexBytes(8);
$sql =<<<SQL
INSERT INTO `pass_batches` (batch_id, size, description, created_on, created_by)
VALUES ('%s', %d, '%s', %d, '%s')
SQL;
$res = mysql_global_call($sql,
$batch_id, $batch_size, $description, $created_on, $created_by
);
if (!$res) {
$this->error('Database error (1)');
}
$status = self::PRE_STATUS;
set_time_limit(self::EXEC_LIMIT);
for ($i = 0; $i < $batch_size; ++$i) {
$pending_id = $this->getRandomHexBytes(16);
$user_hash = $this->generateUserHash();
$plain_pin = $this->generatePin();
$pin = crypt($plain_pin, substr($user_hash, 4, 9));
if (!$pending_id) {
$this->error('Internal Server Error (1)');
}
$transaction = $batch_id . '_' . $pending_id;
$query = <<<SQL
INSERT INTO pass_users(user_hash, transaction_id, pin, purchase_date, status,
last_ip, last_used, email, pending_id, expiration_date, email_sent, gift_email,
price_paid, registration_country)
VALUES ('%s', '%s', '%s', NOW(), $status, '0.0.0.0', NOW(), '', '%s',
NOW() + INTERVAL 1 YEAR, 1, '', 0, 'XX')
SQL;
$res = mysql_global_call($query, $user_hash, $transaction, $plain_pin, $pending_id);
if (!$res) {
$this->error('Database Error (2)');
}
}
$this->success('?action=batches');
}
function export_batch() {
$id = (int)$_GET['id'];
if (!$id) {
$this->error('Batch Not Found.');
}
$query = "SELECT batch_id FROM pass_batches WHERE id = $id";
$res = mysql_global_call($query);
if (!$res) {
$this->error('Database Error (1)');
}
$batch_id = mysql_fetch_row($res)[0];
if (!$batch_id) {
$this->error('Batch Not Found.');
}
$esc_batch_id = mysql_real_escape_string($batch_id);
if (!$esc_batch_id) {
$this->error('Database Error (2)');
}
$esc_batch_id = str_replace('_', "\_", $esc_batch_id . '_');
$query = <<<SQL
SELECT user_hash, pin FROM pass_users
WHERE status = %d AND transaction_id LIKE '{$esc_batch_id}%%'
SQL;
$res = mysql_global_call($query, self::PRE_STATUS);
header('Content-disposition: attachment; filename=passes-' . $batch_id . '.txt');
header('Content-Type: application/octet-stream');
while($row = mysql_fetch_assoc($res)) {
echo "{$row['user_hash']}\t{$row['pin']}\n";
}
}
/**
* Main
*/
public function run() {
$method = $_SERVER['REQUEST_METHOD'] === 'POST' ? $_POST : $_GET;
if (isset($method['action'])) {
$action = $method['action'];
}
else {
$action = 'index';
}
if (in_array($action, $this->actions)) {
$this->$action();
}
else {
$this->error('Bad request');
}
}
}
$ctrl = new App();
$ctrl->run();