handlers = $handlers; if (empty($_REQUEST)) { $this->reqs = null; if (substr($_SERVER["CONTENT_TYPE"] ?? '', 0, 16) === 'application/json') { // screw you too Axios // also come on PHP, you could just support JSON natively instead of putting me through this $input = trim(file_get_contents('php://input')); if ($input[0] === '[') { $this->reqs = json_decode($input, true); } else if ($input[0] === '{') { $this->reqs = [json_decode($input, true)]; } } if (empty($this->reqs)) die("no request data found - you need to send some sort of data"); $_POST['is_post_request'] = true; } else { $this->reqs = [$_REQUEST]; } if (@$_REQUEST['json']) { $this->reqs = json_decode($_REQUEST['json'], true); $this->multiReqs = true; } } public function setPrefix($prefix) { $this->outPrefix = $prefix; } public function getServerHostName($serverid) { global $PokemonServers; $server = @$PokemonServers[$serverid]; return $server ? $server['server'] : $serverid; } public function verifyCrossDomainRequest() { global $psconfig; // No cross-domain multi-requests for security reasons. // No need to do anything if this isn't a cross-domain request. if ($this->multiReqs || !isset($_SERVER['HTTP_ORIGIN'])) { return ''; } $origin = $_SERVER['HTTP_ORIGIN']; $prefix = null; foreach ($psconfig['cors'] as $i => &$j) { if (!preg_match($i, $origin)) continue; $prefix = $j; break; } if ($prefix === null) { // Bogus request. return ''; } // Valid CORS request. header('Access-Control-Allow-Origin: ' . $origin); header('Access-Control-Allow-Credentials: true'); return $prefix; } public function getIp() { global $users; return $users->getIp(); } public function findServer() { global $PokemonServers; $serverid = @$this->reqData['serverid']; $server = null; $ip = $this->getIp(); if (!isset($PokemonServers[$serverid])) { // Try to find the server by source IP, rather than by serverid. foreach ($PokemonServers as &$i) { if (!isset($i['ipcache'])) { $i['ipcache'] = gethostbyname($i['server']); } if ($i['ipcache'] === $ip) { $server =& $i; break; } } if (!$server) return null; } else { $server =& $PokemonServers[$serverid]; if (empty($server['skipipcheck']) && empty($server['token']) && $serverid !== 'showdown') { if (!isset($server['ipcache'])) { $server['ipcache'] = gethostbyname($server['server']); } if ($ip !== $server['ipcache']) return null; } } if (!empty($server['token'])) { if ($server['token'] !== md5($this->reqData['servertoken'])) return null; } return $server; } public function executeActions() { $outArray = null; if ($this->multiReqs) $outArray = array(); foreach ($this->reqs as $this->reqData) { $this->reqData = array_merge($_REQUEST, $this->reqData); if (!isset($this->reqData['act'])) die("action not found - make sure your request data includes act=something"); $action = $this->reqData['act']; if (!ctype_alnum($action)) die("invalid action: " . var_export($action, true)); $out = array(); foreach ($this->handlers as &$i) { if (is_callable(array($i, $action))) { $i->$action($this, $this->reqData, $out); } } if ($this->multiReqs) $outArray[] = $out; } // json output if ($this->outPrefix !== '') { // Technically this is not JSON because of the initial prefix. header('Content-Type: text/plain; charset=utf-8'); } else { header('Content-Type: application/json'); } if ($this->multiReqs) { die($this->outPrefix . json_encode($outArray)); } else { die($this->outPrefix . json_encode($out)); } } } class DefaultActionHandler { public function login($dispatcher, &$reqData, &$out) { global $users, $curuser; $challengeprefix = $dispatcher->verifyCrossDomainRequest(); if (!$_POST) die('for security reasons, logins must happen with POST data'); if (empty($reqData['name']) || empty($reqData['pass'])) die('incorrect login data, you need "name" and "pass" fields'); try { $users->login($reqData['name'], $reqData['pass']); } catch (Exception $e) { $out['error'] = $e->getMessage() . "\n" . $e->getFile() . '(' . $e->getLine() . ')' . "\n" . $e->getTraceAsString(); } unset($curuser['userdata']); $out['curuser'] = $curuser; $out['actionsuccess'] = ($curuser ? $curuser['loggedin'] : false); $serverhostname = '' . $dispatcher->getServerHostName(@$reqData['serverid']); $challengekeyid = !isset($reqData['challengekeyid']) ? -1 : intval($reqData['challengekeyid']); $challenge = !isset($reqData['challenge']) ? '' : $reqData['challenge']; if (!$challenge) { $challenge = !isset($reqData['challstr']) ? '' : $reqData['challstr']; } $out['assertion'] = $users->getAssertion($curuser['userid'], $serverhostname, null, $challengekeyid, $challenge, $challengeprefix); } public function register($dispatcher, &$reqData, &$out) { global $users, $curuser; $challengeprefix = $dispatcher->verifyCrossDomainRequest(); $serverhostname = '' . $dispatcher->getServerHostName(@$reqData['serverid']); $user = [ 'username' => @$_POST['username'], ]; $userid = $users->userid($user['username']); if ((mb_strlen($userid) < 1) || ctype_digit($userid)) { $out['actionerror'] = 'Your username must contain at least one letter.'; } else if (substr($userid, 0, 5) === 'guest') { $out['actionerror'] = 'Your username cannot start with \'guest\'.'; } else if (mb_strlen($user['username']) > 18) { $out['actionerror'] = 'Your username must be less than 19 characters long.'; } else if (mb_strlen(@$_POST['password']) < 5) { $out['actionerror'] = 'Your password must be at least 5 characters long.'; } else if (@$_POST['password'] !== @$_POST['cpassword']) { $out['actionerror'] = 'Your passwords do not match.'; } else if (trim(strtolower(@$_POST['captcha'])) !== 'pikachu') { $out['actionerror'] = 'Please answer the anti-spam question given.'; } else if (($registrationcount = $users->getRecentRegistrationCount()) === false) { $out['actionerror'] = 'A database error occurred. Please try again.'; } else if ($registrationcount >= 2) { $out['actionerror'] = 'You can\'t register more than two usernames every two hours. Try again later.'; } else if ($user = $users->addUser($user, $_POST['password'])) { $challengekeyid = !isset($reqData['challengekeyid']) ? -1 : intval($reqData['challengekeyid']); $challenge = !isset($reqData['challenge']) ? '' : $reqData['challenge']; if (!$challenge) { $challenge = !isset($reqData['challstr']) ? '' : $reqData['challstr']; } $out['curuser'] = $user; $out['assertion'] = $users->getAssertion($user['userid'], $serverhostname, $user, $challengekeyid, $challenge, $challengeprefix); $out['actionsuccess'] = true; } else { $out['actionerror'] = 'Your username is already taken.'; } } public function changepassword($dispatcher, &$reqData, &$out) { global $users, $curuser; if (!$_POST || !isset($reqData['oldpassword']) || !isset($reqData['password']) || !isset($reqData['cpassword'])) { $out['actionerror'] = 'Invalid request.'; } else if (!$curuser['loggedin']) { $out['actionerror'] = 'Your session has expired. Please log in again.'; } else if ($reqData['password'] !== $reqData['cpassword']) { $out['actionerror'] = 'Your new passwords do not match.'; } else if (!$users->passwordVerify($curuser['userid'], $reqData['oldpassword'])) { $out['actionerror'] = 'Your old password was incorrect.'; } else if (mb_strlen($reqData['password']) < 5) { $out['actionerror'] = 'Your new password must be at least 5 characters long.'; } else if (!$users->modifyUser($curuser['userid'], array( 'password' => $reqData['password']))) { $out['actionerror'] = 'A database error occurred. Please try again.'; } else { $out['actionsuccess'] = true; } } public function changeusername($dispatcher, &$reqData, &$out) { global $users, $curuser; if (!$_POST || !isset($reqData['username'])) { $out['actionerror'] = 'Invalid request.'; } else if (!$curuser['loggedin']) { $out['actionerror'] = 'Your session has expired. Please log in again.'; } else if (!$users->modifyUser($curuser['userid'], array( 'username' => $reqData['username']))) { $out['actionerror'] = 'A database error occurred. Please try again.'; } else { $out['actionsuccess'] = true; } } public function logout($dispatcher, &$reqData, &$out) { global $users, $curuser; if (!$_POST || !isset($reqData['userid']) || // some CSRF protection (client must know current userid) ($reqData['userid'] !== $curuser['userid'])) { die; } $users->logout(); // this kills the `sid` cookie setcookie('showdown_username', '', time()-60*60*24*2, '/', 'play.pokemonshowdown.com'); $out['actionsuccess'] = true; } public function getassertion($dispatcher, &$reqData, &$out) { global $users, $curuser; $challengeprefix = $dispatcher->verifyCrossDomainRequest(); $serverhostname = '' . $dispatcher->getServerHostName(@$reqData['serverid']); $challengekeyid = !isset($reqData['challengekeyid']) ? -1 : intval($reqData['challengekeyid']); $challenge = !isset($reqData['challenge']) ? '' : $reqData['challenge']; if (!$challenge) { $challenge = !isset($reqData['challstr']) ? '' : $reqData['challstr']; } header('Content-Type: text/plain; charset=utf-8'); if (empty($reqData['userid'])) { $userid = $curuser['userid']; if ($userid === 'guest') { // Special error message for this case. die(';'); } } else { $userid = $users->userid($reqData['userid']); } $serverhostname = htmlspecialchars($serverhostname); // Protect against theoretical IE6 XSS die($users->getAssertion($userid, $serverhostname, null, $challengekeyid, $challenge, $challengeprefix)); } public function upkeep($dispatcher, &$reqData, &$out) { global $users, $curuser; $challengeprefix = $dispatcher->verifyCrossDomainRequest(); $out['loggedin'] = $curuser['loggedin']; $userid = ''; if ($curuser['loggedin']) { $out['username'] = $curuser['username']; $userid = $curuser['userid']; } else if (isset($_COOKIE['showdown_username'])) { $out['username'] = $_COOKIE['showdown_username']; $userid = $users->userid($out['username']); } if ($userid !== '') { $serverhostname = '' . $dispatcher->getServerHostName(@$reqData['serverid']); $challengekeyid = !isset($reqData['challengekeyid']) ? -1 : intval($reqData['challengekeyid']); $challenge = !isset($reqData['challenge']) ? '' : $reqData['challenge']; if (!$challenge) { $challenge = !isset($reqData['challstr']) ? '' : $reqData['challstr']; } $out['assertion'] = $users->getAssertion($userid, $serverhostname, null, $challengekeyid, $challenge, $challengeprefix); } } public function updateuserstats($dispatcher, &$reqData, &$out) { global $psdb; $server = $dispatcher->findServer(); if (!$server) { $out = 0; return; } $date = @$reqData['date']; $usercount = @$reqData['users']; if (!is_numeric($date) || !is_numeric($usercount)) { $out = 0; return; } $out = !!$psdb->query( "INSERT INTO `ntbb_userstats` (`serverid`, `date`, `usercount`) " . "VALUES ('" . $psdb->escape($server['id']) . "', '" . $psdb->escape($date) . "', '" . $psdb->escape($usercount) . "') " . "ON DUPLICATE KEY UPDATE `date`='" . $psdb->escape($date) . "', `usercount`='" . $psdb->escape($usercount) . "'"); if ($server['id'] === 'showdown') { $psdb->query( "INSERT INTO `ntbb_userstatshistory` (`date`, `usercount`) " . "VALUES ('" . $psdb->escape($date) . "', '" . $psdb->escape($usercount) . "')"); } $dispatcher->setPrefix(''); // No need for prefix since only usable by server. } public function prepreplay($dispatcher, &$reqData, &$out) { global $psdb, $users; // include_once dirname(__FILE__) . '/ntbb-ladder.lib.php'; // not clear if this is needed $server = $dispatcher->findServer(); if (!$server) { $out['errorip'] = $dispatcher->getIp(); return; } if ( // the server must be registered !$server || // the server must send all the required values !isset($reqData['id']) || !isset($reqData['format']) || !isset($reqData['loghash']) || !isset($reqData['p1']) || !isset($reqData['p2']) || // player usernames cannot be longer than 18 characters (mb_strlen($reqData['p1']) > 18) || (mb_strlen($reqData['p2']) > 18) || // the battle ID must be valid !preg_match('/^([a-z0-9]+)-[0-9]+$/', $reqData['id'], $m1) || // the format ID must be valid !preg_match('/^([a-z0-9]+)$/', $reqData['format'], $m2) || // the format from the battle ID must match the format ID ($m1[1] !== $m2[1])) { $out = 0; return; } if ($server['id'] !== 'showdown') { $reqData['id'] = $server['id'].'-'.$reqData['id']; } $reqData['serverid'] = $server['id']; include_once __DIR__.'/../../replay.pokemonshowdown.com/replays.lib.php'; $out = $GLOBALS['Replays']->prepUpload($reqData); $dispatcher->setPrefix(''); // No need for prefix since only usable by server. } public function uploadreplay($dispatcher, &$reqData, &$out) { global $psdb, $users; header('Content-Type: text/plain; charset=utf-8'); include __DIR__.'/../../replay.pokemonshowdown.com/replays.lib.php'; die($GLOBALS['Replays']->upload($reqData)); } public function invalidatecss($dispatcher, &$reqData, &$out) { $server = $dispatcher->findServer(); if (!$server) { $out['errorip'] = $dispatcher->getIp(); return; } // No need to sanitise $server['id'] because it should be safe already. $cssfile = dirname(__FILE__) . '/../../pokemonshowdown.com/config/customcss/' . $server['id'] . '.css'; @unlink($cssfile); } /** * This function returns all friends of $curuser * Formatting: * [prefix][username]|[prefix][username]|... * [prefix] is empty if the request has been accepted, a hash (symbol) * if a request has been sent by the player and is still pending, * or a comma if it is a received friend request. * [username] is the username (yes, NAME, not id) of the player. * Example: Zarel|,haunter|#chaos * Zarel is already on the friend list, a friend request from haunter is * still waiting for approval, and a friend request has been sent to * chaos. */ public function getfriends($dispatcher, &$reqData, &$out) { global $psdb, $curuser; // A valid curuser array is needed if (!@$curuser['loggedin'] || !@$curuser['userid']) { die('Not using a valid nick; you should be registered and logged in in order to add friends.'); } $player = $psdb->escape($curuser['userid']); $friends = array(); $friendsQuery = $psdb->query( "SELECT `us`.`username`, `fr`.`p1`, `fr`.`accepted` " . "FROM `ntbb_friendlist` AS `fr` " . "INNER JOIN `ntbb_users` AS `us` ON `us`.`userid` = IF(`fr`.`p1` = '" . $player . "', `fr`.`p2`, `fr`.`p1`) " . "WHERE `p1`='" . $player . "' OR `p2`='" . $player . "'" ); while ($friend = $psdb->fetch_assoc($friendsQuery)) { $prefix = ''; if ((int) $friend['accepted'] === 0) { if ($friend['p1'] === $curuser['userid']) { // Request sent and pending $prefix = '#'; } else { // Received friend request $prefix = ','; } } $friends[] = $prefix . $friend['username']; } // We add ] here so that we can check in the client if it's a valid // response that we've received (i.e. check if there are errors or not) die(']' . implode('|', $friends)); } /** * This function does two things: * - sends a friend request if the player is not in his/her friend list yet * - accepts a friend request sent by the player given in the query string */ public function addfriend($dispatcher, &$reqData, &$out) { global $psdb, $users, $curuser; // A valid curuser array is needed if (!@$curuser['loggedin'] || !@$curuser['userid']) { die('Not using a valid nick; you should be registered and logged in in order to add friends.'); } // Check if the other player exists $id = $users->userid(@$reqData['player']); if (!$id) die('Invalid playername given.'); $player = $users->getUser($id); if (!$player) die('The given player does not exist.'); // Check if there isn't a friendship between those two already $p1 = $psdb->escape($curuser['userid']); $p2 = $psdb->escape($player['userid']); $res = $psdb->query( "SELECT p1, accepted " . "FROM `ntbb_friendlist` " . "WHERE (" . "`p1`='" . $p1 . "' AND `p2`='" . $p2 . "'" . ") OR (" . "`p1`='" . $p2 . "' AND `p2`='" . $p1 . "'" . ")" ); $record = $psdb->fetch_assoc($res); if ($record) { // A record in the database exists. Now we check if it's accepted // or not. If not, we'll accept it, otherwise send an error if ($record['p1'] !== $curuser['userid'] && ((int) $record['accepted']) === 0) { $psdb->query("UPDATE `ntbb_friendlist` SET `accepted` = '1' WHERE `p1`='" . $p2 . "' AND `p2`='" . $p1 . "'"); // The ] denotes that it was successful die(']The friend request by ' . $player['username'] . ' has been accepted.'); } else { die('This player is already in your friend list or a friend request is still pending.'); } } // Everything's okay, so insert it $psdb->query("INSERT INTO `ntbb_friendlist` (`p1`, `p2`) VALUES ('" . $p1 . "', '" . $p2 . "')"); // The ] denotes that it was successful die(']A friend request has been sent to ' . $player['username'] . '!'); } /** * This function simply removes the friend given in the query string. */ public function removefriend($dispatcher, &$reqData, &$out) { global $psdb, $users, $curuser; // A valid curuser array is needed if (!@$curuser['loggedin'] || !@$curuser['userid']) { die('Not using a valid nick; you should be registered and logged in in order to add friends.'); } $userid = $psdb->escape($curuser['userid']); $player = $psdb->escape($reqData['player']); $res = $psdb->query( "DELETE FROM `ntbb_friendlist` " . "WHERE (" . "`p1`='" . $userid . "' AND `p2`='" . $player . "'" . ") OR (" . "`p1`='" . $player . "' AND `p2`='" . $userid . "'" . ") " . "LIMIT 1" ); if (mysqli_affected_rows($psdb->db)) die(']' . $reqData['player'] . ' has been removed from your friend list.'); die('Could not remove ' . $reqData['player'] . ' from your friend list.'); } } // This class should not depend on ntbb-session.lib.php. class LadderActionHandler { // There's no need to make a database query for this. private function getUserData($username) { if (!$username) $username = ''; if (mb_strlen($username) > 18) return false; $userid = strtr($username, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz"); $userid = preg_replace('/[^A-Za-z0-9]+/', '', $userid); return array('userid' => $userid, 'username' => $username); } public function ladderupdate($dispatcher, &$reqData, &$out) { include_once dirname(__FILE__) . '/ntbb-ladder.lib.php'; $server = $dispatcher->findServer(); if (!$server || $server['id'] !== 'showdown') { $out['errorip'] = "Your version of PS is too old for this ladder system. Please update."; return; } $ladder = new NTBBLadder(@$reqData['format']); $p1 = $this->getUserData(@$reqData['p1']); $p2 = $this->getUserData(@$reqData['p2']); if (!$p1 || !$p2) { // The server should not send usernames > 18 characters long. $out = 0; return; } $ladder->updateRating($p1, $p2, floatval($reqData['score'])); $out['actionsuccess'] = true; $out['p1rating'] = $p1['rating']; $out['p2rating'] = $p2['rating']; unset($out['p1rating']['rpdata']); unset($out['p2rating']['rpdata']); $dispatcher->setPrefix(''); // No need for prefix since only usable by server. } public function ladderget($dispatcher, &$reqData, &$out) { global $PokemonServers; include_once dirname(__FILE__) . '/ntbb-ladder.lib.php'; $server = @$PokemonServers[@$reqData['serverid']]; if (!$server || $server['id'] !== 'showdown') { die; } $ladder = new NTBBLadder(@$reqData['format']); $user = $this->getUserData(@$reqData['user']); if (!$user) die; $ladder->getAllRatings($user); $out = $user['ratings']; } public function mmr($dispatcher, &$reqData, &$out) { global $PokemonServers; include_once dirname(__FILE__) . '/ntbb-ladder.lib.php'; $server = $dispatcher->findServer(); if (!$server || $server['id'] !== 'showdown') { $out['errorip'] = "Your version of PS is too old for this ladder system. Please update."; return; } $ladder = new NTBBLadder(@$reqData['format']); $user = $this->getUserData(@$reqData['user']); $out = 1000; if ($user) { $ladder->getRating($user); if (@$user['rating']) { $out = intval($user['rating']['elo']); } } } }