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