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