1209 lines
30 KiB
PHP
1209 lines
30 KiB
PHP
<?php
|
|
require_once 'lib/sec.php';
|
|
|
|
require_once 'lib/admin.php';
|
|
require_once 'lib/auth.php';
|
|
|
|
require_once 'lib/geoip2.php';
|
|
|
|
define('IN_APP', true);
|
|
|
|
auth_user();
|
|
|
|
if (!has_level()) {
|
|
APP::denied();
|
|
}
|
|
/*
|
|
if (!has_flag('developer')) {
|
|
die('503');
|
|
}
|
|
*/
|
|
require_once 'lib/csp.php';
|
|
|
|
class APP {
|
|
protected
|
|
// Routes
|
|
$actions = array(
|
|
'index',
|
|
'rate',
|
|
'stats',
|
|
'review',
|
|
'accept',
|
|
'reject',
|
|
'send_orientation',
|
|
'create_account'/*,
|
|
'create_table'*/
|
|
),
|
|
// Table name
|
|
$tableName = 'janitor_apps',
|
|
// Maximum score
|
|
$maxScore = 5,
|
|
// Number of entries to display per page (reviews)
|
|
$pageSize = 25,
|
|
// Number of votes before the application is counted as "rated"
|
|
$voteCountThreshold = 5;
|
|
|
|
const TPL_ROOT = 'views/';
|
|
|
|
const REVIEW_OPEN = true;
|
|
|
|
const
|
|
OPEN = 0,
|
|
REJECTED = 1,
|
|
ACCEPTED = 2,
|
|
SIGNED = 4,
|
|
ORIENTED = 3,
|
|
CLOSED = 8,
|
|
IGNORED = 9;
|
|
|
|
const
|
|
MAIL_ACCEPT_FILE = './data/mail_janitor_accept.txt',
|
|
MAIL_ORIENTATION_FILE = './data/mail_janitor_orientation.txt',
|
|
MAIL_ACCOUNT_FILE = './data/mail_janitor_account.txt';
|
|
|
|
const
|
|
ADMIN_SALT_PATH = '/www/keys/2014_admin.salt';
|
|
|
|
const
|
|
HTPASSWD_CMD = '/usr/local/www/bin/htpasswd -Bb -C 10 %s %s %s',
|
|
HTPASSWD_CMD_NGINX = '/usr/local/www/bin/htpasswd -b %s %s %s',
|
|
HTPASSWD_RM_CMD_NGINX = '/usr/local/www/bin/htpasswd -D %s %s',
|
|
JANITOR_HTPASSWD_TMP = '/www/global/htpasswd/temp_agreement_nginx',
|
|
JANITOR_HTPASSWD = '/www/global/htpasswd/janitors',
|
|
JANITOR_HTPASSWD_NGINX = '/www/global/htpasswd/janitors_nginx';
|
|
|
|
const
|
|
STR_Q1 = "Describe your expertise in the board's subject matter.",
|
|
STR_Q2 = "What are the main problems facing the board?",
|
|
STR_Q3 = "What is your favorite thing about the board?",
|
|
STR_Q4 = "What makes you a particularly good applicant for the team?"
|
|
;
|
|
|
|
static public function denied() {
|
|
require_once(self::TPL_ROOT . 'denied.tpl.php');
|
|
die();
|
|
}
|
|
|
|
/**
|
|
* Returns the data as json
|
|
*/
|
|
final protected function success($data = null) {
|
|
$this->renderJSON(array('status' => 'success', 'data' => $data));
|
|
}
|
|
|
|
/**
|
|
* Returns the error as json and exits
|
|
*/
|
|
final protected function error($message, $code = null, $data = null) {
|
|
$payload = array('status' => 'error', 'message' => $message);
|
|
|
|
if ($code) {
|
|
$payload['code'] = $code;
|
|
}
|
|
|
|
if ($data) {
|
|
$payload['data'] = $data;
|
|
}
|
|
|
|
$this->renderJSON($payload, 'error');
|
|
|
|
die();
|
|
}
|
|
|
|
final protected function errorHTML($msg) {
|
|
$this->message = $msg;
|
|
$this->renderHTML('error');
|
|
die();
|
|
}
|
|
|
|
/**
|
|
* Returns a JSON response
|
|
*/
|
|
private function renderJSON($data) {
|
|
header('Content-type: application/json');
|
|
echo json_encode($data);
|
|
}
|
|
|
|
/**
|
|
* Renders HTML template
|
|
*/
|
|
private function renderHTML($view) {
|
|
include(self::TPL_ROOT . $view . '.tpl.php');
|
|
}
|
|
|
|
private function getRandomHexBytes($length = 10) {
|
|
$bytes = openssl_random_pseudo_bytes($length);
|
|
return bin2hex($bytes);
|
|
}
|
|
|
|
private function sortByVoteCountDesc($a, $b) {
|
|
$a = $a[0];
|
|
$b = $b[0];
|
|
|
|
if ($a === $b) {
|
|
return 0;
|
|
}
|
|
|
|
return ($a > $b) ? -1 : 1;
|
|
}
|
|
|
|
private function removeTempAccount($hashed_auth_key) {
|
|
$cmd = sprintf(self::HTPASSWD_RM_CMD_NGINX,
|
|
self::JANITOR_HTPASSWD_TMP,
|
|
escapeshellarg($hashed_auth_key)
|
|
);
|
|
|
|
if (system($cmd) === false) {
|
|
$this->error('Internal Server Error (rta1).');
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private function getStatusCounts() {
|
|
$query = "SELECT closed, COUNT(*) as cnt FROM `{$this->tableName}` GROUP BY closed";
|
|
|
|
$result = mysql_global_call($query);
|
|
|
|
$data = [];
|
|
|
|
while ($row = mysql_fetch_assoc($result)) {
|
|
$data[$row['closed']] = (int)$row['cnt'];
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Email
|
|
*/
|
|
private function sendMail($email, $mail_file, $values = null) {
|
|
if (!file_exists($mail_file)) {
|
|
$this->error('Cannot find e-mail file.');
|
|
}
|
|
|
|
$lines = file($mail_file);
|
|
|
|
$subject = trim(array_shift($lines));
|
|
$message = implode('', $lines);
|
|
|
|
if ($values) {
|
|
$message = str_replace(array_keys($values), array_values($values), $message);
|
|
}
|
|
|
|
$headers = "From: Team 4chan <janitorapps@4chan.org>\r\n";
|
|
$headers .= "MIME-Version: 1.0\r\n";
|
|
$headers .= "Content-Type: text/plain; charset=UTF-8\r\n";
|
|
|
|
$opts = '-f janitorapps@4chan.org';
|
|
|
|
return mail($email, $subject, $message, $headers, $opts);
|
|
}
|
|
|
|
/**
|
|
* Changes application status (accepted/rejected)
|
|
*/
|
|
private function changeStatus($id, $status) {
|
|
$id = (int)$id;
|
|
$status = (int)$status;
|
|
|
|
$query = "SELECT closed FROM `{$this->tableName}` WHERE id = $id";
|
|
|
|
$result = mysql_global_call($query);
|
|
|
|
$app = mysql_fetch_assoc($result);
|
|
|
|
if (!$app) {
|
|
$this->error('Bad ID.');
|
|
}
|
|
|
|
$query = "UPDATE `{$this->tableName}` SET closed = $status WHERE id = $id LIMIT 1";
|
|
|
|
$result = mysql_global_call($query);
|
|
|
|
if (!$result) {
|
|
$this->error("Couldn't change the status.");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates temporary credentials to access /agreement
|
|
*/
|
|
private function createTemporaryCreds($id) {
|
|
$id = (int)$id;
|
|
|
|
$auth_key = $this->getRandomHexBytes(32);
|
|
|
|
if (!$auth_key) {
|
|
$this->error('Internal Server Error (ctc1)');
|
|
}
|
|
|
|
$http_passwd = $this->getRandomHexBytes(32);
|
|
|
|
if (!$http_passwd) {
|
|
$this->error('Internal Server Error (ctc1)');
|
|
}
|
|
|
|
// ---
|
|
|
|
$admin_salt = file_get_contents(self::ADMIN_SALT_PATH);
|
|
|
|
if (!$admin_salt) {
|
|
$this->error('Internal Server Error (ctc2)');
|
|
}
|
|
|
|
$hashed_auth_key = hash('sha256', $auth_key . $admin_salt);
|
|
|
|
$query = "SELECT id FROM `{$this->tableName}` WHERE agreement_key = '%s'";
|
|
$result = mysql_global_call($query, $hashed_auth_key);
|
|
|
|
if (!$result) {
|
|
$this->error("Database error (ctc0).");
|
|
}
|
|
|
|
if (mysql_num_rows($result) !== 0) {
|
|
$this->error('Internal Server Error (ctc3)');
|
|
}
|
|
|
|
// Delete existing account
|
|
$query = "SELECT agreement_key FROM `{$this->tableName}` WHERE id = $id";
|
|
$result = mysql_global_call($query);
|
|
|
|
if (!$result) {
|
|
$this->error("Database error (ctc1).");
|
|
}
|
|
|
|
if (mysql_num_rows($result) > 0) {
|
|
$hashed_old_key = mysql_fetch_row($result)[0];
|
|
$this->removeTempAccount($hashed_old_key);
|
|
}
|
|
|
|
// Update the janitor application with the new temp auth key
|
|
$query = "UPDATE `{$this->tableName}` SET agreement_key = '%s', key_created_on = %d WHERE id = $id LIMIT 1";
|
|
$result = mysql_global_call($query, $hashed_auth_key, time());
|
|
|
|
if (!$result) {
|
|
$this->error("Database error (ctc2).");
|
|
}
|
|
|
|
if (mysql_affected_rows() !== 1) {
|
|
$this->error("Database error (ctc3).");
|
|
}
|
|
|
|
// Update the htpasswd file
|
|
$cmd = sprintf(self::HTPASSWD_CMD_NGINX,
|
|
self::JANITOR_HTPASSWD_TMP,
|
|
escapeshellarg($hashed_auth_key),
|
|
escapeshellarg($http_passwd)
|
|
);
|
|
|
|
if (system($cmd) === false) {
|
|
$this->error('Could not update htpasswd file (ctc1).');
|
|
}
|
|
|
|
return array('login' => $hashed_auth_key, 'pwd' => $http_passwd, 'key' => $auth_key);
|
|
}
|
|
|
|
/**
|
|
* Get boards
|
|
*/
|
|
private function getBoards() {
|
|
$query = 'SELECT dir FROM boardlist';
|
|
|
|
$result = mysql_global_call($query);
|
|
|
|
if (!$result) {
|
|
die('Error while getting the boardlist');
|
|
}
|
|
|
|
$boards = array();
|
|
|
|
while ($board = mysql_fetch_assoc($result)) {
|
|
$boards[] = $board['dir'];
|
|
}
|
|
|
|
return $boards;
|
|
}
|
|
|
|
/**
|
|
* Create table
|
|
*/
|
|
public function create_table() {
|
|
$tableName = $this->tableName;
|
|
|
|
$sql = "DROP TABLE `$tableName`";
|
|
mysql_global_call($sql);
|
|
|
|
$sql =<<<SQL
|
|
CREATE TABLE IF NOT EXISTS `$tableName` (
|
|
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
|
`firstname` varchar(255) CHARACTER SET utf8 NOT NULL,
|
|
`handle` varchar(255) CHARACTER SET utf8 NOT NULL,
|
|
`email` varchar(255) CHARACTER SET utf8 NOT NULL,
|
|
`age` tinyint(2) unsigned NOT NULL,
|
|
`tz` tinyint(3) NOT NULL,
|
|
`hours` tinyint(2) unsigned NOT NULL,
|
|
`times` varchar(50) NOT NULL,
|
|
`board1` char(6) NOT NULL,
|
|
`board2` char(6) NOT NULL,
|
|
`experience` text CHARACTER SET utf8 NOT NULL,
|
|
`why` text CHARACTER SET utf8 NOT NULL,
|
|
`what` text CHARACTER SET utf8 NOT NULL,
|
|
`ip` char(15) NOT NULL,
|
|
`updated` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
`closed` tinyint(1) unsigned NOT NULL,
|
|
`total` tinyint(2) unsigned NOT NULL DEFAULT '0',
|
|
`votes` tinyint(2) unsigned NOT NULL DEFAULT '0',
|
|
`avg` float(4,2) unsigned NOT NULL DEFAULT '0.00',
|
|
`voters` varchar(255) DEFAULT '',
|
|
`agreement_key` varchar(255) CHARACTER SET ascii COLLATE ascii_bin NOT NULL DEFAULT '',
|
|
`key_created_on` int(10) unsigned NOT NULL DEFAULT '0',
|
|
PRIMARY KEY (`id`),
|
|
UNIQUE (`email`),
|
|
KEY `board1_idx` (`board1`),
|
|
KEY `board2_idx` (`board2`),
|
|
KEY `votes_idx` (`votes`),
|
|
KEY `avg_idx` (`avg`),
|
|
KEY `voters_idx` (`voters`),
|
|
KEY `closed_idx` (`closed`),
|
|
KEY `agreement_key` (`agreement_key`)
|
|
) ENGINE=MyISAM DEFAULT CHARSET=latin1
|
|
SQL;
|
|
|
|
/* mysql_global_call($sql);
|
|
|
|
$sql =<<<SQL
|
|
INSERT INTO `$tableName` (firstname, handle, email, age, tz, hours, times,
|
|
board1, board2, experience, why, what, ip, closed)
|
|
VALUES('John', 'janny', 'moot@4chan.org',
|
|
12, 1, 19, '7am-2am', 'po', '', 'None', 'N/A',
|
|
'N/A', '127.0.0.1', 0)
|
|
SQL;
|
|
|
|
mysql_global_call($sql);
|
|
|
|
$sql =<<<SQL
|
|
INSERT INTO `$tableName` (firstname, handle, email, age, tz, hours, times,
|
|
board1, board2, experience, why, what, ip, closed)
|
|
VALUES('John', 'janny', 'desuwa@4chan.org',
|
|
12, 1, 19, '7am-2am', 'po', '', 'None', 'N/A',
|
|
'N/A', '127.0.0.1', 0)
|
|
SQL; */
|
|
|
|
mysql_global_call($sql);
|
|
}
|
|
|
|
private function validate_otp($otp) {
|
|
$query = "SELECT auth_secret FROM mod_users WHERE username = '%s'";
|
|
|
|
$res = mysql_global_call($query, $_COOKIE['4chan_auser']);
|
|
|
|
if (!$res) {
|
|
$this->error('Database error (vO)');
|
|
}
|
|
|
|
$user = mysql_fetch_assoc($res);
|
|
|
|
if (!$user || !$user['auth_secret']) {
|
|
$this->error('Invalid OTP.');
|
|
}
|
|
|
|
require_once 'lib/GoogleAuthenticator.php';
|
|
|
|
$ga = new PHPGangsta_GoogleAuthenticator();
|
|
|
|
$dec_secret = auth_decrypt($user['auth_secret']);
|
|
|
|
if ($dec_secret === false) {
|
|
$this->error('Internal Server Error (vO).');
|
|
}
|
|
|
|
if (!$ga->verifyCode($dec_secret, $otp, 2)) {
|
|
$this->error('Invalid OTP.');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stats and counters
|
|
*/
|
|
public function stats() {
|
|
if (!has_level('manager') && !has_flag('developer')) {
|
|
$this->error('Bad request');
|
|
}
|
|
|
|
// ------
|
|
// Applications
|
|
// ------
|
|
$totals = array();
|
|
|
|
// Total submitted
|
|
$query = "SELECT COUNT(id) FROM `{$this->tableName}`";
|
|
$res = mysql_global_call($query);
|
|
$totals['total'] = mysql_fetch_row($res)[0];
|
|
|
|
// Unrated
|
|
$query = "SELECT COUNT(id) FROM `{$this->tableName}` WHERE closed = "
|
|
. self::OPEN . ' AND votes < ' . $this->voteCountThreshold;
|
|
$res = mysql_global_call($query);
|
|
$totals['open'] = mysql_fetch_row($res)[0];
|
|
|
|
// Auto-Ignored
|
|
$query = "SELECT COUNT(id) FROM `{$this->tableName}` WHERE closed = " . self::IGNORED;
|
|
$res = mysql_global_call($query);
|
|
$totals['ignored'] = mysql_fetch_row($res)[0];
|
|
|
|
// Accepted
|
|
$query = "SELECT COUNT(id) FROM `{$this->tableName}` WHERE closed = " . self::ACCEPTED;
|
|
$res = mysql_global_call($query);
|
|
$totals['accepted'] = mysql_fetch_row($res)[0];
|
|
|
|
// Oriented
|
|
$query = "SELECT COUNT(id) FROM `{$this->tableName}` WHERE closed = " . self::ORIENTED;
|
|
$res = mysql_global_call($query);
|
|
$totals['oriented'] = mysql_fetch_row($res)[0];
|
|
|
|
// Completed
|
|
$query = "SELECT COUNT(id) FROM `{$this->tableName}` WHERE closed = " . self::CLOSED;
|
|
$res = mysql_global_call($query);
|
|
$totals['completed'] = mysql_fetch_row($res)[0];
|
|
|
|
// ------
|
|
// Boards
|
|
// ------
|
|
$boards = array();
|
|
|
|
// As first choice
|
|
$query = "SELECT board1, COUNT(id) as cnt FROM `{$this->tableName}` WHERE closed != "
|
|
. self::IGNORED . ' GROUP BY board1';
|
|
$res = mysql_global_call($query);
|
|
while ($row = mysql_fetch_assoc($res)) {
|
|
if ($row['board1']) {
|
|
$boards[$row['board1']] = array((int)$row['cnt'], 0);
|
|
}
|
|
}
|
|
|
|
// As second choice
|
|
$query = "SELECT board2, COUNT(*) as cnt FROM `{$this->tableName}` WHERE closed != "
|
|
. self::IGNORED . ' GROUP BY board2';
|
|
$res = mysql_global_call($query);
|
|
while ($row = mysql_fetch_assoc($res)) {
|
|
if ($row['board2']) {
|
|
$boards[$row['board2']][1] = (int)$row['cnt'];
|
|
}
|
|
}
|
|
|
|
// ------
|
|
// Mod activity
|
|
// ------
|
|
$users = array();
|
|
|
|
$query = "SELECT username FROM mod_users WHERE level != 'janitor'";
|
|
$res = mysql_global_call($query);
|
|
while ($row = mysql_fetch_row($res)) {
|
|
$users[$row[0]] = array(0, 0);
|
|
}
|
|
|
|
$query = "SELECT voters FROM `{$this->tableName}`";
|
|
$res = mysql_global_call($query);
|
|
while ($row = mysql_fetch_row($res)) {
|
|
$voters = $row[0];
|
|
|
|
if ($voters === '') {
|
|
continue;
|
|
}
|
|
|
|
preg_match_all('/([^<]+)<([0-9])>/', $voters, $matches, PREG_SET_ORDER);
|
|
|
|
foreach ($matches as $m) {
|
|
$username = $m[1];
|
|
$score = (int)$m[2];
|
|
|
|
++$users[$username][0];
|
|
$users[$username][1] += $score;
|
|
}
|
|
}
|
|
|
|
uasort($users, array($this, 'sortByVoteCountDesc'));
|
|
|
|
// -----
|
|
|
|
$this->totals = $totals;
|
|
$this->boards = $boards;
|
|
$this->users = $users;
|
|
|
|
$this->renderHTML('janitorapps-stats');
|
|
}
|
|
|
|
/**
|
|
* Default page
|
|
*/
|
|
public function index() {
|
|
if (!self::REVIEW_OPEN) {
|
|
$this->errorHTML("Applications aren't closed yet");
|
|
}
|
|
|
|
$username = mysql_real_escape_string($_COOKIE['4chan_auser']);
|
|
|
|
$this->is_supadmin = $_COOKIE['4chan_auser'] === 'hiro';
|
|
|
|
$status = self::OPEN;
|
|
|
|
$this->boards = $this->getBoards();
|
|
|
|
if (isset($_GET['board']) && in_array($_GET['board'], $this->boards)) {
|
|
$board = mysql_real_escape_string($_GET['board']);
|
|
|
|
$this->filterBoard = htmlspecialchars($board, ENT_QUOTES);
|
|
|
|
$board = "AND board1 = '$board'";
|
|
}
|
|
else if (isset($_GET['board2']) && in_array($_GET['board2'], $this->boards)) {
|
|
$board = mysql_real_escape_string($_GET['board2']);
|
|
|
|
$this->filterBoard2 = htmlspecialchars($board, ENT_QUOTES);
|
|
|
|
$board = "AND (board1 = '$board' OR board2 = '$board')";
|
|
}
|
|
else {
|
|
$board = '';
|
|
|
|
$this->filterBoard = null;
|
|
}
|
|
|
|
$tableName = $this->tableName;
|
|
|
|
// Number of rated applications / Total number of applications
|
|
$query =<<<SQL
|
|
SELECT COUNT(id) FROM `$tableName`
|
|
WHERE voters LIKE '%$username<%'
|
|
SQL;
|
|
$res = mysql_global_call($query);
|
|
$this->ratedCount = (int)mysql_fetch_row($res)[0];
|
|
|
|
// FIXME: voters column default value should be '>'
|
|
|
|
// Fetch unrated apps
|
|
$query =<<<SQL
|
|
SELECT id FROM `$tableName`
|
|
WHERE closed = $status
|
|
AND (votes < 3 OR avg > 2.0)
|
|
AND voters NOT LIKE '%$username<%'
|
|
$board
|
|
SQL;
|
|
|
|
$res = mysql_global_call($query);
|
|
|
|
$this->unratedCount = mysql_num_rows($res);
|
|
|
|
$ids = array();
|
|
|
|
while ($row = mysql_fetch_assoc($res)) {
|
|
$ids[] = $row['id'];
|
|
}
|
|
|
|
if (!empty($ids)) {
|
|
$key = array_rand($ids);
|
|
|
|
$random_id = $ids[$key];
|
|
|
|
$query = "SELECT * FROM `{$this->tableName}` WHERE id = $random_id";
|
|
|
|
$res = mysql_global_call($query);
|
|
|
|
$this->app = array();
|
|
|
|
$row = mysql_fetch_assoc($res);
|
|
|
|
// Escape HTML
|
|
foreach ($row as $key => $value) {
|
|
$this->app[$key] = htmlspecialchars($value, ENT_QUOTES);
|
|
}
|
|
|
|
if ($this->is_supadmin) {
|
|
$q = "SELECT COUNT(no) FROM banned_users WHERE host = '{$row['ip']}'";
|
|
$r = mysql_global_call($q);
|
|
|
|
if (!$r) {
|
|
$this->app['ban_count'] = -1;
|
|
}
|
|
else {
|
|
$this->app['ban_count'] = (int)mysql_fetch_row($r)[0];
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
$this->app = null;
|
|
}
|
|
|
|
$this->canReview = has_level('manager') || has_flag('developer');
|
|
|
|
$this->renderHTML('janitorapps');
|
|
}
|
|
|
|
/**
|
|
* Rate application
|
|
*/
|
|
public function rate() {
|
|
if (!self::REVIEW_OPEN) {
|
|
$this->error("Applications aren't closed yet");
|
|
}
|
|
|
|
if (!isset($_GET['id']) || !isset($_GET['score'])) {
|
|
$this->error('Missing arguments');
|
|
}
|
|
|
|
$id = (int)$_GET['id'];
|
|
$score = (int)$_GET['score'];
|
|
|
|
if ($score > $this->maxScore) {
|
|
$this->error('Invalid score');
|
|
}
|
|
|
|
$query = "SELECT closed, voters FROM `{$this->tableName}` WHERE id = $id";
|
|
$result = mysql_global_call($query);
|
|
$app = mysql_fetch_assoc($result);
|
|
|
|
if (!$app) {
|
|
$this->error('Bad id');
|
|
}
|
|
|
|
$username = mysql_real_escape_string($_COOKIE['4chan_auser']);
|
|
|
|
if (strpos($app['voters'], "$username<") !== false) {
|
|
$this->error('You have already rated this application');
|
|
}
|
|
|
|
if ((int)$app['closed'] != self::OPEN) {
|
|
$this->error('This application is not open');
|
|
}
|
|
|
|
$tableName = $this->tableName;
|
|
|
|
$query =<<<SQL
|
|
UPDATE `$tableName`
|
|
SET total = total + $score,
|
|
votes = votes + 1,
|
|
avg = total / votes,
|
|
voters = CONCAT(voters, '$username<$score>')
|
|
WHERE id = $id
|
|
LIMIT 1
|
|
SQL;
|
|
$result = mysql_global_call($query);
|
|
|
|
if (!$result) {
|
|
$this->error('Database error');
|
|
}
|
|
|
|
$this->success();
|
|
}
|
|
|
|
public function review() {
|
|
if (!has_level('manager') && !has_flag('developer')) {
|
|
$this->error("Can't let you do that.");
|
|
}
|
|
|
|
$this->timezones = array(
|
|
0 => 'Americas (-12 -2)',
|
|
1 => 'Europe (-1 +4)',
|
|
2 => 'Asia (+5 +14)'
|
|
);
|
|
|
|
$timezone_offsets = array(array(-12, -2), array(-1, 4), array(5, 14));
|
|
|
|
// Filtering
|
|
$clauses = array();
|
|
$url_clauses = array();
|
|
|
|
$this->filterStatus = null;
|
|
$this->filterBoard = null;
|
|
$this->filterTz = null;
|
|
$this->filterSearch = null;
|
|
|
|
// Type
|
|
if (isset($_GET['status'])) {
|
|
if ($_GET['status'] == 'rejected') {
|
|
$clauses[] = 'closed = ' . self::REJECTED;
|
|
}
|
|
else if ($_GET['status'] == 'accepted') {
|
|
$clauses[] = 'closed = ' . self::ACCEPTED;
|
|
}
|
|
else if ($_GET['status'] == 'signed') {
|
|
$clauses[] = 'closed = ' . self::SIGNED;
|
|
}
|
|
else if ($_GET['status'] == 'oriented') {
|
|
$clauses[] = 'closed = ' . self::ORIENTED;
|
|
}
|
|
else if ($_GET['status'] == 'closed') {
|
|
$clauses[] = 'closed = ' . self::CLOSED;
|
|
}
|
|
else if ($_GET['status'] == 'ignored') {
|
|
$clauses[] = 'closed = ' . self::IGNORED;
|
|
}
|
|
|
|
$this->filterStatus = $_GET['status'];
|
|
$url_clauses[] = 'status=' . htmlspecialchars($_GET['status'], ENT_QUOTES);
|
|
}
|
|
else {
|
|
$clauses[] = 'closed = ' . self::OPEN;
|
|
}
|
|
|
|
// Board
|
|
if (isset($_GET['board'])) {
|
|
$board_escaped = mysql_real_escape_string($_GET['board']);
|
|
$clauses[] = "(board1 = '$board_escaped' OR board2 = '$board_escaped')";
|
|
$this->filterBoard = $_GET['board'];
|
|
$url_clauses[] = 'board=' . htmlspecialchars($_GET['board'], ENT_QUOTES);
|
|
}
|
|
|
|
// Timezone
|
|
if (isset($_GET['tz']) && $timezone_offsets[(int)$_GET['tz']]) {
|
|
$int_tz = (int)$_GET['tz'];
|
|
$tz_range = $timezone_offsets[$int_tz];
|
|
$clauses[] = "(tz >= {$tz_range[0]} AND tz <= {$tz_range[1]})";
|
|
$this->filterTz = $int_tz;
|
|
$url_clauses[] = 'tz=' . $int_tz;
|
|
}
|
|
|
|
// Search (email or ID)
|
|
if (isset($_GET['search'])) {
|
|
$search_terms = preg_split("/[\s,]+/", $_GET['search']);
|
|
|
|
$search_clauses = array();
|
|
$email_clauses = array();
|
|
$id_clauses = array();
|
|
|
|
foreach ($search_terms as $term) {
|
|
if (stripos($term, '@') !== false) {
|
|
$email_clauses[] = mysql_real_escape_string($term);
|
|
}
|
|
else {
|
|
$id_clauses[] = (int)$term;
|
|
}
|
|
}
|
|
|
|
if (!empty($email_clauses)) {
|
|
$search_clauses[] = "email IN('" . implode("','", $email_clauses) . "')";
|
|
}
|
|
|
|
if (!empty($id_clauses)) {
|
|
$search_clauses[] = "id IN(" . implode(",", $id_clauses) . ")";
|
|
}
|
|
|
|
if (!empty($search_clauses)) {
|
|
$clauses[] = '(' . implode(' OR ', $search_clauses) . ')';
|
|
}
|
|
|
|
$this->filterSearch = htmlspecialchars($_GET['search'], ENT_QUOTES);
|
|
|
|
// Don't paginate search results
|
|
$needPagination = false;
|
|
}
|
|
else {
|
|
$needPagination = true;
|
|
}
|
|
|
|
if (!empty($clauses)) {
|
|
$where = 'WHERE ' . implode(' AND ', $clauses);
|
|
}
|
|
else {
|
|
$where = '';
|
|
}
|
|
|
|
// Offset
|
|
if ($needPagination) {
|
|
if (isset($_GET['offset'])) {
|
|
$offset = (int)$_GET['offset'];
|
|
}
|
|
else {
|
|
$offset = 0;
|
|
}
|
|
$limit = $this->pageSize + 1;
|
|
$limit = "LIMIT $offset, $limit";
|
|
}
|
|
else {
|
|
$limit = '';
|
|
}
|
|
|
|
$tableName = $this->tableName;
|
|
|
|
$query =<<<SQL
|
|
SELECT *, UNIX_TIMESTAMP(updated) as updated_on FROM `$tableName`
|
|
$where
|
|
ORDER BY avg DESC, votes DESC
|
|
$limit
|
|
SQL;
|
|
|
|
$result = mysql_global_call($query);
|
|
|
|
$this->apps = array();
|
|
|
|
while ($row = mysql_fetch_assoc($result)) {
|
|
$app = array();
|
|
|
|
// Escape HTML
|
|
foreach ($row as $key => $value) {
|
|
$app[$key] = htmlspecialchars($value, ENT_QUOTES);
|
|
}
|
|
|
|
$q = "SELECT COUNT(no) FROM banned_users WHERE host = '{$app['ip']}'";
|
|
$r = mysql_global_call($q);
|
|
|
|
if (!$r) {
|
|
$app['ban_count'] = -1;
|
|
}
|
|
else {
|
|
$app['ban_count'] = (int)mysql_fetch_row($r)[0];
|
|
}
|
|
|
|
$geoinfo = GeoIP2::get_country($app['ip']);
|
|
|
|
if ($geoinfo && isset($geoinfo['country_code'])) {
|
|
$country = $geoinfo['country_code'];
|
|
|
|
$geo_loc = array();
|
|
|
|
if (isset($geoinfo['city_name'])) {
|
|
$geo_loc[] = $geoinfo['city_name'];
|
|
}
|
|
|
|
if (isset($geoinfo['state_code'])) {
|
|
$geo_loc[] = $geoinfo['state_code'];
|
|
}
|
|
|
|
$geo_loc[] = $geoinfo['country_name'];
|
|
|
|
$app['geo_loc'] = implode(', ', $geo_loc);
|
|
}
|
|
|
|
$this->apps[] = $app;
|
|
}
|
|
|
|
$url_search = implode('&', $url_clauses);
|
|
|
|
if ($url_search !== '') {
|
|
$url_search = '&' . $url_search;
|
|
}
|
|
|
|
$this->previousOffset = false;
|
|
$this->nextOffset = false;
|
|
|
|
if ($needPagination) {
|
|
if ($offset > 0) {
|
|
$this->filterOffset = $offset;
|
|
|
|
$this->previousOffset = $offset - $this->pageSize;
|
|
|
|
if ($this->previousOffset < 0) {
|
|
$this->previousOffset = 0;
|
|
}
|
|
|
|
$this->previousOffset = $url_search . '&offset=' . $this->previousOffset;
|
|
}
|
|
|
|
if (count($this->apps) > $this->pageSize) {
|
|
$this->nextOffset = $url_search . '&offset=' . ($offset + $this->pageSize);
|
|
array_pop($this->apps);
|
|
}
|
|
}
|
|
|
|
$this->boards = $this->getBoards();
|
|
|
|
$this->counts = $this->getStatusCounts();
|
|
|
|
$this->renderHTML('janitorapps-review');
|
|
}
|
|
|
|
/**
|
|
* Accept
|
|
*/
|
|
public function accept() {
|
|
if (!has_level('manager') && !has_flag('developer')) {
|
|
$this->error("Can't let you do that.");
|
|
}
|
|
|
|
if (!isset($_POST['id'])) {
|
|
$this->error('Bad Request.');
|
|
}
|
|
|
|
$id = (int)$_POST['id'];
|
|
|
|
// Get application
|
|
$query = "SELECT email, closed FROM `{$this->tableName}` WHERE id = $id";
|
|
$result = mysql_global_call($query);
|
|
$app = mysql_fetch_assoc($result);
|
|
|
|
if (!$app) {
|
|
$this->error('Bad ID.');
|
|
}
|
|
|
|
if ($app['closed'] == self::CLOSED) {
|
|
$this->error("This application is already fully accepted.");
|
|
}
|
|
|
|
$creds = $this->createTemporaryCreds($id);
|
|
$this->changeStatus($id, self::ACCEPTED);
|
|
|
|
// Email
|
|
$values = array(
|
|
'{{LOGIN}}' => $creds['login'],
|
|
'{{PWD}}' => $creds['pwd'],
|
|
'{{KEY}}' => $creds['key']
|
|
);
|
|
|
|
if (!$this->sendMail($app['email'], self::MAIL_ACCEPT_FILE, $values)) {
|
|
$this->error('Email not accepted for delivery.');
|
|
}
|
|
|
|
$this->success();
|
|
}
|
|
|
|
/**
|
|
* Reject
|
|
*/
|
|
public function reject() {
|
|
if (!has_level('manager') && !has_flag('developer')) {
|
|
$this->error("Can't let you do that.");
|
|
}
|
|
|
|
if (!isset($_POST['id'])) {
|
|
$this->error('Bad Request.');
|
|
}
|
|
|
|
$id = (int)$_POST['id'];
|
|
|
|
// Get application
|
|
$query = "SELECT closed, agreement_key FROM `{$this->tableName}` WHERE id = $id";
|
|
$result = mysql_global_call($query);
|
|
$app = mysql_fetch_assoc($result);
|
|
|
|
if (!$app) {
|
|
$this->error('Bad ID.');
|
|
}
|
|
|
|
if ($app['closed'] == self::CLOSED) {
|
|
$this->error("This application is already fully accepted.");
|
|
}
|
|
|
|
if ($app['closed'] == self::ACCEPTED) {
|
|
$this->removeTempAccount($app['agreement_key']);
|
|
}
|
|
|
|
$this->changeStatus($_POST['id'], self::REJECTED);
|
|
|
|
$this->success();
|
|
}
|
|
|
|
/**
|
|
* Send orientation email
|
|
*/
|
|
public function send_orientation() {
|
|
if (!has_level('manager') && !has_flag('developer')) {
|
|
$this->error("Can't let you do that.");
|
|
}
|
|
|
|
if (!isset($_POST['id'])) {
|
|
$this->error('Bad Request.');
|
|
}
|
|
|
|
$id = (int)$_POST['id'];
|
|
|
|
// Get application
|
|
$query = "SELECT email, closed FROM `{$this->tableName}` WHERE id = $id";
|
|
$result = mysql_global_call($query);
|
|
$app = mysql_fetch_assoc($result);
|
|
|
|
if (!$app) {
|
|
$this->error('Bad ID.');
|
|
}
|
|
|
|
if ($app['closed'] != self::SIGNED) {
|
|
$this->error("The Volunteer Moderator Agreement needs to be signed first.");
|
|
}
|
|
|
|
if ($app['closed'] == self::CLOSED) {
|
|
$this->error("This application is already fully accepted.");
|
|
}
|
|
|
|
$this->changeStatus($id, self::ORIENTED);
|
|
|
|
// Email
|
|
if (!$this->sendMail($app['email'], self::MAIL_ORIENTATION_FILE)) {
|
|
$this->error('Email not accepted for delivery.');
|
|
}
|
|
|
|
$this->success();
|
|
}
|
|
|
|
/**
|
|
* Create janitor account
|
|
*/
|
|
public function create_account() {
|
|
if (!has_level('manager') && !has_flag('developer')) {
|
|
$this->error("Can't let you do that.");
|
|
}
|
|
|
|
if (!isset($_POST['otp']) || !$_POST['otp']) {
|
|
$this->error('Bad Request.');
|
|
}
|
|
|
|
$this->validate_otp($_POST['otp']);
|
|
|
|
if (!isset($_POST['id'])) {
|
|
$this->error('Bad Request.');
|
|
}
|
|
|
|
$id = (int)$_POST['id'];
|
|
|
|
// Get application
|
|
$query = "SELECT * FROM `{$this->tableName}` WHERE id = $id";
|
|
$result = mysql_global_call($query);
|
|
$app = mysql_fetch_assoc($result);
|
|
|
|
if (!$app) {
|
|
$this->error('Bad ID.');
|
|
}
|
|
|
|
if ($app['closed'] != self::ORIENTED) {
|
|
$this->error("This user hasn't received the orientation mail yet.");
|
|
}
|
|
|
|
if ($app['closed'] == self::CLOSED) {
|
|
$this->error("This application is already fully accepted.");
|
|
}
|
|
|
|
// Board
|
|
if (isset($_POST['board'])) {
|
|
$board = preg_split('/[\s,]+/', $_POST['board']);
|
|
|
|
if (count($board) > 2) {
|
|
$this->error('You can only assign janitors to a maximum of two boards.');
|
|
}
|
|
|
|
$board = implode(',', $board);
|
|
}
|
|
else {
|
|
$board = $app['board1'];
|
|
}
|
|
|
|
if (!preg_match('/^[,a-z0-9]+$/', $board)) {
|
|
$this->error('Invalid format for board.');
|
|
}
|
|
|
|
$board = mysql_real_escape_string($board);
|
|
|
|
// Username
|
|
if (isset($_POST['username'])) {
|
|
$username = $_POST['username'];
|
|
}
|
|
else {
|
|
$username = $app['handle'];
|
|
}
|
|
|
|
if (!preg_match('/^[-_a-zA-Z0-9]+$/', $username)) {
|
|
$this->error('Invalid user name. Allowed characters are -_a-zA-Z0-9');
|
|
}
|
|
|
|
$username = mysql_real_escape_string($username);
|
|
|
|
$query = "SELECT username FROM mod_users WHERE LOWER(username) = '" . strtolower($username) . "'";
|
|
$result = mysql_global_call($query);
|
|
|
|
if (mysql_num_rows($result) > 0) {
|
|
$this->error('Username already exists.');
|
|
}
|
|
|
|
// Password
|
|
$plain_password = $this->getRandomHexBytes();
|
|
$hashed_password = password_hash($plain_password, PASSWORD_DEFAULT);
|
|
|
|
// Email
|
|
$values = array(
|
|
'{{USERNAME}}' => $username,
|
|
'{{PASSWORD}}' => $plain_password,
|
|
'{{BOARD}}' => $board
|
|
);
|
|
|
|
if (!$this->sendMail($app['email'], self::MAIL_ACCOUNT_FILE, $values)) {
|
|
$this->error('Email not accepted for delivery.');
|
|
}
|
|
|
|
$email = mysql_real_escape_string($app['email']);
|
|
|
|
// Query
|
|
$query =<<<SQL
|
|
INSERT INTO mod_users (
|
|
username,
|
|
password,
|
|
password_expired,
|
|
level,
|
|
allow,
|
|
email,
|
|
signed_agreement,
|
|
janitorapp_id
|
|
) VALUES (
|
|
'$username',
|
|
'$hashed_password',
|
|
1,
|
|
'janitor',
|
|
'$board',
|
|
'$email',
|
|
1,
|
|
$id
|
|
)
|
|
SQL;
|
|
|
|
$res = mysql_global_call($query);
|
|
|
|
if (!$res || mysql_affected_rows() !== 1) {
|
|
$this->error('Database error.');
|
|
}
|
|
|
|
$cmd = sprintf(self::HTPASSWD_CMD,
|
|
self::JANITOR_HTPASSWD,
|
|
escapeshellarg($username),
|
|
escapeshellarg($plain_password)
|
|
);
|
|
|
|
if (system($cmd) === false) {
|
|
$this->error('Could not update htpasswd file (1).');
|
|
}
|
|
|
|
$cmd = sprintf(self::HTPASSWD_CMD_NGINX,
|
|
self::JANITOR_HTPASSWD_NGINX,
|
|
escapeshellarg($username),
|
|
escapeshellarg($plain_password)
|
|
);
|
|
|
|
if (system($cmd) === false) {
|
|
$this->error('Could not update htpasswd file (2).');
|
|
}
|
|
|
|
$this->changeStatus($id, self::CLOSED);
|
|
|
|
$this->success();
|
|
}
|
|
|
|
/**
|
|
* 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();
|