· 6 years ago · Apr 03, 2019, 05:52 PM
1<?php
2error_reporting(0);
3
4/*
5Hello, Nostalgia here!
6Hostname - External hostname
7External - External path to play
8SecretKey - reCaptcha secret key, leave blank to disable
9Cloudflare - Is host behind cloudflare?
10Activate - Activate user by default (true) or send verification email (false)
11Approve - Approve usernames by default or leave them for manual approval
12Clean - Delete old inactive accounts?
13CleanDays - Number of days before inactive accounts expire
14ForceCase - Force CamelCase on usernames
15AllowedChars - Allowed characters in usernames
16EmailWhitelist - List of allowed email domains, can be array or path to list file, leave blank to disable
17MaxPerEmail - Max no of accounts per email
18Database
19 Host - MySQL host
20 User - MySQL user
21 Pass - MySQL password
22 Name - Database name
23*/
24
25$config = [
26 "Hostname" => "localhost",
27 "External" => "http://play.localhost/",
28 "SecretKey" => "KEY",
29 "Cloudflare" => false,
30 "Activate" => false,
31 "Approve" => false,
32 "Clean" => true,
33 "CleanDays" => 10,
34 "ForceCase" => true,
35 "AllowedChars" => "A-Za-z0-9",
36 // "EmailWhitelist" => ["gmail.com", "hotmail.com"]
37 // "EmailWhitelist" => "/path/to/whitelist"
38 "EmailWhitelist" => [],
39 "MaxPerEmail" => 5,
40 "Database" => [
41 "Host" => "127.0.0.1",
42 "User" => "root",
43 "Pass" => "",
44 "Name" => "DATABASENAME"
45 ]
46];
47
48
49final class Database extends PDO {
50
51 private $connection = null;
52
53 public function __construct($host, $user, $password, $database) {
54 $connectionString = sprintf("mysql:dbname=%s;host=%s", $database, $host);
55
56 parent::__construct($connectionString, $user, $password);
57 }
58
59 public function encryptPassword($password, $md5 = true) {
60 if($md5 !== false) {
61 $password = md5($password);
62 }
63 $hash = substr($password, 16, 16) . substr($password, 0, 16);
64 return $hash;
65 }
66
67 public function getLoginHash($password, $staticKey) {
68 $hash = $this->encryptPassword($password, false);
69 $hash .= $staticKey;
70 $hash .= 'Y(02.>\'H}t":E1';
71 $hash = $this->encryptPassword($hash);
72 return $hash;
73 }
74
75 public function addUser($username, $password, $color, $email, $isActive = 0, $approval = 0) {
76 $hashedPassword = strtoupper(md5($password));
77 $staticKey = "houdini";
78 $flashClientHash = $this->getLoginHash($hashedPassword, $staticKey);
79 $bcryptPassword = password_hash($flashClientHash, PASSWORD_DEFAULT, [ "cost" => 12 ]);
80 $insertPenguin = "INSERT INTO `penguin` (`ID`, `Username`, `Nickname`, `Approval`, `Password`, `Email`, `Active`, `Color`) VALUES ";
81 $insertPenguin .= "(NULL, :Username, :Username, :Approval, :Password, :Email, :Active, :Color);";
82
83 $insertStatement = $this->prepare($insertPenguin);
84 $insertStatement->bindValue(":Username", $username);
85 $insertStatement->bindValue(":Password", $bcryptPassword);
86 $insertStatement->bindValue(":Approval", $approval);
87 $insertStatement->bindValue(":Email", $email);
88 $insertStatement->bindValue(":Active", $isActive);
89 $insertStatement->bindValue(":Color", $color);
90
91 $insertStatement->execute();
92 $insertStatement->closeCursor();
93
94 $penguinId = $this->lastInsertId();
95
96 $this->insertInventory($penguinId, $color);
97 $this->addActiveIgloo($penguinId);
98 $this->sendMail($penguinId, null, 125);
99
100 return $penguinId;
101 }
102
103 public function insertInventory($penguinId, $itemId) {
104 $insertInventory = $this->prepare("INSERT INTO `inventory` (`PenguinID`, `ItemID`) VALUES (:PenguinID, :ItemID);");
105 $insertInventory->bindValue(":PenguinID", $penguinId);
106 $insertInventory->bindValue(":ItemID", $itemId);
107 $insertInventory->execute();
108 $insertInventory->closeCursor();
109 }
110
111 public function sendMail($recipientId, $senderId, $postcardType) {
112 $sendMail = $this->prepare("INSERT INTO `postcard` (`ID`, `SenderID`, `RecipientID`, `Type`) VALUES (NULL, :SenderID, :RecipientID, :Type);");
113 $sendMail->bindValue(":RecipientID", $recipientId);
114 $sendMail->bindValue(":SenderID", $senderId);
115 $sendMail->bindValue(":Type", $postcardType);
116 $sendMail->execute();
117 $sendMail->closeCursor();
118
119 $postcardId = $this->lastInsertId();
120
121 return $postcardId;
122 }
123
124 private function addActiveIgloo($penguinId) {
125 $insertStatement = $this->prepare("INSERT INTO `igloo` (`ID`, `PenguinID`) VALUES (NULL, :PenguinID);");
126 $insertStatement->bindValue(":PenguinID", $penguinId);
127 $insertStatement->execute();
128 $insertStatement->closeCursor();
129
130 $iglooId = $this->lastInsertId();
131 return $iglooId;
132 }
133
134 public function usernameTaken($username) {
135 $usernameTaken = "SELECT Username FROM `penguin` WHERE Username = :Username;";
136
137 $takenQuery = $this->prepare($usernameTaken);
138 $takenQuery->bindValue(":Username", $username);
139 $takenQuery->execute();
140
141 $rowCount = $takenQuery->rowCount();
142 $takenQuery->closeCursor();
143
144 return $rowCount > 0;
145 }
146
147 public function getEmailCount($email) {
148 $emailCount = "SELECT ID FROM `penguin` WHERE Email = :Email;";
149
150 $emailQuery = $this->prepare($emailCount);
151 $emailQuery->bindValue(":Email", $email);
152 $emailQuery->execute();
153
154 $rowCount = $emailQuery->rowCount();
155 $emailQuery->closeCursor();
156
157 return $rowCount;
158 }
159
160 public function createActivationKey($penguinId, $key) {
161 $insertStatement = $this->prepare("INSERT INTO `activation_key` (`PenguinID`, `ActivationKey`) VALUES (:PenguinID, :Key);");
162 $insertStatement->bindValue(":PenguinID", $penguinId);
163 $insertStatement->bindValue(":Key", $key);
164 $insertStatement->execute();
165 $insertStatement->closeCursor();
166 }
167
168 public function activateUser($penguinId, $key) {
169 $setActive = $this->prepare("UPDATE `penguin` INNER JOIN activation_key on penguin.ID = activation_key.PenguinID " .
170 "SET penguin.Active = 1 WHERE activation_key.ActivationKey = :Key;");
171 $setActive->bindValue(":Key", $key);
172 $setActive->execute();
173 if($setActive->rowCount() > 0) {
174 $deleteActivation = $this->prepare("DELETE FROM `activation_key` WHERE `PenguinID` = :PenguinID");
175 $deleteActivation->bindValue(":PenguinID", $penguinId);
176 $deleteActivation->execute();
177 }
178 $setActive->closeCursor();
179 $deleteActivation->closeCursor();
180 }
181
182 public function takenUsernames($username) {
183 $usernamesTaken = "SELECT Username FROM `penguin` WHERE Username LIKE :Username;";
184
185 $usernamesQuery = $this->prepare($usernamesTaken);
186 $usernamesQuery->bindValue(":Username", $username . "%");
187 $usernamesQuery->execute();
188
189 $usernames = $usernamesQuery->fetchAll(self::FETCH_COLUMN);
190 return $usernames;
191 }
192
193 public function cleanInactive($expiry = 10) {
194 $deleteInactive = "DELETE FROM `penguin` WHERE Active = 0 AND RegistrationDate < :Expiry;";
195
196 $deleteQuery = $this->prepare($deleteInactive);
197 $deleteQuery->bindValue(":Expiry", date("Y-m-d", strtotime("-$expiry days", time())));
198 $deleteQuery->execute();
199 }
200
201}
202
203$localization = [
204 "en" => [
205 "terms" => "You must agree to the Rules and Terms of Use.",
206 "name_missing" => "You need to name your penguin.",
207 "name_short" => "Penguin name is too short.",
208 "name_number" => "Penguin names can only contain 5 numbers.",
209 "penguin_letter" => "Penguin names must contain at least 1 letter.",
210 "name_not_allowed" => "That penguin name is not allowed.",
211 "name_taken" => "That penguin name is already taken.",
212 "name_suggest" => "That penguin name is already taken. Try {suggestion}.",
213 "passwords_match" => "Passwords do not match.",
214 "password_short" => "Password is too short.",
215 "email_invalid" => "Invalid email address."
216 ],
217 "fr" => [
218 "terms" => "Tu dois accepter les conditions d'utilisation.",
219 "name_missing" => "Tu dois donner un nom à ton pingouin.",
220 "name_short" => "Le nom de pingouin est trop court.",
221 "name_number" => "Un nom de pingouin ne peut contenir plus de 5 nombres.",
222 "penguin_letter" => "Un nom de pingouin doit contenir au moins une lettre.",
223 "name_not_allowed" => "Ce nom de pingouing n'est pas autorisé.",
224 "name_taken" => "Ce nom de pingouin est pris.",
225 "name_suggest" => "Ce nom de pingouin est pris. Essaye {suggestion}.",
226 "passwords_match" => "Les mots de passes ne correspondent pas.",
227 "password_short" => "Le mot de passe est trop court.",
228 "email_invalid" => "Adresse email invalide."
229 ],
230 "es" => [
231 "terms" => "Debes seguir las reglas y los términos de uso.",
232 "name_missing" => "Debes escoger un nombre para tu pingüino.",
233 "name_short" => "El nombre de tu pingüino es muy corto.",
234 "name_number" => "Los nombres de usuario sólo pueden tener 5 números.",
235 "penguin_letter" => "Los nombres de usuario deben tener por lo menos 1 letra.",
236 "name_not_allowed" => "Ese nombre de usuario no está permitido.",
237 "name_taken" => "Ese nombre de usuario ya ha sido escogido.",
238 "name_suggest" => "Ese nombre de usuario ya ha sido escogido. Intenta éste {suggestion}.",
239 "passwords_match" => "Las contraseñas no coinciden.",
240 "password_short" => "La contraseña es muy corta.",
241 "email_invalid" => "El correo eléctronico es incorrecto."
242 ],
243 "pt" => [
244 "terms" => "Você precisa concordar com as Regras e com os Termos de Uso.",
245 "name_missing" => "Você precisa nomear seu pinguim.",
246 "name_short" => "O nome do pinguim é muito curto.",
247 "name_number" => "O nome do pinguim só pode conter 5 números",
248 "penguin_letter" => "O nome do seu pinguim tem de conter pelo menos uma letra.",
249 "name_not_allowed" => "Esse nome de pinguim não é permitido.",
250 "name_taken" => "Esse nome de pinguim já foi escolhido.",
251 "name_suggest" => "Esse nome de pinguim já foi escolhido. Tente {suggestion}.",
252 "passwords_match" => "As senhas não correspondem.",
253 "password_short" => "A senha é muito curta.",
254 "email_invalid" => "Esse endereço de E-Mail é invalido."
255 ]
256];
257
258if(!is_array($config["EmailWhitelist"]) && !empty($config["EmailWhitelist"])) {
259 $emailWhitelistFile = file_get_contents($config["EmailWhitelist"]);
260 $config["EmailWhitelist"] = explode("\n", $emailWhitelistFile);
261}
262
263if(isset($_GET["key"])) {
264 $db = new Database($config["Database"]["Host"], $config["Database"]["User"],
265 $config["Database"]["Pass"], $config["Database"]["Name"]);
266
267 $key = $_GET["key"];
268 $rawKey = base64_decode($key);
269 $rawKey = explode(":", $rawKey);
270 list($penguinId, $activationKey) = $rawKey;
271
272 $db->activateUser($penguinId, $activationKey);
273
274 header("Location: " . $config["External"]);
275 die($penguinId . $activationKey);
276}
277
278
279session_start();
280
281function response($data) {
282 die(http_build_query($data));
283}
284
285function attemptDataRetrieval($key, $session = false) {
286 if(!$session && array_key_exists($key, $_POST)) {
287 return $_POST[$key];
288 }
289
290 if($session && array_key_exists($key, $_SESSION)) {
291 return $_SESSION[$key];
292 }
293
294 response([
295 "error" => ""
296 ]);
297}
298
299function generateActivationKey($length, $keyspace = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") {
300 $str = "";
301 $max = mb_strlen($keyspace, "8bit") - 1;
302 for ($i = 0; $i < $length; ++$i) {
303 $str .= $keyspace[random_int(0, $max)];
304 }
305 return $str;
306}
307
308function createActivateUrl($baseUrl, $penguinId, $activationKey) {
309 $rawKey = implode(":", [$penguinId, $activationKey]);
310 $key = base64_encode($rawKey);
311 return $baseUrl . "/create_account/create_account.php?key=" . $key;
312}
313
314$action = attemptDataRetrieval("action");
315$lang = attemptDataRetrieval("lang");
316
317if(!in_array($lang, array_keys($localization))) {
318 response([
319 "error" => ""
320 ]);
321}
322
323if($action == "validate_agreement") {
324 $agreeTerms = attemptDataRetrieval("agree_to_terms");
325 $agreeRules = attemptDataRetrieval("agree_to_rules");
326 if(!$agreeTerms || !$agreeRules) {
327 response([
328 "error" => $localization[$lang]["terms"]
329 ]);
330 }
331
332 response([
333 "success" => 1
334 ]);
335} elseif($action == "validate_username") {
336 $username = attemptDataRetrieval("username");
337 $color = attemptDataRetrieval("colour");
338 $colors = range(1, 15);
339
340 if(strlen($username) == 0) {
341 response([
342 "error" => $localization[$lang]["name_missing"]
343 ]);
344 } elseif(strlen($username) < 4 || strlen($username) > 12) {
345 response([
346 "error" => $localization[$lang]["name_short"]
347 ]);
348 } elseif(preg_match_all("/[0-9]/", $username) > 5) {
349 response([
350 "error" => $localization[$lang]["name_number"]
351 ]);
352 } elseif(!preg_match("/[A-z]/i", $username)) {
353 response([
354 "error" => $localization[$lang]["penguin_letter"]
355 ]);
356 } elseif(preg_match("/[^" . $config["AllowedChars"] . "]/", $username)) {
357 response([
358 "error" => $localization[$lang]["name_not_allowed"]
359 ]);
360 } elseif(!is_numeric($color) || !in_array($color, $colors)) {
361 response([
362 "error" => ""
363 ]);
364 }
365
366 $db = new Database($config["Database"]["Host"], $config["Database"]["User"],
367 $config["Database"]["Pass"], $config["Database"]["Name"]);
368
369 if($db->usernameTaken($username)) {
370 $username = preg_replace("/\d+$/", "", $username);
371 $takenUsernames = $db->takenUsernames($username);
372 $i = 1;
373 while(true) {
374 $suggestion = $username . $i++;
375 if(preg_match_all("/[0-9]/", $username) > 1) {
376 response([
377 "error" => $localization[$lang]["name_taken"]
378 ]);
379 }
380 if(!in_array(strtolower($suggestion), $takenUsernames)) {
381 break;
382 }
383 }
384 response([
385 "error" => str_replace("{suggestion}", $suggestion, $localization[$lang]["name_suggest"])
386 ]);
387 }
388
389 $_SESSION["sid"] = session_id();
390 $_SESSION["username"] = ($config["ForceCase"] ? ucfirst(strtolower($username)) : $username);
391 $_SESSION["colour"] = $color;
392
393 response([
394 "success" => 1,
395 "sid" => session_id()
396 ]);
397} elseif($action == "validate_password_email") {
398 $sessionId = attemptDataRetrieval("sid", true);
399 $username = attemptDataRetrieval("username", true);
400 $color = attemptDataRetrieval("colour", true);
401 $password = attemptDataRetrieval("password");
402 $passwordConfirm = attemptDataRetrieval("password_confirm");
403 $email = attemptDataRetrieval("email");
404
405 if(!empty($config["SecretKey"])) {
406 $gtoken = attemptDataRetrieval("gtoken");
407 $data = [
408 "secret" => $config["SecretKey"],
409 "response" => $gtoken,
410 "remoteip" => ($config["Cloudflare"] ? $_SERVER["HTTP_CF_CONNECTING_IP"] : $_SERVER['REMOTE_ADDR'])
411 ];
412 $verify = curl_init();
413 curl_setopt($verify, CURLOPT_URL, "https://www.google.com/recaptcha/api/siteverify");
414 curl_setopt($verify, CURLOPT_POST, true);
415 curl_setopt($verify, CURLOPT_POSTFIELDS, http_build_query($data));
416 curl_setopt($verify, CURLOPT_SSL_VERIFYPEER, false);
417 curl_setopt($verify, CURLOPT_RETURNTRANSFER, true);
418 $response = curl_exec($verify);
419 $result = json_decode($response);
420 }
421
422 $emailDomain = substr(strrchr($email, "@"), 1);
423
424 if($sessionId !== session_id()) {
425 response([
426 "error" => ""
427 ]);
428 } elseif(empty($result->success) && !empty($config["SecretKey"])) {
429 response([
430 "error" => ""
431 ]);
432 } elseif($password !== $passwordConfirm) {
433 response([
434 "error" => $localization[$lang]["passwords_match"]
435 ]);
436 } elseif(strlen($password) < 4) {
437 response([
438 "error" => $localization[$lang]["password_short"]
439 ]);
440 } elseif(!filter_var($email, FILTER_VALIDATE_EMAIL)) {
441 response([
442 "error" => $localization[$lang]["email_invalid"]
443 ]);
444 } elseif(!in_array($emailDomain, $config["EmailWhitelist"]) && !empty($config["EmailWhitelist"])) {
445 response([
446 "error" => $localization[$lang]["email_invalid"]
447 ]);
448 }
449
450 $db = new Database($config["Database"]["Host"], $config["Database"]["User"],
451 $config["Database"]["Pass"], $config["Database"]["Name"]);
452
453 if($db->getEmailCount($email) >= $config["MaxPerEmail"]) {
454 response([
455 "error" => $localization[$lang]["email_invalid"]
456 ]);
457 }
458
459 $penguinId = $db->addUser($username, $password, $color, $email, ($config["Activate"] ? 1 : 0), ($config["Approve"] ? 1 : 0));
460
461 if(!$config["Activate"]) {
462 $activationKey = generateActivationKey(60);
463 $db->createActivationKey($penguinId, $activationKey);
464
465 $activationLink = createActivateUrl($config["External"], $penguinId, $activationKey);
466
467 $headers = "From: noreply@{$config['Hostname']}\r\n";
468 $headers .= "Reply-To: noreply@{$config['Hostname']}\r\n";
469 $headers .= "Return-Path: noreply@{$config['Hostname']}\r\n";
470 $headers .= "MIME-Version: 1.0\r\n";
471 $headers .= "Content-type: text/html; charset=iso-8859-1\r\n";
472 $headers .= "X-Mailer: PHP/" . phpversion();
473
474 ob_start();
475?>
476
477<!doctype html>
478<html>
479 <head>
480 <title>Activate your penguin!</title>
481 </head>
482 <body>
483 <p>Hello,</p>
484 <p>Thank you for creating a penguin on <?php print($config["Hostname"]); ?>. Please click below to activate your penguin account.</p>
485 <a href="<?php print($activationLink); ?>">Activate</a>
486 </body>
487</html>
488
489<?php
490 $emailContent = ob_get_clean();
491 mail($email, "Activate your penguin!", $emailContent, $headers);
492 }
493
494 if($config["Clean"] == true) {
495 $db->cleanInactive($config["CleanDays"]);
496 }
497
498 session_destroy();
499
500 response([
501 "success" => 1
502 ]);
503}
504
505?>