From c50c5ed422610a72ccb8176a9176ac5517a3e2cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=91cze=20Bence?= Date: Sun, 14 Jun 2020 17:11:48 +0200 Subject: [PATCH 1/8] MAPG-69 initialize Request earlier add Request to global Container add base URL to Request --- main.php | 1 + public/index.php | 9 ++++----- src/Interfaces/Request/IRequest.php | 4 ++++ src/Request/Request.php | 19 +++++++++++++++---- web.php | 6 ++++-- 5 files changed, 28 insertions(+), 11 deletions(-) diff --git a/main.php b/main.php index aeab442..c283f94 100644 --- a/main.php +++ b/main.php @@ -15,6 +15,7 @@ class Container static MapGuesser\Interfaces\Database\IConnection $dbConnection; static MapGuesser\Routing\RouteCollection $routeCollection; static \SessionHandlerInterface $sessionHandler; + static MapGuesser\Interfaces\Request\IRequest $request; } Container::$dbConnection = new MapGuesser\Database\Mysql\Connection($_ENV['DB_HOST'], $_ENV['DB_USER'], $_ENV['DB_PASSWORD'], $_ENV['DB_NAME']); diff --git a/public/index.php b/public/index.php index e4b5722..ad425a0 100644 --- a/public/index.php +++ b/public/index.php @@ -2,7 +2,6 @@ require '../web.php'; -$host = $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST']; $method = strtolower($_SERVER['REQUEST_METHOD']); $url = substr($_SERVER['REQUEST_URI'], strlen('/')); if (($pos = strpos($url, '?')) !== false) { @@ -15,10 +14,10 @@ $match = Container::$routeCollection->match($method, explode('/', $url)); if ($match !== null) { list($route, $params) = $match; - $request = new MapGuesser\Request\Request($_GET, $params, $_POST, $_SESSION); + Container::$request->setParsedRouteParams($params); $handler = $route->getHandler(); - $controller = new $handler[0]($request); + $controller = new $handler[0](Container::$request); if ($controller instanceof MapGuesser\Interfaces\Authorization\ISecured) { $authorized = $controller->authorize(); @@ -26,7 +25,7 @@ if ($match !== null) { $authorized = true; } - if ($method === 'post' && $request->post('anti_csrf_token') !== $request->session()->get('anti_csrf_token')) { + if ($method === 'post' && Container::$request->post('anti_csrf_token') !== Container::$request->session()->get('anti_csrf_token')) { header('Content-Type: text/html; charset=UTF-8', true, 403); echo json_encode(['error' => 'no_valid_anti_csrf_token']); return; @@ -41,7 +40,7 @@ if ($match !== null) { return; } elseif ($response instanceof MapGuesser\Interfaces\Response\IRedirect) { - header('Location: ' . $host . '/' . $response->getUrl(), true, $response->getHttpCode()); + header('Location: ' . Container::$request->getBase() . '/' . $response->getUrl(), true, $response->getHttpCode()); return; } diff --git a/src/Interfaces/Request/IRequest.php b/src/Interfaces/Request/IRequest.php index bb9c402..351550f 100644 --- a/src/Interfaces/Request/IRequest.php +++ b/src/Interfaces/Request/IRequest.php @@ -4,6 +4,10 @@ use MapGuesser\Interfaces\Authentication\IUser; interface IRequest { + public function setParsedRouteParams(array &$routeParams); + + public function getBase(): string; + public function query(string $key); public function post(string $key); diff --git a/src/Request/Request.php b/src/Request/Request.php index 2c10f16..fb95646 100644 --- a/src/Request/Request.php +++ b/src/Request/Request.php @@ -3,26 +3,37 @@ use MapGuesser\Interfaces\Authentication\IUser; use MapGuesser\Interfaces\Request\IRequest; use MapGuesser\Interfaces\Request\ISession; -use MapGuesser\Model\User; class Request implements IRequest { + private string $base; + private array $get; - private array $routeParams; + private array $routeParams = []; private array $post; private Session $session; - public function __construct(array &$get, array &$routeParams, array &$post, array &$session) + public function __construct(string $base, array &$get, array &$post, array &$session) { + $this->base = $base; $this->get = &$get; - $this->routeParams = &$routeParams; $this->post = &$post; $this->session = new Session($session); } + public function setParsedRouteParams(array &$routeParams) + { + $this->routeParams = &$routeParams; + } + + public function getBase(): string + { + return $this->base; + } + public function query($key) { if (isset($this->get[$key])) { diff --git a/web.php b/web.php index e00c293..b9ef8a1 100644 --- a/web.php +++ b/web.php @@ -40,6 +40,8 @@ session_start([ 'cookie_samesite' => 'Lax' ]); -if (!isset($_SESSION['anti_csrf_token'])) { - $_SESSION['anti_csrf_token'] = hash('sha256', random_bytes(10) . microtime()); +Container::$request = new MapGuesser\Request\Request($_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST'], $_GET, $_POST, $_SESSION); + +if (!Container::$request->session()->has('anti_csrf_token')) { + Container::$request->session()->set('anti_csrf_token', hash('sha256', random_bytes(10) . microtime())); } From 01db72796b306e662112d356e733ce324a6fcd70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=91cze=20Bence?= Date: Sun, 14 Jun 2020 17:12:29 +0200 Subject: [PATCH 2/8] MAPG-69 fix migration - transaction should not be started in that scope --- database/migrations/data/20200612_2124_map_area.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/database/migrations/data/20200612_2124_map_area.php b/database/migrations/data/20200612_2124_map_area.php index d6460ef..dae5e11 100644 --- a/database/migrations/data/20200612_2124_map_area.php +++ b/database/migrations/data/20200612_2124_map_area.php @@ -10,8 +10,6 @@ $select->columns(['id', 'bound_south_lat', 'bound_west_lng', 'bound_north_lat', $result = $select->execute(); -\Container::$dbConnection->startTransaction(); - while ($map = $result->fetch(IResultSet::FETCH_ASSOC)) { $bounds = Bounds::createDirectly($map['bound_south_lat'], $map['bound_west_lng'], $map['bound_north_lat'], $map['bound_east_lng']); @@ -20,5 +18,3 @@ while ($map = $result->fetch(IResultSet::FETCH_ASSOC)) { $modify->set('area', $bounds->calculateApproximateArea()); $modify->save(); } - -\Container::$dbConnection->commit(); From 334df3462b66098a213990d9545e744af96421ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=91cze=20Bence?= Date: Sun, 14 Jun 2020 17:13:54 +0200 Subject: [PATCH 3/8] MAPG-69 add menu with user info to header --- src/Interfaces/Authentication/IUser.php | 2 ++ src/Model/User.php | 19 +++++++++++++++- views/templates/header.php | 29 ++++++++++++++++++++----- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/src/Interfaces/Authentication/IUser.php b/src/Interfaces/Authentication/IUser.php index 363cfe8..f95824c 100644 --- a/src/Interfaces/Authentication/IUser.php +++ b/src/Interfaces/Authentication/IUser.php @@ -7,4 +7,6 @@ interface IUser const PERMISSION_ADMIN = 1; public function hasPermission(int $permission): bool; + + public function getDisplayName(): string; } diff --git a/src/Model/User.php b/src/Model/User.php index cdc34dd..0814a78 100644 --- a/src/Model/User.php +++ b/src/Model/User.php @@ -6,7 +6,7 @@ class User extends BaseModel implements IUser { private static array $types = ['user', 'admin']; - protected static array $fields = ['email', 'password', 'type']; + protected static array $fields = ['email', 'password', 'type', 'active']; private string $email; @@ -14,6 +14,8 @@ class User extends BaseModel implements IUser private string $type = 'user'; + private bool $active = false; + public function setEmail(string $email): void { $this->email = $email; @@ -36,6 +38,11 @@ class User extends BaseModel implements IUser } } + public function setActive($active): void + { + $this->active = (bool) $active; + } + public function getEmail(): string { return $this->email; @@ -51,6 +58,11 @@ class User extends BaseModel implements IUser return $this->type; } + public function getActive(): bool + { + return $this->active; + } + public function hasPermission(int $permission): bool { switch ($permission) { @@ -63,6 +75,11 @@ class User extends BaseModel implements IUser } } + public function getDisplayName(): string + { + return $this->email; + } + public function checkPassword(string $password): bool { return password_verify($password, $this->password); diff --git a/views/templates/header.php b/views/templates/header.php index 62db163..88d5bf9 100644 --- a/views/templates/header.php +++ b/views/templates/header.php @@ -1,8 +1,25 @@ \ No newline at end of file From 28ed02091a3bac063bc1f1fc243694d78018632a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=91cze=20Bence?= Date: Sun, 14 Jun 2020 17:14:41 +0200 Subject: [PATCH 4/8] MAPG-69 add mailing --- .env.example | 3 ++ composer.json | 3 +- composer.lock | 70 +++++++++++++++++++++++++++++++++++++- docker-compose.yml | 6 ++++ src/Mailing/Mail.php | 81 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 src/Mailing/Mail.php diff --git a/.env.example b/.env.example index cff20cf..cdc2cd4 100644 --- a/.env.example +++ b/.env.example @@ -8,3 +8,6 @@ GOOGLE_MAPS_JS_API_KEY=your_google_maps_js_api_key LEAFLET_TILESERVER_URL=a_leaflet_compatible_tileserver_url LEAFLET_TILESERVER_ATTRIBUTION=attribution_to_be_shown_for_tiles STATIC_ROOT=/static +MAIL_FROM=mapguesser@mapguesser-dev.ch +MAIL_HOST=mail +MAIL_PORT=2500 diff --git a/composer.json b/composer.json index 0b434e7..4b32718 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,8 @@ "license": "GNU GPL 3.0", "require": { "vlucas/phpdotenv": "^4.1", - "symfony/console": "^5.1" + "symfony/console": "^5.1", + "phpmailer/phpmailer": "^6.1" }, "require-dev": {}, "autoload": { diff --git a/composer.lock b/composer.lock index ad2a23a..65cf8b5 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,76 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "13a0eaff2786786caff2be86ac704fc7", + "content-hash": "67a75c3149ef859545476427e7f2f686", "packages": [ + { + "name": "phpmailer/phpmailer", + "version": "v6.1.6", + "source": { + "type": "git", + "url": "https://github.com/PHPMailer/PHPMailer.git", + "reference": "c2796cb1cb99d7717290b48c4e6f32cb6c60b7b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/c2796cb1cb99d7717290b48c4e6f32cb6c60b7b3", + "reference": "c2796cb1cb99d7717290b48c4e6f32cb6c60b7b3", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-filter": "*", + "php": ">=5.5.0" + }, + "require-dev": { + "doctrine/annotations": "^1.2", + "friendsofphp/php-cs-fixer": "^2.2", + "phpunit/phpunit": "^4.8 || ^5.7" + }, + "suggest": { + "ext-mbstring": "Needed to send email in multibyte encoding charset", + "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication", + "league/oauth2-google": "Needed for Google XOAUTH2 authentication", + "psr/log": "For optional PSR-3 debug logging", + "stevenmaguire/oauth2-microsoft": "Needed for Microsoft XOAUTH2 authentication", + "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPMailer\\PHPMailer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-only" + ], + "authors": [ + { + "name": "Marcus Bointon", + "email": "phpmailer@synchromedia.co.uk" + }, + { + "name": "Jim Jagielski", + "email": "jimjag@gmail.com" + }, + { + "name": "Andy Prevost", + "email": "codeworxtech@users.sourceforge.net" + }, + { + "name": "Brent R. Matzelle" + } + ], + "description": "PHPMailer is a full-featured email creation and transfer class for PHP", + "funding": [ + { + "url": "https://github.com/synchro", + "type": "github" + } + ], + "time": "2020-05-27T12:24:03+00:00" + }, { "name": "phpoption/phpoption", "version": "1.7.3", diff --git a/docker-compose.yml b/docker-compose.yml index 3effcfe..b506eb1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,6 +8,7 @@ services: - .:/var/www/mapguesser links: - 'mariadb' + - 'mail' mariadb: image: mariadb:10.1 volumes: @@ -17,5 +18,10 @@ services: MYSQL_DATABASE: 'mapguesser' MYSQL_USER: 'mapguesser' MYSQL_PASSWORD: 'mapguesser' + mail: + image: marcopas/docker-mailslurper:latest + ports: + - 8080:8080 + - 8085:8085 volumes: mysql: diff --git a/src/Mailing/Mail.php b/src/Mailing/Mail.php new file mode 100644 index 0000000..4c75dcb --- /dev/null +++ b/src/Mailing/Mail.php @@ -0,0 +1,81 @@ +recipients[] = [$mail, $name]; + } + + public function setSubject(string $subject): void + { + $this->subject = $subject; + } + + public function setBody(string $body): void + { + $this->body = $body; + } + + public function setBodyFromTemplate(string $template, array $params = []): void + { + $this->body = file_get_contents(ROOT . '/mail/' . $template . '.tpl'); + + foreach ($params as $key => $param) { + $this->body = str_replace('{{' . $key . '}}', $param, $this->body); + } + } + + public function send(): void + { + $mailer = new PHPMailer(true); + + $mailer->CharSet = 'utf-8'; + $mailer->Hostname = substr($_ENV['MAIL_FROM'], strpos($_ENV['MAIL_FROM'], '@') + 1); + + if (!empty($_ENV['MAIL_HOST'])) { + $mailer->Mailer = 'smtp'; + $mailer->Host = $_ENV['MAIL_HOST']; + $mailer->Port = !empty($_ENV['MAIL_PORT']) ? $_ENV['MAIL_PORT'] : 25; + $mailer->SMTPSecure = !empty($_ENV['MAIL_SECURE']) ? $_ENV['MAIL_SECURE'] : ''; + + if (!empty($_ENV['MAIL_USER'])) { + $mailer->SMTPAuth = true; + $mailer->Username = $_ENV['MAIL_USER']; + $mailer->Password = $_ENV['MAIL_PASSWORD']; + } else { + $mailer->SMTPAuth = false; + } + } else { + $mailer->Mailer = 'mail'; + } + + $mailer->setFrom($_ENV['MAIL_FROM'], 'MapGuesser'); + $mailer->addReplyTo($_ENV['MAIL_FROM'], 'MapGuesser'); + + $mailer->Sender = !empty($_ENV['MAIL_BOUNCE']) ? $_ENV['MAIL_BOUNCE'] : $_ENV['MAIL_FROM']; + + $mailer->Subject = $this->subject; + $mailer->Body = $this->body; + + foreach ($this->recipients as $recipient) { + $this->sendMail($mailer, $recipient); + } + } + + private function sendMail(PHPMailer $mailer, array $recipient) + { + $mailer->clearAddresses(); + $mailer->addAddress($recipient[0], $recipient[1]); + + $mailer->send(); + } +} From 66b21ec7100d598f8c53f9195add287ef31ce22d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=91cze=20Bence?= Date: Sun, 14 Jun 2020 17:16:08 +0200 Subject: [PATCH 5/8] MAPG-69 add active flag for users and check if user is active before login --- .../data/20200614_1328_user_confirmation.php | 17 +++++++++++++++++ .../20200614_1328_user_confirmation.sql | 14 ++++++++++++++ public/static/js/login.js | 5 ++++- src/Controller/LoginController.php | 7 ++++++- views/login.php | 2 +- 5 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 database/migrations/data/20200614_1328_user_confirmation.php create mode 100644 database/migrations/structure/20200614_1328_user_confirmation.sql diff --git a/database/migrations/data/20200614_1328_user_confirmation.php b/database/migrations/data/20200614_1328_user_confirmation.php new file mode 100644 index 0000000..fd7aa0b --- /dev/null +++ b/database/migrations/data/20200614_1328_user_confirmation.php @@ -0,0 +1,17 @@ +columns(['id']); + +$result = $select->execute(); + +while ($map = $result->fetch(IResultSet::FETCH_ASSOC)) { + $modify = new Modify(\Container::$dbConnection, 'users'); + $modify->setId($map['id']); + $modify->set('active', true); + $modify->save(); +} diff --git a/database/migrations/structure/20200614_1328_user_confirmation.sql b/database/migrations/structure/20200614_1328_user_confirmation.sql new file mode 100644 index 0000000..92ad62c --- /dev/null +++ b/database/migrations/structure/20200614_1328_user_confirmation.sql @@ -0,0 +1,14 @@ +CREATE TABLE `user_confirmations` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `user_id` int(10) unsigned NOT NULL, + `token` varchar(64) NOT NULL, + PRIMARY KEY (`id`), + KEY `user_id` (`user_id`), + KEY `token` (`token`), + CONSTRAINT `user_confirmations_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; + +ALTER TABLE + `users` +ADD + `active` tinyint(1) NOT NULL DEFAULT 0; diff --git a/public/static/js/login.js b/public/static/js/login.js index 809b49e..22f306c 100644 --- a/public/static/js/login.js +++ b/public/static/js/login.js @@ -13,7 +13,10 @@ var errorText; switch (this.response.error) { case 'user_not_found': - errorText = 'No user found with the given email address.'; + errorText = 'No user found with the given email address. You can sign up here!'; + break; + case 'user_not_active': + errorText = 'User found with the given email address, but the account is not activated. Please check your email and click on the activation link!'; break; case 'password_not_match': errorText = 'The given password is wrong.' diff --git a/src/Controller/LoginController.php b/src/Controller/LoginController.php index 5f124d4..a8227af 100644 --- a/src/Controller/LoginController.php +++ b/src/Controller/LoginController.php @@ -53,6 +53,11 @@ class LoginController $user = new User($userData); + if (!$user->getActive()) { + $data = ['error' => 'user_not_active']; + return new JsonContent($data); + } + if (!$user->checkPassword($this->request->post('password'))) { $data = ['error' => 'password_not_match']; return new JsonContent($data); @@ -68,6 +73,6 @@ class LoginController { $this->request->session()->delete('user'); - return new Redirect([\Container::$routeCollection->getRoute('login'), []], IRedirect::TEMPORARY); + return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY); } } diff --git a/views/login.php b/views/login.php index de040c1..4d74970 100644 --- a/views/login.php +++ b/views/login.php @@ -11,7 +11,7 @@ $jsFiles = [
-

+

From a69ba3a99b7e8f627ca3d503459dcc2865b7f295 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=91cze=20Bence?= Date: Sun, 14 Jun 2020 17:16:47 +0200 Subject: [PATCH 6/8] MAPG-69 unifying some style and text --- public/static/css/mapguesser.css | 26 ++++++------ public/static/js/mapguesser.js | 2 +- views/admin/map_editor.php | 68 ++++++++++++++++---------------- views/game.php | 24 ++++++----- views/templates/header.php | 44 ++++++++++----------- views/templates/main_header.php | 2 +- 6 files changed, 78 insertions(+), 88 deletions(-) diff --git a/public/static/css/mapguesser.css b/public/static/css/mapguesser.css index 1fec7b8..377a7a2 100644 --- a/public/static/css/mapguesser.css +++ b/public/static/css/mapguesser.css @@ -69,10 +69,6 @@ sub { bottom: -0.4em; } -.mono { - font-family: 'Roboto Mono', monospace; -} - .bold { font-weight: 500; } @@ -261,13 +257,18 @@ div.modal { visibility: hidden; } -p.formError { +p.error, p.formError { color: #7f2929; font-weight: 500; +} + +p.formError { display: none; } div.header { + display: grid; + grid-template-columns: auto auto; background-color: #333333; height: 50px; line-height: 50px; @@ -275,30 +276,25 @@ div.header { color: white; } -div.header>div.grid { - display: grid; - grid-template-columns: auto auto; -} - div.header.small { height: 40px; line-height: 40px; } -div.header>div.grid>:nth-child(2) { +div.header>p.header { line-height: inherit; text-align: right; } -div.header>div.grid>:nth-child(2)>span { +div.header>p.header>span { padding-left: 6px; } -div.header>div.grid>:nth-child(2)>span>a:link, div.header>div.grid>:nth-child(2)>span>a:visited { +div.header>p.header>span>a:link, div.header>p.header>span>a:visited { color: inherit; } -div.header>div.grid>:nth-child(2)>span:not(:last-child) { +div.header>p.header>span:not(:last-child) { border-right: solid white 1px; padding-right: 6px; } @@ -337,7 +333,7 @@ div.box { } @media screen and (max-width: 599px) { - div.header.small h1 span { + div.header h1 span { display: none; } button, a.button { diff --git a/public/static/js/mapguesser.js b/public/static/js/mapguesser.js index f870be9..9c7f26d 100644 --- a/public/static/js/mapguesser.js +++ b/public/static/js/mapguesser.js @@ -72,7 +72,7 @@ var MapGuesser = { closeButton.classList.add('gray'); closeButton.classList.add('marginTop'); - closeButton.textContent = 'Cancel'; + closeButton.textContent = 'Close'; closeButton.onclick = function () { MapGuesser.hideModal(); }; diff --git a/views/admin/map_editor.php b/views/admin/map_editor.php index fcb92c1..a3e2360 100644 --- a/views/admin/map_editor.php +++ b/views/admin/map_editor.php @@ -14,41 +14,39 @@ $jsFiles = [ ?>
-
-

- - - MapGuesser - -

-

- - - - - - 0 - - - 0 - - - - 0 -

-
+

+ + + MapGuesser + +

+

+ + + + + + 0 + + + 0 + + + + 0 +