Compare commits

..
Author SHA1 Message Date
bence 22f4abd82d MAPG-80 mark guessable area on static maps
default-pipeline default-pipeline #198
2021-01-01 22:41:17 +01:00
142 changed files with 4399 additions and 5884 deletions

No files matched your search

-9
View File
@@ -1,5 +1,4 @@
APP_NAME=MapGuesser APP_NAME=MapGuesser
APP_URL=mapguesser.dev
DEV=1 DEV=1
DB_HOST=mariadb DB_HOST=mariadb
DB_USER=mapguesser DB_USER=mapguesser
@@ -8,7 +7,6 @@ DB_NAME=mapguesser
GOOGLE_MAPS_SERVER_API_KEY=your_google_maps_server_api_key GOOGLE_MAPS_SERVER_API_KEY=your_google_maps_server_api_key
GOOGLE_MAPS_JS_API_KEY=your_google_maps_js_api_key GOOGLE_MAPS_JS_API_KEY=your_google_maps_js_api_key
LEAFLET_TILESERVER_URL=a_leaflet_compatible_tileserver_url LEAFLET_TILESERVER_URL=a_leaflet_compatible_tileserver_url
LEAFLET_TILESERVER_SUBDOMAINS=list_of_subdomains_for_the_tileserver_without_separators
LEAFLET_TILESERVER_ATTRIBUTION=attribution_to_be_shown_for_tiles LEAFLET_TILESERVER_ATTRIBUTION=attribution_to_be_shown_for_tiles
STATIC_ROOT=/static STATIC_ROOT=/static
MAIL_FROM=mapguesser@mapguesser-dev.ch MAIL_FROM=mapguesser@mapguesser-dev.ch
@@ -17,10 +15,3 @@ MAIL_PORT=2500
GOOGLE_OAUTH_CLIENT_ID=your_google_oauth_client_id GOOGLE_OAUTH_CLIENT_ID=your_google_oauth_client_id
GOOGLE_OAUTH_CLIENT_SECRET=your_google_oauth_client_secret GOOGLE_OAUTH_CLIENT_SECRET=your_google_oauth_client_secret
GOOGLE_ANALITICS_ID=your_google_analytics_id GOOGLE_ANALITICS_ID=your_google_analytics_id
MULTI_INTERNAL_HOST=multi
MULTI_INTERNAL_PORT=5000
MULTI_WS_URL=mapguesser-dev.ch:8090
MULTI_WS_PORT=8090
ENABLE_GAME_FOR_GUESTS=0
RECAPTCHA_SITEKEY=your_recaptcha_sitekey
RECAPTCHA_SECRET=your_recaptcha_secret
-1
View File
@@ -1,4 +1,3 @@
.env .env
installed installed
vendor vendor
node_modules
-8
View File
@@ -9,14 +9,6 @@
"pathMappings": { "pathMappings": {
"/var/www/mapguesser": "${workspaceRoot}", "/var/www/mapguesser": "${workspaceRoot}",
} }
},
{
"name": "Listen for NodeJS Inspector in Docker",
"type": "node",
"request": "attach",
"port": 9229,
"localRoot": "${workspaceFolder}",
"remoteRoot": "/var/www/mapguesser"
} }
] ]
} }
Vendored
-62
View File
@@ -1,62 +0,0 @@
pipeline {
agent {
node {
label 'mapguesser'
customWorkspace 'workspace/mapguesser'
}
}
stages {
stage('Install composer') {
environment {
COMPOSER_HOME="${WORKSPACE}/.composer"
}
agent {
dockerfile {
filename 'docker/Dockerfile-test'
dir '.'
reuseNode true
}
}
steps {
sh 'composer install'
}
}
stage('Unit Testing') {
agent {
dockerfile {
filename 'docker/Dockerfile-test'
dir '.'
reuseNode true
}
}
steps {
sh 'vendor/bin/phpunit --log-junit unit_test_results.xml --testdox tests'
}
post {
success {
archiveArtifacts 'unit_test_results.xml'
}
}
}
stage('Static Code Analysis') {
agent {
dockerfile {
filename 'docker/Dockerfile-test'
dir '.'
reuseNode true
}
}
steps {
sh 'php -d memory_limit=1G vendor/bin/phpstan analyse -c phpstan.neon --error-format=prettyJson > static_code_analysis_results.json'
}
post {
success {
archiveArtifacts 'static_code_analysis_results.json'
}
}
}
}
}
+9 -44
View File
@@ -1,7 +1,5 @@
# MapGuesser # MapGuesser
[![Build Status](https://jenkins.e5tv.hu/job/mapguesser/job/develop/badge/icon)](https://jenkins.e5tv.hu/job/mapguesser/job/develop/)
This is the MapGuesser Application project. This is a game about guessing where you are based on a street view panorama - inspired by existing applications. This is the MapGuesser Application project. This is a game about guessing where you are based on a street view panorama - inspired by existing applications.
## Installation ## Installation
@@ -16,7 +14,7 @@ git clone https://gitea.e5tv.hu/esoko/mapguesser.git
All the commands listed here should be executed from the repository root. All the commands listed here should be executed from the repository root.
### Setup Docker stack (recommended) ### (Optional) Setup Docker stack
The easiest way to build up a fully working application with web server and database is to use Docker Compose with the included `docker-compose.yml`. The easiest way to build up a fully working application with web server and database is to use Docker Compose with the included `docker-compose.yml`.
@@ -26,17 +24,9 @@ All you have to do is executing the following command:
docker-compose up -d docker-compose up -d
``` ```
Attach shell to the container of `mapguesser_app`: Attach shell to the container of `mapguesser_app`. All of the following commands should be executed there.
``` **If you don't use the Docker stack you need to install your environment manually. Check `docker-compose.yml` and `docker/Dockerfile` to see the system requirements.**
docker exec -it mapguesser_app_1 bash
```
All of the following commands should be executed there.
### Manual setup (alternative)
If you don't use the Docker stack you need to install your environment manually. Check `docker-compose.yml` and `docker/Dockerfile` to see the system requirements.
### Initialize project ### Initialize project
@@ -50,34 +40,15 @@ composer create-project
The `.env` file contains several environment variables that are needed by the application to work properly. These should be configured for your environment. The `.env` file contains several environment variables that are needed by the application to work properly. These should be configured for your environment.
One very important variable is `DEV`. This indicates that the application operates in development (staging) and not in production mode. **You should set here the API keys that enable playing the game. Without these API keys the application cannot work well. To get Google API keys visit this page: https://console.developers.google.com/**
**Hint:** If you install the application in the Docker stack for development (staging) environment, only the variables for external dependencies (API keys, map attribution, etc.) should be adapted. All other variables (for DB connection, static root, mailing, multiplayer, etc.) are fine with the default value. One very important variable is `DEV`. This indicates that the application operates in development (staging) and not in produciton mode.
#### API keys If you install the application in the Docker stack for development (staging) environment, only the variables for external dependencies (API keys, map attribution, etc.) should be adapted. All other variables (for DB connection, static root, mailing, etc.) are fine with the default value.
**You should set the API keys that enable playing the game. Without these API keys the application cannot work well. To get Google API keys visit this page: https://console.developers.google.com/**
Required Google APIs:
* **Maps JavaScript API**: for the interactive maps and street views
* **Maps Static API**: for the static map images
* **Street View Static API**: for the backend metadata requests
Required API keys:
* **GOOGLE_MAPS_SERVER_API_KEY**: this it used by the backend and should have access to **Street View Static API**
* **GOOGLE_MAPS_JS_API_KEY**: this is used by the frontend and should have access to **Maps JavaScript API** and **Maps Static API**
Additionally, a tile provider is also needed for map editor. This should be configured by `LEAFLET_TILESERVER_URL`, `LEAFLET_TILESERVER_SUBDOMAINS` and `LEAFLET_TILESERVER_ATTRIBUTION`. You can find some providers here: https://wiki.openstreetmap.org/wiki/Tile_servers. OpenStreetMap's tile server is fine for testing.
Example:
```
LEAFLET_TILESERVER_URL=https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png
LEAFLET_TILESERVER_SUBDOMAINS=abc
LEAFLET_TILESERVER_ATTRIBUTION="&copy; <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors"
```
### (Production only) Create cron job ### (Production only) Create cron job
To maintain database (delete inactive users, old sessions etc.), the command `db:maintain` should be regularly executed. It is recommended to create a cron job that runs every hour: To maintain database (delete inactive users, old sessions etc.), the command `db:maintain` should be regularly executed. It is recommened to create a cron job that runs every hour:
``` ```
0 * * * * /path/to/your/installation/mapg db:maintain >>/var/log/cron-mapguesser.log 2>&1 0 * * * * /path/to/your/installation/mapg db:maintain >>/var/log/cron-mapguesser.log 2>&1
@@ -85,19 +56,13 @@ To maintain database (delete inactive users, old sessions etc.), the command `db
### Finalize installation ### Finalize installation
After you followed the above steps, execute the following command: After you set the environment variables in the `.env` file, execute the following command:
``` ```
scripts/install.sh scripts/install.sh
``` ```
**Warning: Because of a known issue the image `mapguesser_multi` fails to run without the installation steps. You have to relauch `docker-compose up -d` after you finished the installation process.** **And you are done!** The application is ready to use and develop.
**And you are done!** The application is ready to use and develop. In development mode an administrative user is also created by the installation script, email is **mapg@mapg.dev**, password is **123456**. In production mode you should create the first administrative user with the following command:
```
./mapg user:add EMAIL PASSWORD admin
```
If you installed it in the Docker stack, you can reach it on http://localhost. The mails that are sent by the application can be found on http://localhost:8080/. If needed, the database server can be directly reached on localhost:3306. If you installed it in the Docker stack, you can reach it on http://localhost. The mails that are sent by the application can be found on http://localhost:8080/. If needed, the database server can be directly reached on localhost:3306.
+5 -10
View File
@@ -3,19 +3,14 @@
"type": "project", "type": "project",
"description": "MapGuesser Application", "description": "MapGuesser Application",
"license": "GNU GPL 3.0", "license": "GNU GPL 3.0",
"repositories": [
{
"url": "https://git.esoko.eu/esoko/soko-web.git",
"type": "git"
}
],
"require": { "require": {
"esoko/soko-web": "0.10", "vlucas/phpdotenv": "^4.1",
"fzaninotto/faker": "^1.9" "symfony/console": "^5.1",
"phpmailer/phpmailer": "^6.1"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^9.6", "phpunit/phpunit": "^9",
"phpstan/phpstan": "^1.10" "phpstan/phpstan": "^0.12.32"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
Generated
+720 -1079
View File
File diff suppressed because it is too large. Load diff
@@ -1,8 +1,8 @@
<?php <?php
use SokoWeb\Database\Query\Modify; use MapGuesser\Database\Query\Modify;
use SokoWeb\Database\Query\Select; use MapGuesser\Database\Query\Select;
use SokoWeb\Interfaces\Database\IResultSet; use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Util\Geo\Bounds; use MapGuesser\Util\Geo\Bounds;
$select = new Select(\Container::$dbConnection, 'maps'); $select = new Select(\Container::$dbConnection, 'maps');
@@ -1,8 +1,8 @@
<?php <?php
use SokoWeb\Database\Query\Modify; use MapGuesser\Database\Query\Modify;
use SokoWeb\Database\Query\Select; use MapGuesser\Database\Query\Select;
use SokoWeb\Interfaces\Database\IResultSet; use MapGuesser\Interfaces\Database\IResultSet;
$select = new Select(\Container::$dbConnection, 'users'); $select = new Select(\Container::$dbConnection, 'users');
$select->columns(['id']); $select->columns(['id']);
@@ -1,9 +0,0 @@
CREATE TABLE `multi_rooms` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`room_id` varchar(6) NOT NULL,
`state` text NOT NULL,
`members` text NOT NULL,
`updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `room_id` (`room_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
@@ -1,12 +0,0 @@
CREATE TABLE `user_played_place` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL,
`place_id` int(10) unsigned NOT NULL,
`last_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`occurrences` int(10) NOT NULL DEFAULT 1,
PRIMARY KEY(`id`),
KEY `user_id` (`user_id`),
KEY `place_id` (`place_id`),
CONSTRAINT `user_played_place_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`),
CONSTRAINT `user_played_place_place_id` FOREIGN KEY (`place_id`) REFERENCES `places` (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
@@ -1,54 +0,0 @@
CREATE TABLE `challenges` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`token` int(10) unsigned NOT NULL,
`time_limit` int(10) unsigned,
`time_limit_type` enum('game', 'round') NOT NULL DEFAULT 'game',
`no_move` tinyint(1) NOT NULL DEFAULT 0,
`no_pan` tinyint(1) NOT NULL DEFAULT 0,
`no_zoom` tinyint(1) NOT NULL DEFAULT 0,
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
CREATE TABLE `user_in_challenge` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL,
`challenge_id` int(10) unsigned NOT NULL,
`current_round` smallint(5) signed NOT NULL DEFAULT 0,
`time_left` int(10) unsigned,
`is_owner` tinyint(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
KEY `challenge_id` (`challenge_id`),
CONSTRAINT `user_in_challenge_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`),
CONSTRAINT `user_in_challenge_challenge_id` FOREIGN KEY (`challenge_id`) REFERENCES `challenges` (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
CREATE TABLE `place_in_challenge` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`place_id` int(10) unsigned NOT NULL,
`challenge_id` int(10) unsigned NOT NULL,
`round` smallint(5) unsigned NOT NULL,
PRIMARY KEY (`id`),
KEY `place_id` (`place_id`),
KEY `challenge_id` (`challenge_id`),
CONSTRAINT `place_in_challenge_place_id` FOREIGN KEY (`place_id`) REFERENCES `places` (`id`),
CONSTRAINT `place_in_challenge_challenge_id` FOREIGN KEY (`challenge_id`) REFERENCES `challenges` (`id`),
CONSTRAINT `unique_order_in_challenge` UNIQUE (`round`, `challenge_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
CREATE TABLE `guesses` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL,
`place_in_challenge_id` int(10) unsigned NOT NULL,
`lat` decimal(8,6) NOT NULL,
`lng` decimal(9,6) NOT NULL,
`score` int(10) NOT NULL,
`distance` int(10) NOT NULL,
`time_spent` int(10),
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
KEY `place_in_challenge_id` (`place_in_challenge_id`),
CONSTRAINT `guesses_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`),
CONSTRAINT `guesses_place_in_challenge_id` FOREIGN KEY (`place_in_challenge_id`) REFERENCES `place_in_challenge` (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
@@ -1,2 +0,0 @@
ALTER TABLE `maps`
ADD `unlisted` TINYINT(1) NOT NULL DEFAULT 0;
+4 -19
View File
@@ -1,23 +1,14 @@
version: '3' version: '3'
services: services:
app: app:
build: build: ./docker
context: ./docker
dockerfile: Dockerfile-app
ports: ports:
- 80:80 - 80:80
volumes: volumes:
- .:/var/www/mapguesser - .:/var/www/mapguesser
multi: links:
build: - 'mariadb'
context: ./docker - 'mail'
dockerfile: Dockerfile-multi
ports:
- 5000:5000
- 8090:8090
- 9229:9229
volumes:
- .:/var/www/mapguesser
mariadb: mariadb:
image: mariadb:10.3 image: mariadb:10.3
ports: ports:
@@ -29,12 +20,6 @@ services:
MYSQL_DATABASE: 'mapguesser' MYSQL_DATABASE: 'mapguesser'
MYSQL_USER: 'mapguesser' MYSQL_USER: 'mapguesser'
MYSQL_PASSWORD: 'mapguesser' MYSQL_PASSWORD: 'mapguesser'
adminer:
image: adminer:4.8.1-standalone
ports:
- 9090:8080
environment:
- ADMINER_DEFAULT_SERVER=mariadb
mail: mail:
image: marcopas/docker-mailslurper:latest image: marcopas/docker-mailslurper:latest
ports: ports:
File renamed without changes.
-16
View File
@@ -1,16 +0,0 @@
FROM ubuntu:focal
ENV DEBIAN_FRONTEND noninteractive
# Install necessary packages
RUN apt update --fix-missing
RUN apt install -y curl build-essential
# Install Node.js and required packages
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash -
RUN apt install -y nodejs
VOLUME /var/www/mapguesser
WORKDIR /var/www/mapguesser
ENTRYPOINT /usr/bin/node --inspect=0.0.0.0:9229 multi
-6
View File
@@ -1,6 +0,0 @@
FROM ubuntu:focal
ENV DEBIAN_FRONTEND noninteractive
RUN apt update && apt install -y curl git unzip php7.4-cli php7.4-mbstring php7.4-xml
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
+5 -7
View File
@@ -14,12 +14,10 @@ $dotenv->load();
class Container class Container
{ {
static SokoWeb\Interfaces\Database\IConnection $dbConnection; static MapGuesser\Interfaces\Database\IConnection $dbConnection;
static SokoWeb\Interfaces\PersistentData\IPersistentDataManager $persistentDataManager; static MapGuesser\Routing\RouteCollection $routeCollection;
static SokoWeb\Interfaces\Routing\IRouteCollection $routeCollection; static MapGuesser\Interfaces\Session\ISessionHandler $sessionHandler;
static SokoWeb\Interfaces\Session\ISessionHandler $sessionHandler; static MapGuesser\Interfaces\Request\IRequest $request;
static SokoWeb\Interfaces\Request\IRequest $request;
} }
Container::$dbConnection = new SokoWeb\Database\Mysql\Connection($_ENV['DB_HOST'], $_ENV['DB_USER'], $_ENV['DB_PASSWORD'], $_ENV['DB_NAME']); Container::$dbConnection = new MapGuesser\Database\Mysql\Connection($_ENV['DB_HOST'], $_ENV['DB_USER'], $_ENV['DB_PASSWORD'], $_ENV['DB_NAME']);
Container::$persistentDataManager = new SokoWeb\PersistentData\PersistentDataManager(Container::$dbConnection);
+1 -1
View File
@@ -8,6 +8,6 @@ $app = new Symfony\Component\Console\Application('MapGuesser Console', '');
$app->add(new MapGuesser\Cli\MigrateDatabaseCommand()); $app->add(new MapGuesser\Cli\MigrateDatabaseCommand());
$app->add(new MapGuesser\Cli\AddUserCommand()); $app->add(new MapGuesser\Cli\AddUserCommand());
$app->add(new MapGuesser\Cli\LinkViewCommand()); $app->add(new MapGuesser\Cli\LinkViewCommand());
$app->add(new MapGuesser\Cli\MaintainDatabaseCommand()); $app->add(new \MapGuesser\Cli\MaintainDatabaseCommand());
$app->run(); $app->run();
-369
View File
@@ -1,369 +0,0 @@
'use strict';
process.title = 'mapguesser-multi';
class MultiGame {
static ROUND_TIMEOUT_DEFAULT = 120000;
static ROUND_TIMEOUT_MINIMUM = 15000;
static ROUND_TIMEOUT_DIVIDER = 1.5;
static ROUND_TIMEOUT_OFFSET = 500;
constructor() {
this.rooms = new Map();
}
cleanupRooms() {
this.rooms.forEach(function (room, roomId) {
var lastValidDate = new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000);
if (room.updated < lastValidDate) {
this.rooms.delete(roomId);
}
});
}
connectToRoom(roomId, token, connection) {
if (!this.rooms.has(roomId) || !this.rooms.get(roomId).members.has(token)) {
return;
}
var room = this.rooms.get(roomId)
var member = room.members.get(token);
member.connection = connection;
this._sendInitialData(room, member, token);
}
createRoom(roomId) {
this.rooms.set(roomId, { members: new Map(), rounds: [], currentRound: -1, updated: new Date() });
return { ok: true };
}
joinRoom(roomId, token, userName) {
if (!this.rooms.has(roomId)) {
return { error: 'room_not_found' };
}
var room = this.rooms.get(roomId);
room.updated = new Date();
if (room.members.has(token)) {
return { error: 'member_already_joined' };
}
var data = { userName: userName };
var self = this;
room.members.forEach(function (member) {
self._sendToMember(member, 'member_joined', data);
});
room.members.set(token, { userName: userName, connection: null });
return { ok: true };
}
startGame(roomId, places) {
if (!this.rooms.has(roomId)) {
return { error: 'room_not_found' };
}
var room = this.rooms.get(roomId);
room.updated = new Date();
var rounds = [];
places.forEach(function (place) {
rounds.push({
place: place,
results: new Map(),
timeout: MultiGame.ROUND_TIMEOUT_DEFAULT,
timeoutStarted: null,
timeoutHandler: null
})
});
room.rounds = rounds;
this.nextRound(roomId, 0);
return { ok: true };
}
guess(roomId, token, guessPosition, distance, score) {
if (!this.rooms.has(roomId)) {
return { error: 'room_not_found' };
}
var room = this.rooms.get(roomId);
room.updated = new Date();
var round = room.rounds[room.currentRound];
if (round.results.has(token)) {
return { error: 'already_guessed' };
}
var member = room.members.get(token);
var allResults = this._collectResultsInRound(room, round);
this._broadcastGuess(room, member.userName, guessPosition, distance, score);
round.results.set(token, { guessPosition: guessPosition, distance: distance, score: score });
this._setNewTimeout(room, round);
return { allResults: allResults };
}
nextRound(roomId, currentRound) {
if (!this.rooms.has(roomId)) {
return { error: 'room_not_found' };
}
var room = this.rooms.get(roomId);
room.updated = new Date();
room.currentRound = currentRound;
var round = room.rounds[room.currentRound];
round.timeoutStarted = new Date();
var self = this;
round.timeoutHandler = setTimeout(function () {
self._endRound(room, round);
}, round.timeout + MultiGame.ROUND_TIMEOUT_OFFSET);
var data = {};
data.place = { panoId: round.place.panoId, pov: round.place.pov };
data.timeout = round.timeout;
var self = this;
room.members.forEach(function (member) {
self._sendToMember(member, 'new_round', data);
});
return { ok: true };
}
_setNewTimeout(room, round) {
clearTimeout(round.timeoutHandler);
if (room.members.size === round.results.size) {
round.timeout = 0;
round.timeoutStarted = new Date();
this._endRound(room, round);
} else {
round.timeout = round.timeout - (new Date() - round.timeoutStarted);
if (round.timeout > MultiGame.ROUND_TIMEOUT_DIVIDER * MultiGame.ROUND_TIMEOUT_MINIMUM) {
round.timeout = Math.round(round.timeout / MultiGame.ROUND_TIMEOUT_DIVIDER);
} else if (round.timeout > MultiGame.ROUND_TIMEOUT_MINIMUM) {
round.timeout = MultiGame.ROUND_TIMEOUT_MINIMUM;
}
round.timeoutStarted = new Date();
var self = this;
round.timeoutHandler = setTimeout(function () {
self._endRound(room, round);
}, round.timeout + MultiGame.ROUND_TIMEOUT_OFFSET);
this._broadcastTimeout(room, round);
}
}
_endRound(room, round) {
var allResults = this._collectResultsInRound(room, round);
var self = this;
room.members.forEach(function (member, token) {
var result = { guessPosition: null, distance: null, score: 0 };
if (round.results.has(token)) {
result = round.results.get(token);
} else {
round.results.set(token, result);
}
var data = { position: round.place.position, result: result, allResults: allResults };
self._sendToMember(member, 'end_round', data);
});
}
_sendInitialData(room, member, token) {
var data = {};
if (room.currentRound >= 0) {
var round = room.rounds[room.currentRound];
data.place = round.place;
data.timeout = round.timeout - (new Date() - round.timeoutStarted);
}
data.history = [];
for (var i = 0; i <= room.currentRound; ++i) {
var round = room.rounds[i];
if (i === room.currentRound && !round.results.has(token)) {
continue;
}
var result = { guessPosition: null, distance: null, score: 0 };
var allResults = [];
round.results.forEach(function (currentResult, currentToken) {
if (token === currentToken) {
result = currentResult;
return;
}
allResults.push({ userName: room.members.get(currentToken).userName, guessPosition: currentResult.guessPosition, distance: currentResult.distance, score: currentResult.score });
});
data.history.push({
position: round.place.position,
result: result,
allResults: allResults
});
}
data.members = [];
room.members.forEach(function (currentMember) {
data.members.push({ userName: currentMember.userName, me: member === currentMember });
});
data.readyToContinue = room.currentRound >= 0 && room.members.size === room.rounds[room.currentRound].results.size
this._sendToMember(member, 'initialize', data);
}
_collectResultsInRound(room, round) {
var results = [];
round.results.forEach(function (result, token) {
results.push({
userName: room.members.get(token).userName,
guessPosition: result.guessPosition,
distance: result.distance,
score: result.score
});
});
return results;
}
_broadcastTimeout(room, round) {
var self = this;
room.members.forEach(function (member) {
self._sendToMember(member, 'timeout_changed', { timeout: round.timeout });
});
}
_broadcastGuess(room, userName, guessPosition, distance, score) {
var data = { userName: userName, guessPosition: guessPosition, distance: distance, score: score };
var round = room.rounds[room.currentRound];
var self = this;
room.members.forEach(function (member, token) {
if (!round.results.has(token)) {
return;
}
self._sendToMember(member, 'guess', data);
});
}
_sendToMember(member, type, data) {
if (!member.connection) {
return;
}
if (member.connection.readyState !== ws.OPEN) {
member.connection = null;
return;
}
member.connection.send(JSON.stringify({ type: type, data: data }));
}
}
require('dotenv').config();
var
net = require('net'),
ws = require('ws');
var multiGame = new MultiGame();
//TODO: following should be in a separate class/function
var tcpServer = net.createServer(function (socket) {
socket.on('data', function (data) {
try {
data = JSON.parse(data);
} catch (e) {
console.error('Cannot parse data: ' + data);
return;
}
var response;
switch (data.func) {
case 'create_room':
response = multiGame.createRoom(data.args.roomId);
break;
case 'join_room':
response = multiGame.joinRoom(data.args.roomId, data.args.token, data.args.userName);
break;
case 'start_game':
response = multiGame.startGame(data.args.roomId, data.args.places);
break
case 'guess':
response = multiGame.guess(data.args.roomId, data.args.token, data.args.guessPosition, data.args.distance, data.args.score);
break;
case 'next_round':
response = multiGame.nextRound(data.args.roomId, data.args.currentRound);
break;
}
socket.write(JSON.stringify(response));
socket.end();
});
});
tcpServer.on('listening', function () {
console.log('[INFO] TCP server started');
});
tcpServer.listen(process.env.MULTI_INTERNAL_PORT);
var wsServer = new ws.Server({ port: process.env.MULTI_WS_PORT });
wsServer.on('connection', function (connection, request) {
console.log('[INFO] New WS connection: ' + request.connection.remoteAddress);
connection.on('message', function (data) {
try {
data = JSON.parse(data);
} catch (e) {
console.error('Cannot parse data: ' + data);
return;
}
switch (data.func) {
case 'connect_to_room':
multiGame.connectToRoom(data.args.roomId, data.args.token, connection);
break;
}
});
connection.on('close', function () {
console.log('[INFO] WS connection ended: ' + request.connection.remoteAddress);
});
});
wsServer.on('listening', function () {
console.log('[INFO] WS server started');
});
setInterval(function () {
multiGame.cleanupRooms();
}, 24 * 60 * 60 * 1000);
-17
View File
@@ -1,17 +0,0 @@
{
"name": "mapguesser-multi",
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"dotenv": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
"integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw=="
},
"ws": {
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz",
"integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw=="
}
}
}
-13
View File
@@ -1,13 +0,0 @@
{
"name": "mapguesser-multi",
"version": "",
"description": "MapGuesser Application - Multiplayer",
"main": "index.js",
"dependencies": {
"dotenv": "^8.2.0",
"ws": "^7.4.4"
},
"scripts": {},
"author": "Pőcze Bence and The MapGuesser Contributors <bence@pocze.ch>",
"license": "GNU AGPL 3.0"
}
+50
View File
@@ -1,3 +1,53 @@
<?php <?php
require '../web.php'; require '../web.php';
$method = strtolower($_SERVER['REQUEST_METHOD']);
$url = substr($_SERVER['REQUEST_URI'], strlen('/'));
if (($pos = strpos($url, '?')) !== false) {
$url = substr($url, 0, $pos);
}
$url = rawurldecode($url);
$match = Container::$routeCollection->match($method, $url == '' ? [] : explode('/', $url));
if ($match !== null) {
list($route, $params) = $match;
Container::$request->setParsedRouteParams($params);
$handler = $route->getHandler();
$controller = new $handler[0](Container::$request);
if ($controller instanceof MapGuesser\Interfaces\Authorization\ISecured) {
$authorized = $controller->authorize();
} else {
$authorized = true;
}
if ($method === 'post' && Container::$request->post('anti_csrf_token') !== Container::$request->session()->get('anti_csrf_token')) {
$content = new MapGuesser\Response\JsonContent(['error' => 'no_valid_anti_csrf_token']);
header('Content-Type: text/html; charset=UTF-8', true, 403);
$content->render();
return;
}
if ($authorized) {
$response = call_user_func([$controller, $handler[1]]);
if ($response instanceof MapGuesser\Interfaces\Response\IContent) {
header('Content-Type: ' . $response->getContentType() . '; charset=UTF-8');
$response->render();
return;
} elseif ($response instanceof MapGuesser\Interfaces\Response\IRedirect) {
header('Location: ' . $response->getUrl(), true, $response->getHttpCode());
return;
}
}
}
$content = new MapGuesser\Response\HtmlContent('error/404');
header('Content-Type: text/html; charset=UTF-8', true, 404);
$content->render();
+3 -118
View File
@@ -15,17 +15,6 @@
right: 0; right: 0;
background-color: #000000; background-color: #000000;
opacity: 0.5; opacity: 0.5;
z-index: 4;
}
#panningBlockerCover {
display: none;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
opacity: 0;
z-index: 3; z-index: 3;
} }
@@ -33,7 +22,7 @@
position: absolute; position: absolute;
bottom: 30px; bottom: 30px;
right: 20px; right: 20px;
z-index: 3; z-index: 2;
} }
#guess.result { #guess.result {
@@ -90,7 +79,7 @@
line-height: 1; line-height: 1;
} }
#distanceInfo>p:nth-child(2), #distanceInfo>p:nth-child(3), #scoreInfo>p:nth-child(2) { #distanceInfo>p:nth-child(2), #scoreInfo>p:nth-child(2) {
display: none; display: none;
} }
@@ -113,105 +102,13 @@
display: none; display: none;
} }
#startMultiGameButton {
display: none;
}
#players > p {
font-size: 14px;
font-weight: bold;
}
#countdown {
position: absolute;
top: 5px;
right: 5px;
height: 28px;
line-height: 28px;
padding: 0 8px;
background-color: #eeeeee;
border: solid 1px #555555;
border-radius: 3px;
opacity: 0.95;
z-index: 5;
visibility: hidden;
}
#countdown.yellow {
background-color: #f7c789;
border: solid 1px #e8a349;
}
#countdown.red {
background-color: #f7a5a5;
border: solid 1px #aa5e5e;
}
#countdown p {
font-size: 16px;
line-height: inherit;
}
#countdown.yellow p {
color: #9c4308;
}
#countdown.red p {
color: #701919;
}
#navigation {
z-index: 2;
}
#goToStart {
display: none;
}
#highscoresTable {
margin: 1em;
border-collapse: collapse;
width: 90%;
}
#highscoresTable td, #highscoresTable th {
border: 1px solid #ddd;
padding: 8px;
}
#highscoresTable tr:nth-child(even) {
background-color: #f2f2f2;
}
#highscoresTable tr:hover {
background-color: #ddd;
}
#highscoresTable th {
padding-top: 12px;
padding-bottom: 12px;
text-align: left;
background-color: #e8a349;
color: white;
}
#highscoresTable tr.ownPlayer {
font-weight: 500;
}
@media screen and (max-width: 899px) {
.hideOnNarrowScreen {
display: none;
}
}
@media screen and (max-width: 599px) { @media screen and (max-width: 599px) {
#mapName { #mapName {
display: none; display: none;
} }
#showGuessButtonContainer { #showGuessButtonContainer {
position: absolute; position: absolute;
left: 65px; left: 20px;
bottom: 30px; bottom: 30px;
right: 20px; right: 20px;
z-index: 2; z-index: 2;
@@ -228,10 +125,6 @@
#scoreBarBase { #scoreBarBase {
width: 100%; width: 100%;
} }
#navigation {
bottom: 25px;
left: 10px;
}
} }
@media screen and (min-width: 600px) { @media screen and (min-width: 600px) {
@@ -275,10 +168,6 @@
#scoreBarBase { #scoreBarBase {
width: 60%; width: 60%;
} }
#navigation {
bottom: 50px;
left: 20px;
}
@media screen and (max-height: 424px) { @media screen and (max-height: 424px) {
#guess { #guess {
top: 10px; top: 10px;
@@ -293,9 +182,5 @@
right: 20px; right: 20px;
bottom: 30px; bottom: 30px;
} }
#navigation {
bottom: 30px;
left: 10px;
}
} }
} }
+1 -17
View File
@@ -1,4 +1,4 @@
.map { #map {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
@@ -7,13 +7,6 @@
z-index: 1; z-index: 1;
} }
#mapSelection img {
display: inline;
width: 1em;
height: 1em;
vertical-align: -0.15em;
}
/* modify the cursor for the Leaflet map */ /* modify the cursor for the Leaflet map */
.leaflet-container { .leaflet-container {
cursor: crosshair; cursor: crosshair;
@@ -55,12 +48,6 @@
visibility: hidden; visibility: hidden;
} }
#overlayControl {
bottom: 20px;
right: 10px;
z-index: 3;
}
#deleteButton { #deleteButton {
display: none; display: none;
} }
@@ -78,9 +65,6 @@
#placeControl { #placeControl {
top: calc(50% + 10px); top: calc(50% + 10px);
} }
.hideOnMobile {
display: none;
}
} }
@media screen and (min-width: 1000px), (max-height: 599px) { @media screen and (min-width: 1000px), (max-height: 599px) {
+13 -77
View File
@@ -12,10 +12,6 @@ html, body {
padding: 0; padding: 0;
} }
body {
background-color: #cccccc;
}
button::-moz-focus-inner, input::-moz-focus-inner { button::-moz-focus-inner, input::-moz-focus-inner {
padding: 0; padding: 0;
border: 0; border: 0;
@@ -31,11 +27,11 @@ main {
color: #ffffff; color: #ffffff;
} }
p, h1, h2, h3, input, textarea, select, button, a, table, label { p, h1, h2, input, textarea, select, button, a {
font-family: 'Roboto', sans-serif; font-family: 'Roboto', sans-serif;
} }
h1, h2, h3 { h1, h2 {
font-weight: 500; font-weight: 500;
} }
@@ -55,11 +51,7 @@ h2, header.small h1 {
font-size: 24px; font-size: 24px;
} }
h3 { p, h2 {
font-size: 18px;
}
p, h2, h3 {
line-height: 150%; line-height: 150%;
} }
@@ -241,7 +233,7 @@ button.green:enabled:hover, button.green:enabled:focus, a.button.green:hover, a.
background-color: #1b7d31; background-color: #1b7d31;
} }
input.text, select, textarea { input, select, textarea {
background-color: #f9fafb; background-color: #f9fafb;
border: solid #c8d2e1 1px; border: solid #c8d2e1 1px;
border-radius: 2px; border-radius: 2px;
@@ -250,26 +242,22 @@ input.text, select, textarea {
font-weight: 300; font-weight: 300;
} }
input.text, select { input, select {
height: 30px; height: 30px;
line-height: 30px; line-height: 30px;
padding: 0 5px; padding: 0 5px;
} }
input[type=checkbox], input[type=radio] {
margin-right: 0.5em;
}
textarea { textarea {
padding: 5px; padding: 5px;
resize: none; resize: none;
} }
input.text.big, select.big, textarea.big, div.inputWithButton>input.text { input.big, select.big, textarea.big, div.inputWithButton>input {
font-size: 18px; font-size: 18px;
} }
input.text.big, select.big, div.inputWithButton>input.text { input.big, select.big, div.inputWithButton>input {
height: 35px; height: 35px;
line-height: 35px; line-height: 35px;
padding: 0 6px; padding: 0 6px;
@@ -284,19 +272,19 @@ input.fullWidth, select.fullWidth, textarea.fullWidth {
width: 100%; width: 100%;
} }
input.text:disabled, select:disabled, textarea:disabled { input:disabled, select:disabled, textarea:disabled {
background-color: #dfdfdf; background-color: #dfdfdf;
border: solid #dfdfdf 1px; border: solid #dfdfdf 1px;
color: #000000; color: #000000;
} }
input.text:focus, select:focus, textarea:focus { input:focus, select:focus, textarea:focus {
background-color: #ffffff; background-color: #ffffff;
border: solid #29457f 2px; border: solid #29457f 2px;
outline: none; outline: none;
} }
input.text:focus, select:focus { input:focus, select:focus {
padding: 0 4px; padding: 0 4px;
} }
@@ -304,16 +292,16 @@ textarea:focus {
padding: 4px; padding: 4px;
} }
input.text.big:focus, select.big:focus { input.big:focus, select.big:focus {
padding: 0 5px; padding: 0 5px;
} }
div.inputWithButton>input.text { div.inputWithButton>input {
width: 100%; width: 100%;
padding: 0 83px 0 6px; padding: 0 83px 0 6px;
} }
div.inputWithButton>input.text:focus { div.inputWithButton>input:focus {
padding: 0 82px 0 5px; padding: 0 82px 0 5px;
} }
@@ -386,7 +374,6 @@ header>p>span:not(:last-child) {
} }
main { main {
background-color: #ffffff;
padding: 6px 12px; padding: 6px 12px;
} }
@@ -451,45 +438,6 @@ div.box {
box-sizing: border-box; box-sizing: border-box;
} }
.circleControl {
position: absolute;
width: 60px;
bottom: 20px;
right: 10px;
}
.circleControl .controlItem {
position: relative;
height: 60px;
margin-top: 10px;
opacity: 70%;
cursor: pointer;
}
.circleControl .controlItem:hover {
opacity: 100%;
}
.circleControl .controlItem div {
position: absolute;
width: 100%;
height: 100%;
}
.circleControl .controlBackground {
width: 100%;
height: 100%;
opacity: 50%;
}
.circleControl .controlIcon {
width: 75%;
height: 75%;
margin: auto;
margin-top: 50%;
transform: translateY(-50%);
}
@media screen and (max-width: 599px) { @media screen and (max-width: 599px) {
header h1 span { header h1 span {
display: none; display: none;
@@ -516,12 +464,6 @@ div.box {
div.box { div.box {
width: initial; width: initial;
} }
.circleControl {
width: 45px;
}
.circleControl .controlItem {
height: 45px;
}
} }
@media screen and (min-width: 600px) { @media screen and (min-width: 600px) {
@@ -551,12 +493,6 @@ div.box {
padding-top: 10px; padding-top: 10px;
padding-bottom: 10px; padding-bottom: 10px;
} }
.circleControl {
width: 45px;
}
.circleControl .controlItem {
height: 45px;
}
} }
@media screen and (min-height: 400px) and (max-height: 499px) { @media screen and (min-height: 400px) and (max-height: 499px) {
-8
View File
@@ -13,10 +13,6 @@ div.mapItem.new {
align-items: center; align-items: center;
} }
div.mapItem.unlisted {
opacity: 0.6;
}
div.mapItem>div.title { div.mapItem>div.title {
background-color: #28a745; background-color: #28a745;
color: white; color: white;
@@ -79,10 +75,6 @@ div.mapItem>div.buttonContainer {
grid-auto-flow: column; grid-auto-flow: column;
} }
#timeLimitType {
margin-left: 1.5em;
}
@media screen and (min-width: 1504px) { @media screen and (min-width: 1504px) {
#mapContainer { #mapContainer {
grid-template-columns: auto auto auto auto; grid-template-columns: auto auto auto auto;
-3
View File
@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 16 16">
<circle cx="50%" cy="50%" r="50%" fill="black" />
</svg>

Before

Width:  |  Height:  |  Size: 146 B

-5
View File
@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<polygon id="needleNorth" points="50,0 33,50 66,50" style="fill:red;stroke:black;stroke-width:1" />
<polygon id="needleSouth" points="50,100 33,50 66,50" style="fill:white;stroke:black;stroke-width:1" />
Sorry, your browser does not support inline SVG.
</svg>

Before

Width:  |  Height:  |  Size: 328 B

-5
View File
@@ -1,5 +0,0 @@
<!-- Original image: Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" fill="#3183ce" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="m8 3.293 6 6V13.5a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 2 13.5V9.293l6-6zm5-.793V6l-2-2V2.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5z"/>
<path fill-rule="evenodd" d="M7.293 1.5a1 1 0 0 1 1.414 0l6.647 6.646a.5.5 0 0 1-.708.708L8 2.207 1.354 8.854a.5.5 0 1 1-.708-.708L7.293 1.5z"/>
</svg>

Before

Width:  |  Height:  |  Size: 556 B

-4
View File
@@ -1,4 +0,0 @@
<!-- Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
<svg xmlns="http://www.w3.org/2000/svg" fill="#ffffff" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M15.817.113A.5.5 0 0 1 16 .5v14a.5.5 0 0 1-.402.49l-5 1a.502.502 0 0 1-.196 0L5.5 15.01l-4.902.98A.5.5 0 0 1 0 15.5v-14a.5.5 0 0 1 .402-.49l5-1a.5.5 0 0 1 .196 0L10.5.99l4.902-.98a.5.5 0 0 1 .415.103zM10 1.91l-4-.8v12.98l4 .8V1.91zm1 12.98 4-.8V1.11l-4 .8v12.98zm-6-.8V1.11l-4 .8v12.98l4-.8z"/>
</svg>

Before

Width:  |  Height:  |  Size: 530 B

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,10 +0,0 @@
<!-- Original image: Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
<svg viewBox="0 0 12 16" xmlns="http://www.w3.org/2000/svg">
<path
fill="#3183ce"
fill-rule="evenodd"
stroke="#19456d"
stroke-width="0.3"
stroke-linecap="round"
d="m 5.9999998,15.849652 c 0,0 5.8511182,-5.579947 5.8511182,-9.8134832 a 5.8511179,5.8880898 0 0 0 -11.7022358,0 c 0,4.2335362 5.8511176,9.8134832 5.8511176,9.8134832" />
</svg>

Before

Width:  |  Height:  |  Size: 529 B

-5
View File
@@ -1,5 +0,0 @@
<!-- Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
<svg xmlns="http://www.w3.org/2000/svg" fill="#ffffff" viewBox="0 0 16 16">
<path d="m14.12 10.163 1.715.858c.22.11.22.424 0 .534L8.267 15.34a.598.598 0 0 1-.534 0L.165 11.555a.299.299 0 0 1 0-.534l1.716-.858 5.317 2.659c.505.252 1.1.252 1.604 0l5.317-2.66zM7.733.063a.598.598 0 0 1 .534 0l7.568 3.784a.3.3 0 0 1 0 .535L8.267 8.165a.598.598 0 0 1-.534 0L.165 4.382a.299.299 0 0 1 0-.535L7.733.063z"/>
<path d="m14.12 6.576 1.715.858c.22.11.22.424 0 .534l-7.568 3.784a.598.598 0 0 1-.534 0L.165 7.968a.299.299 0 0 1 0-.534l1.716-.858 5.317 2.659c.505.252 1.1.252 1.604 0l5.317-2.659z"/>
</svg>

Before

Width:  |  Height:  |  Size: 718 B

+96 -873
View File
File diff suppressed because it is too large. Load diff
+48 -348
View File
@@ -7,6 +7,7 @@
description: null description: null
}, },
map: null, map: null,
markers: null,
panorama: null, panorama: null,
selectedMarker: null, selectedMarker: null,
added: {}, added: {},
@@ -18,7 +19,6 @@
MapEditor.metadata.name = form.elements.name.value; MapEditor.metadata.name = form.elements.name.value;
MapEditor.metadata.description = form.elements.description.value; MapEditor.metadata.description = form.elements.description.value;
MapEditor.metadata.unlisted = form.elements.unlisted.checked;
document.getElementById('mapName').innerHTML = form.elements.name.value ? form.elements.name.value : '[unnamed map]'; document.getElementById('mapName').innerHTML = form.elements.name.value ? form.elements.name.value : '[unnamed map]';
@@ -87,7 +87,7 @@
sv.getPanorama({ sv.getPanorama({
location: location, location: location,
preference: google.maps.StreetViewPreference.NEAREST, preference: google.maps.StreetViewPreference.NEAREST,
radius: MapEditor.map.getSearchRadius(location), radius: 100,
source: canBeIndoor ? google.maps.StreetViewSource.DEFAULT : google.maps.StreetViewSource.OUTDOOR source: canBeIndoor ? google.maps.StreetViewSource.DEFAULT : google.maps.StreetViewSource.OUTDOOR
}, function (data, status) { }, function (data, status) {
var panoLocationData = status === google.maps.StreetViewStatus.OK ? data.location : null; var panoLocationData = status === google.maps.StreetViewStatus.OK ? data.location : null;
@@ -118,13 +118,16 @@
MapEditor.resetSelected(); MapEditor.resetSelected();
MapEditor.selectedMarker = marker; MapEditor.selectedMarker = marker;
MapEditor.map.resize(); MapEditor.map.invalidateSize(true);
MapEditor.map.panTo(marker.getLatLng()); MapEditor.map.panTo(marker.getLatLng());
MapEditor.panorama.setVisible(false); MapEditor.panorama.setVisible(false);
if (marker.placeId) { if (marker.placeId) {
MapEditor.map.changeMarkerIcon(marker, MapEditor.map.iconCollection.iconBlue); MapEditor.markers.removeLayer(MapEditor.selectedMarker);
MapEditor.map.addLayer(MapEditor.selectedMarker);
marker.setIcon(IconCollection.iconBlue);
marker.setZIndexOffset(2000);
document.getElementById('deleteButton').style.display = 'block'; document.getElementById('deleteButton').style.display = 'block';
@@ -162,13 +165,13 @@
var placeId = MapEditor.selectedMarker.placeId var placeId = MapEditor.selectedMarker.placeId
if (places[placeId].id && !del) { if (places[placeId].id && !del) {
MapEditor.map.changeMarkerIcon( MapEditor.map.removeLayer(MapEditor.selectedMarker);
MapEditor.selectedMarker, MapEditor.markers.addLayer(MapEditor.selectedMarker);
places[placeId].noPano ? MapEditor.map.iconCollection.iconRed : MapEditor.map.iconCollection.iconGreen MapEditor.selectedMarker.setIcon(places[placeId].noPano ? IconCollection.iconRed : IconCollection.iconGreen);
); MapEditor.selectedMarker.setZIndexOffset(1000);
} else { } else {
delete places[placeId]; delete places[placeId];
MapEditor.map.removeMarker(MapEditor.selectedMarker); MapEditor.map.removeLayer(MapEditor.selectedMarker);
} }
document.getElementById('deleteButton').style.display = 'none'; document.getElementById('deleteButton').style.display = 'none';
@@ -220,7 +223,7 @@
MapEditor.resetSelected(del); MapEditor.resetSelected(del);
MapEditor.selectedMarker = null; MapEditor.selectedMarker = null;
MapEditor.map.resize(); MapEditor.map.invalidateSize(true);
}, },
deletePlace: function () { deletePlace: function () {
@@ -236,7 +239,6 @@
delete MapEditor.added[placeId]; delete MapEditor.added[placeId];
delete MapEditor.edited[placeId]; delete MapEditor.edited[placeId];
delete places[placeId];
document.getElementById('added').innerHTML = String(Object.keys(MapEditor.added).length); document.getElementById('added').innerHTML = String(Object.keys(MapEditor.added).length);
document.getElementById('edited').innerHTML = String(Object.keys(MapEditor.edited).length); document.getElementById('edited').innerHTML = String(Object.keys(MapEditor.edited).length);
@@ -255,9 +257,6 @@
if (MapEditor.metadata.description !== null) { if (MapEditor.metadata.description !== null) {
data.append('description', MapEditor.metadata.description); data.append('description', MapEditor.metadata.description);
} }
if (MapEditor.metadata.unlisted !== null) {
data.append('unlisted', MapEditor.metadata.unlisted);
}
for (var placeId in MapEditor.added) { for (var placeId in MapEditor.added) {
if (!MapEditor.added.hasOwnProperty(placeId)) { if (!MapEditor.added.hasOwnProperty(placeId)) {
@@ -314,49 +313,7 @@
} }
}; };
var Util = { var IconCollection = {
getHighResData: function () {
if (window.devicePixelRatio >= 4) {
return { ppi: 320, tileSize: 128, zoomOffset: 1, minZoom: 0, maxZoom: 18 };
} else if (window.devicePixelRatio >= 2) {
return { ppi: 250, tileSize: 256, zoomOffset: 0, minZoom: 1, maxZoom: 19 };
} else {
return { ppi: 72, tileSize: 512, zoomOffset: -1, minZoom: 2, maxZoom: 20 };
}
},
extractCoordinates: function (coordinatesStr) {
var coordinates = { valid: false, latlng: { lat: 0., lng: 0. } };
var delimiters = [',', ' ', ';'];
coordinatesStr = coordinatesStr.trim();
if (coordinatesStr.length == 0) {
return coordinates;
}
for (var delimiter of delimiters) {
if (coordinatesStr.indexOf(delimiter) != -1) {
var coordinatesArr = coordinatesStr.split(delimiter);
coordinates.latlng.lat = parseFloat(coordinatesArr[0]);
coordinates.latlng.lng = parseFloat(coordinatesArr[1]);
if (!isNaN(coordinates.latlng.lat) && !isNaN(coordinates.latlng.lng)) {
coordinates.valid = true;
return coordinates;
}
}
}
return coordinates;
}
};
var LMapWrapper = {
map: null,
markers: null,
divId: null,
iconCollection: {
iconGreen: L.icon({ iconGreen: L.icon({
iconUrl: STATIC_ROOT + '/img/markers/marker-green.svg?rev' + REVISION, iconUrl: STATIC_ROOT + '/img/markers/marker-green.svg?rev' + REVISION,
iconSize: [24, 32], iconSize: [24, 32],
@@ -371,57 +328,55 @@
iconUrl: STATIC_ROOT + '/img/markers/marker-blue.svg?rev=' + REVISION, iconUrl: STATIC_ROOT + '/img/markers/marker-blue.svg?rev=' + REVISION,
iconSize: [24, 32], iconSize: [24, 32],
iconAnchor: [12, 32] iconAnchor: [12, 32]
}) }),
}, };
init: function (divId, places) { var Util = {
document.getElementById(divId).style.display = "block"; getHighResData: function () {
if (window.devicePixelRatio >= 4) {
return { ppi: 320, tileSize: 128, zoomOffset: 1, minZoom: 0, maxZoom: 18 };
} else if (window.devicePixelRatio >= 2) {
return { ppi: 250, tileSize: 256, zoomOffset: 0, minZoom: 1, maxZoom: 19 };
} else {
return { ppi: 72, tileSize: 512, zoomOffset: -1, minZoom: 2, maxZoom: 20 };
}
}
};
if (!LMapWrapper.map) { MapEditor.map = L.map('map', {
LMapWrapper.divId = divId; zoomControl: false
LMapWrapper.map = L.map(LMapWrapper.divId, {
center: { lat: 0., lng: 0. },
zoom: 2
}); });
LMapWrapper.map.on('click', function (e) { MapEditor.map.on('click', function (e) {
LMapWrapper.placeMarker(e.latlng); var marker = L.marker(e.latlng, {
icon: IconCollection.iconBlue,
zIndexOffset: 2000
})
.addTo(MapEditor.map)
.on('click', function () {
MapEditor.select(this);
});
MapEditor.select(marker);
}); });
var highResData = Util.getHighResData(); var highResData = Util.getHighResData();
L.tileLayer(tileUrl, { L.tileLayer(tileUrl, {
attribution: tileAttribution, attribution: tileAttribution,
subdomains: tileSubdomains, subdomains: '1234',
ppi: highResData.ppi, ppi: highResData.ppi,
tileSize: highResData.tileSize, tileSize: highResData.tileSize,
zoomOffset: highResData.zoomOffset, zoomOffset: highResData.zoomOffset,
minZoom: highResData.minZoom, minZoom: highResData.minZoom,
maxZoom: highResData.maxZoom maxZoom: highResData.maxZoom
}).addTo(LMapWrapper.map); }).addTo(MapEditor.map);
if (mapId) { MapEditor.map.fitBounds(L.latLngBounds({ lat: mapBounds.south, lng: mapBounds.west }, { lat: mapBounds.north, lng: mapBounds.east }));
LMapWrapper.map.fitBounds(L.latLngBounds({ lat: mapBounds.south, lng: mapBounds.west }, { lat: mapBounds.north, lng: mapBounds.east }));
}
}
LMapWrapper.loadMarkers(places); MapEditor.markers = L.markerClusterGroup({
document.getElementById('streetViewCoverSelector').style.display = 'none';
},
hide: function () {
document.getElementById(LMapWrapper.divId).style.display = 'none';
},
loadMarkers: function (places) {
if (!LMapWrapper.markers) {
LMapWrapper.markers = L.markerClusterGroup({
maxClusterRadius: 50 maxClusterRadius: 50
}); });
} else {
LMapWrapper.markers.clearLayers();
}
for (var placeId in places) { for (var placeId in places) {
if (!places.hasOwnProperty(placeId)) { if (!places.hasOwnProperty(placeId)) {
@@ -431,229 +386,18 @@
var place = places[placeId]; var place = places[placeId];
var marker = L.marker({ lat: place.lat, lng: place.lng }, { var marker = L.marker({ lat: place.lat, lng: place.lng }, {
icon: place.noPano ? LMapWrapper.iconCollection.iconRed : LMapWrapper.iconCollection.iconGreen, icon: place.noPano ? IconCollection.iconRed : IconCollection.iconGreen,
zIndexOffset: 1000 zIndexOffset: 1000
}) })
.addTo(LMapWrapper.markers) .addTo(MapEditor.markers)
.on('click', function () { .on('click', function () {
MapEditor.select(this); MapEditor.select(this);
}); });
marker.placeId = placeId; marker.placeId = place.id;
} }
LMapWrapper.map.addLayer(LMapWrapper.markers); MapEditor.map.addLayer(MapEditor.markers);
},
// TODO: check whether marker is already existing on the map for the coordinates
// or alternatively block saving for matching coordinates
placeMarker: function (latLng) {
var marker = L.marker(latLng, {
icon: LMapWrapper.iconCollection.iconBlue,
zIndexOffset: 2000
})
.addTo(LMapWrapper.markers)
.on('click', function () {
MapEditor.select(this);
});
MapEditor.select(marker);
},
panTo: function (latLng) {
LMapWrapper.map.panTo(latLng);
},
resize: function () {
LMapWrapper.map.invalidateSize(true);
},
changeMarkerIcon: function (marker, icon) {
marker.setIcon(icon);
marker.setZIndexOffset(2000);
},
removeMarker: function (marker) {
LMapWrapper.markers.removeLayer(marker);
},
toggleStreetViewCover: function () { },
getSearchRadius: function (location) {
return 100;
}
};
var GMapWrapper = {
map: null,
markers: null,
divId: null,
streetViewCover: null,
streetViewCoverOn: false,
iconCollection: {
iconGreen: {
url: STATIC_ROOT + '/img/markers/marker-green.svg?rev' + REVISION,
scaledSize: new google.maps.Size(24, 32), // scaled size
origin: new google.maps.Point(0, 0), // origin
anchor: new google.maps.Point(12, 32) // anchor
},
iconRed: {
url: STATIC_ROOT + '/img/markers/marker-red.svg?rev' + REVISION,
scaledSize: new google.maps.Size(24, 32), // scaled size
origin: new google.maps.Point(0, 0), // origin
anchor: new google.maps.Point(12, 32) // anchor
},
iconBlue: {
url: STATIC_ROOT + '/img/markers/marker-blue.svg?rev' + REVISION,
scaledSize: new google.maps.Size(24, 32), // scaled size
origin: new google.maps.Point(0, 0), // origin
anchor: new google.maps.Point(12, 32) // anchor
}
},
init: function (divId, places) {
document.getElementById(divId).style.display = "block";
if (!GMapWrapper.map) {
GMapWrapper.divId = divId;
GMapWrapper.map = new google.maps.Map(document.getElementById(GMapWrapper.divId), {
center: { lat: 0., lng: 0. },
zoom: 2,
fullscreenControl: false,
zoomControl: true,
zoomControlOptions: {
position: google.maps.ControlPosition.LEFT_BOTTOM
},
streetViewControl: false,
draggableCursor: 'crosshair'
});
GMapWrapper.streetViewCover = new google.maps.StreetViewCoverageLayer();
GMapWrapper.map.addListener('click', function (mapsMouseEvent) {
GMapWrapper.placeMarker({
lat: mapsMouseEvent.latLng.lat(),
lng: mapsMouseEvent.latLng.lng()
});
});
if (mapId) {
GMapWrapper.map.fitBounds({ south: mapBounds.south, west: mapBounds.west, north: mapBounds.north, east: mapBounds.east });
}
}
GMapWrapper.loadMarkers(places);
document.getElementById('streetViewCoverSelector').style.display = 'block'
},
hide: function () {
document.getElementById(GMapWrapper.divId).style.display = 'none';
},
loadMarkers: function (places) {
if (!GMapWrapper.markers) {
GMapWrapper.markers = new MarkerClusterer(GMapWrapper.map, [], {
imagePath: STATIC_ROOT + '/img/markers/m',
imageExtension: 'png?rev' + REVISION
});
} else {
GMapWrapper.markers.clearMarkers();
}
for (var placeId in places) {
if (!places.hasOwnProperty(placeId)) {
continue;
}
var place = places[placeId];
var marker = new google.maps.Marker({
position: {
lat: place.lat,
lng: place.lng
},
icon: place.noPano ? GMapWrapper.iconCollection.iconRed : GMapWrapper.iconCollection.iconGreen
});
marker.getLatLng = function () { return { lat: this.getPosition().lat(), lng: this.getPosition().lng() } };
marker.setLatLng = function (coords) { this.setPosition(coords) };
marker.addListener('click', function () {
MapEditor.select(this);
});
marker.placeId = placeId;
GMapWrapper.markers.addMarker(marker);
}
},
// TODO: check whether marker is already existing on the map for the coordinates
// or alternatively block saving for matching coordinates
placeMarker: function (latLng) {
var marker = new google.maps.Marker({
map: GMapWrapper.map,
position: latLng,
icon: GMapWrapper.iconCollection.iconBlue,
});
marker.getLatLng = function () { return { lat: this.getPosition().lat(), lng: this.getPosition().lng() } };
marker.setLatLng = function (coords) { this.setPosition(coords) };
marker.addListener('click', function () {
MapEditor.select(this);
});
GMapWrapper.markers.addMarker(marker);
MapEditor.select(marker);
},
panTo: function (latLng) {
GMapWrapper.map.panTo(latLng);
},
resize: function () {
google.maps.event.trigger(GMapWrapper.map, 'resize');
},
changeMarkerIcon: function (marker, icon) {
marker.setIcon(icon);
},
removeMarker: function (marker) {
GMapWrapper.markers.removeMarker(marker);
},
toggleStreetViewCover: function () {
if (GMapWrapper.streetViewCoverOn) {
GMapWrapper.streetViewCover.setMap(null);
GMapWrapper.streetViewCoverOn = false;
} else {
GMapWrapper.streetViewCover.setMap(GMapWrapper.map);
GMapWrapper.streetViewCoverOn = true;
}
},
getSearchRadius: function (location) {
// source: https://www.yorku.ca/mack/CHI01.htm
var movementOffset = 4;
// source: https://groups.google.com/g/google-maps-js-api-v3/c/hDRO4oHVSeM/m/osOYQYXg2oUJ?pli=1
var metersPerPixel = 156543.03392 * Math.cos(location.lat * Math.PI / 180) / Math.pow(2, GMapWrapper.map.getZoom());
var minSearchRadius = 5;
var searchRadius = Math.max(minSearchRadius, Math.round(movementOffset * metersPerPixel));
return searchRadius;
}
};
// initialize content of #map with google maps
MapEditor.map = GMapWrapper;
MapEditor.map.init('gmap', places);
MapEditor.panorama = new google.maps.StreetViewPanorama(document.getElementById('panorama'), { MapEditor.panorama = new google.maps.StreetViewPanorama(document.getElementById('panorama'), {
// switch off fullscreenControl because positioning doesn't work // switch off fullscreenControl because positioning doesn't work
@@ -696,48 +440,4 @@
document.getElementById('deleteButton').onclick = function () { document.getElementById('deleteButton').onclick = function () {
MapEditor.deletePlace(); MapEditor.deletePlace();
}; };
document.getElementById('jumpButton').onclick = function (e) {
var coordinatesStr = document.getElementById("jumpCoordinates").value;
var coordinates = Util.extractCoordinates(coordinatesStr);
if (coordinates.valid) {
MapEditor.map.placeMarker(coordinates.latlng);
}
};
document.getElementById('jumpCoordinates').onkeyup = function (e) {
var coordinatesStr = document.getElementById("jumpCoordinates").value;
var coordinates = Util.extractCoordinates(coordinatesStr);
var jumpButton = document.getElementById("jumpButton");
if (coordinates.valid) {
jumpButton.disabled = false;
if (e.key == 'Enter') {
MapEditor.map.placeMarker(coordinates.latlng);
}
}
else {
jumpButton.disabled = true;
}
};
document.getElementById('mapSelector').onclick = function () {
MapEditor.closePlace();
MapEditor.map.hide();
if (MapEditor.map === GMapWrapper) {
MapEditor.map = LMapWrapper;
MapEditor.map.init('lmap', places);
} else {
MapEditor.map = GMapWrapper;
MapEditor.map.init('gmap', places);
}
}
document.getElementById('streetViewCoverSelector').onclick = function () {
MapEditor.map.toggleStreetViewCover();
}
})(); })();
-1
View File
@@ -1,5 +1,4 @@
var MapGuesser = { var MapGuesser = {
isSecure: window.location.protocol === 'https:',
cookiesAgreed: false, cookiesAgreed: false,
sessionAvailableHooks: {}, sessionAvailableHooks: {},
+6 -106
View File
@@ -8,9 +8,12 @@
var imgContainer = imgContainers[i]; var imgContainer = imgContainers[i];
var imgSrc = 'https://maps.googleapis.com/maps/api/staticmap?size=350x175&' + var imgSrc = 'https://maps.googleapis.com/maps/api/staticmap?size=350x175&' +
'scale=' + (window.devicePixelRatio >= 2 ? 2 : 1) + '&' + 'scale=' + (window.devicePixelRatio >= 2 ? 2 : 1) + '&path=color:0x0000ff40|weight:3|fillcolor:0x0000ff20|' +
'visible=' + imgContainer.dataset.boundSouthLat + ',' + imgContainer.dataset.boundWestLng + '|' + imgContainer.dataset.boundSouthLat + ',' + imgContainer.dataset.boundWestLng + '|' +
imgContainer.dataset.boundNorthLat + ',' + imgContainer.dataset.boundEastLng + imgContainer.dataset.boundNorthLat + ',' + imgContainer.dataset.boundWestLng + '|' +
imgContainer.dataset.boundNorthLat + ',' + imgContainer.dataset.boundEastLng + '|' +
imgContainer.dataset.boundSouthLat + ',' + imgContainer.dataset.boundEastLng + '|' +
imgContainer.dataset.boundSouthLat + ',' + imgContainer.dataset.boundWestLng +
'&key=' + GOOGLE_MAPS_JS_API_KEY; '&key=' + GOOGLE_MAPS_JS_API_KEY;
imgContainer.style.backgroundImage = 'url("' + imgSrc + '")'; imgContainer.style.backgroundImage = 'url("' + imgSrc + '")';
@@ -65,32 +68,6 @@
} }
}; };
var Util = {
printTimeForHuman: function (time) {
const minutes = Math.floor(time / 60);
const seconds = time % 60;
var time_str = '';
if (minutes == 1) {
time_str += '1 minute';
} else if (minutes > 1) {
time_str += minutes + ' minutes';
}
if (minutes > 0 && seconds > 0) {
time_str += ' and ';
}
if (seconds == 1) {
time_str += '1 second';
} else if (seconds > 1) {
time_str += seconds + ' seconds';
}
return time_str;
}
};
Maps.addStaticMaps(); Maps.addStaticMaps();
Maps.initializeDescriptionDivs(); Maps.initializeDescriptionDivs();
@@ -99,81 +76,4 @@
window.onresize = function () { window.onresize = function () {
Maps.calculateDescriptionDivHeights(); Maps.calculateDescriptionDivHeights();
}; };
document.getElementById('multiForm').onsubmit = function (e) {
e.preventDefault();
var roomId = this.elements.roomId.value;
if (roomId.length !== 6) {
return;
}
window.location.href = '/multiGame/' + this.elements.roomId.value;
};
document.getElementById('challengeForm').onsubmit = function (e) {
e.preventDefault();
var url = '/challenge/create.json';
var formData = new FormData(this);
document.getElementById('loading').style.visibility = 'visible';
MapGuesser.httpRequest('POST', url, function() {
document.getElementById('loading').style.visibility = 'hidden';
if (this.response.error) {
Game.handleErrorResponse(this.response.error);
return;
}
window.location.href = '/challenge/' + this.response.challengeToken;
}, formData);
};
document.getElementById('multiButton').onclick = function () {
MapGuesser.showModal('multi');
document.getElementById('createNewRoomButton').href = '/multiGame/new/' + this.dataset.mapId;
document.getElementById('multiForm').elements.roomId.select();
document.getElementById('playMode').style.visibility = 'hidden';
}
if (document.getElementById('challengeButton')) {
document.getElementById('challengeButton').onclick = function () {
MapGuesser.showModal('challenge');
document.getElementById('playMode').style.visibility = 'hidden';
var timeLimit = document.getElementById('timeLimit').value;
document.getElementById('timeLimitLabel').innerText = 'Time limit of ' + Util.printTimeForHuman(timeLimit);
};
}
document.getElementById('closePlayModeButton').onclick = function () {
MapGuesser.hideModal();
};
document.getElementById('closeMultiButton').onclick = function () {
MapGuesser.hideModal();
};
document.getElementById('closeChallengeButton').onclick = function () {
MapGuesser.hideModal();
}
var buttons = document.getElementById('mapContainer').getElementsByClassName('playButton');
for (var i = 0; i < buttons.length; i++) {
var button = buttons[i];
button.onclick = function () {
MapGuesser.showModal('playMode');
document.getElementById('singleButton').href = '/game/' + this.dataset.mapId;
document.getElementById('multiButton').dataset.mapId = this.dataset.mapId;
document.getElementById('challengeMapId').value = this.dataset.mapId;
};
}
document.getElementById('timeLimit').oninput = function () {
var timeLimit = document.getElementById('timeLimit').value;
document.getElementById('timeLimitLabel').innerText = 'Time limit of ' + Util.printTimeForHuman(timeLimit);
document.getElementById('timerEnabled').checked = true;
}
})(); })();
+2 -17
View File
@@ -59,25 +59,10 @@ def getRevisionForRef(ref):
return subprocess.check_output(["git", "rev-list", "-1", ref], cwd=REPO).decode().strip() return subprocess.check_output(["git", "rev-list", "-1", ref], cwd=REPO).decode().strip()
def getLatestReleaseTag(): def getLatestReleaseTag():
process = subprocess.Popen(["git", "for-each-ref", "refs/tags/Release*", "--sort=-creatordate", "--format=%(refname:short)"], stdout=subprocess.PIPE, cwd=REPO) return subprocess.check_output(["git", "for-each-ref", "refs/tags/Release*", "--count=1", "--sort=-creatordate", "--format=%(refname:short)"], cwd=REPO).decode().strip()
for line in process.stdout:
tag = line.decode().rstrip()
if isTagVerified(tag):
return tag
print(f"[WARNING] Tag '{tag}' is not verified, skipping.")
raise Exception("No verified 'Release*' tag found!")
def isTagVerified(tag):
process = subprocess.run(["git", "tag", "--verify", tag], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=REPO)
return process.returncode == 0
def updateRepoFromRemote(): def updateRepoFromRemote():
subprocess.call(["git", "fetch", "origin", "--prune", "--prune-tags"], cwd=REPO) subprocess.call(["git", "fetch", "origin", "--prune"], cwd=REPO)
def checkoutWorktree(worktreePath, ref): def checkoutWorktree(worktreePath, ref):
subprocess.call(["git", "checkout", "-f", ref], cwd=worktreePath) subprocess.call(["git", "checkout", "-f", ref], cwd=worktreePath)
+1 -7
View File
@@ -9,14 +9,11 @@ if [ -f ${ROOT_DIR}/installed ]; then
exit 1 exit 1
fi fi
echo "Installing NPM packages..."
(cd ${ROOT_DIR}/multi && npm install)
echo "Installing Yarn packages..." echo "Installing Yarn packages..."
(cd ${ROOT_DIR}/public/static && yarn install) (cd ${ROOT_DIR}/public/static && yarn install)
echo "Installing MapGuesser DB..." echo "Installing MapGuesser DB..."
mysql --host=${DB_HOST} --user=${DB_USER} --password=${DB_PASSWORD} ${DB_NAME} < ${ROOT_DIR}/database/mapguesser.sql mysql --host=${DB_HOST} --user=${DB_USER} --password=${DB_PASSWORD} ${DB_NAME} < ${ROOT_DIR}/db/mapguesser.sql
echo "Migrating DB..." echo "Migrating DB..."
(cd ${ROOT_DIR} && ./mapg db:migrate) (cd ${ROOT_DIR} && ./mapg db:migrate)
@@ -27,9 +24,6 @@ if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
echo "Linking view files..." echo "Linking view files..."
(cd ${ROOT_DIR} && ./mapg view:link) (cd ${ROOT_DIR} && ./mapg view:link)
else
echo "Creating the first user..."
(cd ${ROOT_DIR} && ./mapg user:add mapg@mapg.dev 123456 admin)
fi fi
touch ${ROOT_DIR}/installed touch ${ROOT_DIR}/installed
-3
View File
@@ -11,9 +11,6 @@ else
(cd ${ROOT_DIR} && composer install --dev) (cd ${ROOT_DIR} && composer install --dev)
fi fi
echo "Installing NPM packages..."
(cd ${ROOT_DIR}/multi && npm install)
echo "Installing Yarn packages..." echo "Installing Yarn packages..."
(cd ${ROOT_DIR}/public/static && yarn install) (cd ${ROOT_DIR}/public/static && yarn install)
+4 -2
View File
@@ -1,6 +1,7 @@
<?php namespace MapGuesser\Cli; <?php namespace MapGuesser\Cli;
use DateTime; use DateTime;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\PersistentData\Model\User; use MapGuesser\PersistentData\Model\User;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
@@ -26,12 +27,13 @@ class AddUserCommand extends Command
$user->setActive(true); $user->setActive(true);
$user->setCreatedDate(new DateTime()); $user->setCreatedDate(new DateTime());
if ($input->hasArgument('type') && $input->getArgument('type') !== null) { if ($input->hasArgument('type')) {
$user->setType($input->getArgument('type')); $user->setType($input->getArgument('type'));
} }
try { try {
\Container::$persistentDataManager->saveToDb($user); $pdm = new PersistentDataManager();
$pdm->saveToDb($user);
} catch (\Exception $e) { } catch (\Exception $e) {
$output->writeln('<error>Adding user failed!</error>'); $output->writeln('<error>Adding user failed!</error>');
$output->writeln(''); $output->writeln('');
+1 -1
View File
@@ -1,7 +1,7 @@
<?php namespace MapGuesser\Cli; <?php namespace MapGuesser\Cli;
use FilesystemIterator; use FilesystemIterator;
use SokoWeb\View\Linker; use MapGuesser\View\Linker;
use RecursiveDirectoryIterator; use RecursiveDirectoryIterator;
use RecursiveIteratorIterator; use RecursiveIteratorIterator;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
+11 -27
View File
@@ -1,13 +1,12 @@
<?php namespace MapGuesser\Cli; <?php namespace MapGuesser\Cli;
use DateTime; use DateTime;
use SokoWeb\Database\Query\Modify; use MapGuesser\Database\Query\Modify;
use SokoWeb\Database\Query\Select; use MapGuesser\Database\Query\Select;
use SokoWeb\Interfaces\Database\IResultSet; use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Repository\MultiRoomRepository; use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Repository\UserConfirmationRepository; use MapGuesser\Repository\UserConfirmationRepository;
use MapGuesser\Repository\UserPasswordResetterRepository; use MapGuesser\Repository\UserPasswordResetterRepository;
use MapGuesser\Repository\UserPlayedPlaceRepository;
use MapGuesser\Repository\UserRepository; use MapGuesser\Repository\UserRepository;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
@@ -15,25 +14,22 @@ use Symfony\Component\Console\Output\OutputInterface;
class MaintainDatabaseCommand extends Command class MaintainDatabaseCommand extends Command
{ {
private PersistentDataManager $pdm;
private UserRepository $userRepository; private UserRepository $userRepository;
private UserConfirmationRepository $userConfirmationRepository; private UserConfirmationRepository $userConfirmationRepository;
private UserPasswordResetterRepository $userPasswordResetterRepository; private UserPasswordResetterRepository $userPasswordResetterRepository;
private MultiRoomRepository $multiRoomRepository;
private UserPlayedPlaceRepository $userPlayedPlaceRepository;
public function __construct() public function __construct()
{ {
parent::__construct(); parent::__construct();
$this->pdm = new PersistentDataManager();
$this->userRepository = new UserRepository(); $this->userRepository = new UserRepository();
$this->userConfirmationRepository = new UserConfirmationRepository(); $this->userConfirmationRepository = new UserConfirmationRepository();
$this->userPasswordResetterRepository = new UserPasswordResetterRepository(); $this->userPasswordResetterRepository = new UserPasswordResetterRepository();
$this->multiRoomRepository = new MultiRoomRepository();
$this->userPlayedPlaceRepository = new UserPlayedPlaceRepository();
} }
public function configure(): void public function configure(): void
@@ -47,7 +43,6 @@ class MaintainDatabaseCommand extends Command
try { try {
$this->deleteInactiveExpiredUsers(); $this->deleteInactiveExpiredUsers();
$this->deleteExpiredPasswordResetters(); $this->deleteExpiredPasswordResetters();
$this->deleteExpiredRooms();
$this->deleteExpiredSessions(); $this->deleteExpiredSessions();
} catch (\Exception $e) { } catch (\Exception $e) {
$output->writeln('<error>Maintenance failed!</error>'); $output->writeln('<error>Maintenance failed!</error>');
@@ -73,19 +68,15 @@ class MaintainDatabaseCommand extends Command
//TODO: these can be in some wrapper class //TODO: these can be in some wrapper class
$userConfirmation = $this->userConfirmationRepository->getByUser($user); $userConfirmation = $this->userConfirmationRepository->getByUser($user);
if ($userConfirmation !== null) { if ($userConfirmation !== null) {
\Container::$persistentDataManager->deleteFromDb($userConfirmation); $this->pdm->deleteFromDb($userConfirmation);
} }
$userPasswordResetter = $this->userPasswordResetterRepository->getByUser($user); $userPasswordResetter = $this->userPasswordResetterRepository->getByUser($user);
if ($userPasswordResetter !== null) { if ($userPasswordResetter !== null) {
\Container::$persistentDataManager->deleteFromDb($userPasswordResetter); $this->pdm->deleteFromDb($userPasswordResetter);
} }
foreach ($this->userPlayedPlaceRepository->getAllByUser($user) as $userPlayedPlace) { $this->pdm->deleteFromDb($user);
\Container::$persistentDataManager->deleteFromDb($userPlayedPlace);
}
\Container::$persistentDataManager->deleteFromDb($user);
} }
\Container::$dbConnection->commit(); \Container::$dbConnection->commit();
@@ -94,14 +85,7 @@ class MaintainDatabaseCommand extends Command
private function deleteExpiredPasswordResetters(): void private function deleteExpiredPasswordResetters(): void
{ {
foreach ($this->userPasswordResetterRepository->getAllExpired() as $passwordResetter) { foreach ($this->userPasswordResetterRepository->getAllExpired() as $passwordResetter) {
\Container::$persistentDataManager->deleteFromDb($passwordResetter); $this->pdm->deleteFromDb($passwordResetter);
}
}
private function deleteExpiredRooms(): void
{
foreach ($this->multiRoomRepository->getAllExpired() as $multiRoom) {
\Container::$persistentDataManager->deleteFromDb($multiRoom);
} }
} }
+3 -3
View File
@@ -1,8 +1,8 @@
<?php namespace MapGuesser\Cli; <?php namespace MapGuesser\Cli;
use SokoWeb\Database\Query\Modify; use MapGuesser\Database\Query\Modify;
use SokoWeb\Database\Query\Select; use MapGuesser\Database\Query\Select;
use SokoWeb\Interfaces\Database\IResultSet; use MapGuesser\Interfaces\Database\IResultSet;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
+21 -264
View File
@@ -1,294 +1,51 @@
<?php namespace MapGuesser\Controller; <?php namespace MapGuesser\Controller;
use DateTime; use MapGuesser\Interfaces\Request\IRequest;
use Faker\Factory; use MapGuesser\Response\HtmlContent;
use SokoWeb\Interfaces\Authentication\IAuthenticationRequired; use MapGuesser\Response\JsonContent;
use SokoWeb\Response\HtmlContent; use MapGuesser\Interfaces\Response\IContent;
use SokoWeb\Response\JsonContent;
use SokoWeb\Interfaces\Response\IContent;
use SokoWeb\Interfaces\Response\IRedirect;
use MapGuesser\Multi\MultiConnector;
use MapGuesser\PersistentData\Model\Challenge;
use MapGuesser\PersistentData\Model\MultiRoom;
use MapGuesser\PersistentData\Model\PlaceInChallenge;
use MapGuesser\PersistentData\Model\UserInChallenge;
use MapGuesser\Repository\ChallengeRepository;
use MapGuesser\Repository\MapRepository; use MapGuesser\Repository\MapRepository;
use MapGuesser\Repository\MultiRoomRepository;
use MapGuesser\Repository\PlaceRepository;
use MapGuesser\Repository\UserInChallengeRepository;
use SokoWeb\Response\Redirect;
class GameController implements IAuthenticationRequired class GameController
{ {
const NUMBER_OF_ROUNDS = 5; private IRequest $request;
private MultiConnector $multiConnector;
private MultiRoomRepository $multiRoomRepository;
private MapRepository $mapRepository; private MapRepository $mapRepository;
private PlaceRepository $placeRepository; public function __construct(IRequest $request)
private ChallengeRepository $challengeRepository;
private UserInChallengeRepository $userInChallengeRepository;
public function __construct()
{ {
$this->multiConnector = new MultiConnector(); $this->request = $request;
$this->multiRoomRepository = new MultiRoomRepository();
$this->mapRepository = new MapRepository(); $this->mapRepository = new MapRepository();
$this->placeRepository = new PlaceRepository();
$this->challengeRepository = new ChallengeRepository();
$this->userInChallengeRepository = new UserInChallengeRepository();
}
public function isAuthenticationRequired(): bool
{
return empty($_ENV['ENABLE_GAME_FOR_GUESTS']);
} }
public function getGame(): IContent public function getGame(): IContent
{ {
$mapId = (int) \Container::$request->query('mapId'); $mapId = (int) $this->request->query('mapId');
return new HtmlContent('game', ['mapId' => $mapId]); return new HtmlContent('game', $this->prepareGame($mapId));
} }
public function getNewMultiGame(): IRedirect public function getGameJson(): IContent
{
$mapId = (int) $this->request->query('mapId');
return new JsonContent($this->prepareGame($mapId));
}
private function prepareGame(int $mapId): array
{ {
$mapId = (int) \Container::$request->query('mapId');
$map = $this->mapRepository->getById($mapId); $map = $this->mapRepository->getById($mapId);
$roomId = bin2hex(random_bytes(3));
$token = $this->getMultiToken($roomId);
$room = new MultiRoom(); $session = $this->request->session();
$room->setRoomId($roomId);
$room->setStateArray([
'mapId' => $mapId,
'area' => $map->getArea(),
'rounds' => [],
'currentRound' => -1
]);
$room->setMembersArray(['owner' => $token, 'all' => []]);
$room->setUpdatedDate(new DateTime());
\Container::$persistentDataManager->saveToDb($room);
$this->multiConnector->sendMessage('create_room', ['roomId' => $roomId]);
return new Redirect(
\Container::$routeCollection
->getRoute('multiGame')
->generateLink(['roomId' => $roomId]),
IRedirect::TEMPORARY
);
}
public function getMultiGame(): IContent
{
$roomId = \Container::$request->query('roomId');
return new HtmlContent('game', ['roomId' => $roomId]);
}
public function getChallenge(): IContent
{
$challengeToken = \Container::$request->query('challengeToken');
return new HtmlContent('game', ['challengeToken' => $challengeToken]);
}
public function createNewChallenge(): IContent
{
// create Challenge
do {
// initiliaze or if a challenge with the same token already exists
$challengeToken = mt_rand();
} while ($this->challengeRepository->getByToken($challengeToken));
$challenge = new Challenge();
$challenge->setToken($challengeToken);
$challenge->setCreatedDate(new DateTime());
if (\Container::$request->post('timerEnabled') !== null && \Container::$request->post('timeLimit') !== null) {
$challenge->setTimeLimit(\Container::$request->post('timeLimit'));
}
if (\Container::$request->post('timeLimitType') !== null) {
$challenge->setTimeLimitType(\Container::$request->post('timeLimitType'));
}
if (\Container::$request->post('noMove') !== null) {
$challenge->setNoMove(true);
}
if (\Container::$request->post('noPan') !== null) {
$challenge->setNoPan(true);
}
if (\Container::$request->post('noZoom') !== null) {
$challenge->setNoZoom(true);
}
\Container::$persistentDataManager->saveToDb($challenge);
// save owner/creator
$session = \Container::$request->session();
$userId = $session->get('userId');
$userInChallenge = new UserInChallenge();
$userInChallenge->setUserId($userId);
$userInChallenge->setChallenge($challenge);
$userInChallenge->setTimeLeft($challenge->getTimeLimit());
$userInChallenge->setIsOwner(true);
\Container::$persistentDataManager->saveToDb($userInChallenge);
// select places
$mapId = (int) \Container::$request->post('mapId');
// $map = $this->mapRepository->getById($mapId);
$places = $this->placeRepository->getRandomNPlaces($mapId, static::NUMBER_OF_ROUNDS, $userId);
$round = 0;
foreach ($places as $place) {
$placeInChallenge = new PlaceInChallenge();
$placeInChallenge->setPlace($place);
$placeInChallenge->setChallenge($challenge);
$placeInChallenge->setRound($round++);
\Container::$persistentDataManager->saveToDb($placeInChallenge);
}
return new JsonContent(['challengeToken' => dechex($challengeToken)]);
}
public function prepareGame(): IContent
{
$mapId = (int) \Container::$request->query('mapId');
$map = $this->mapRepository->getById($mapId);
$session = \Container::$request->session();
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) { if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
$session->set('state', [ $session->set('state', [
'mapId' => $mapId, 'mapId' => $mapId,
'area' => $map->getArea(), 'area' => $map->getArea(),
'rounds' => [], 'rounds' => []
'currentRound' => -1
]);
} else { // update the area of the map in the session in any case
$state['area'] = $map->getArea();
$session->set('state', $state);
}
return new JsonContent([
'mapId' => $mapId,
'mapName' => $map->getName(),
'bounds' => $map->getBounds()->toArray()
]); ]);
} }
public function prepareMultiGame(): IContent return ['mapId' => $mapId, 'mapName' => $map->getName(), 'bounds' => $map->getBounds()->toArray()];
{
$roomId = \Container::$request->query('roomId');
$userName = \Container::$request->post('userName');
if (empty($userName)) {
$faker = Factory::create();
$userName = $faker->userName;
}
$room = $this->multiRoomRepository->getByRoomId($roomId);
if (!isset($room)) {
return new JsonContent(['error' => 'game_not_found']);
}
$state = $room->getStateArray();
$map = $this->mapRepository->getById($state['mapId']);
$token = $this->getMultiToken($roomId);
$members = $room->getMembersArray();
if (!in_array($token, $members['all'])) {
if ($state['currentRound'] >= 0) {
return new JsonContent(['error' => 'game_already_started']);
}
$members['all'][] = $token;
}
$room->setMembersArray($members);
$room->setUpdatedDate(new DateTime());
\Container::$persistentDataManager->saveToDb($room);
$this->multiConnector->sendMessage('join_room', [
'roomId' => $roomId,
'token' => $token,
'userName' => $userName
]);
return new JsonContent([
'roomId' => $roomId,
'token' => $token,
'owner' => $members['owner'] == $token,
'mapId' => $state['mapId'],
'mapName' => $map->getName(),
'bounds' => $map->getBounds()->toArray()
]);
}
public function prepareChallenge(): IContent
{
$challengeToken_str = \Container::$request->query('challengeToken');
$session = \Container::$request->session();
$userId = $session->get('userId');
if (!isset($userId))
{
return new JsonContent(['error' => 'anonymous_user']);
}
$challenge = $this->challengeRepository->getByTokenStr($challengeToken_str);
if (!isset($challenge))
{
return new JsonContent(['error' => 'game_not_found']);
}
if (!$this->userInChallengeRepository->isUserParticipatingInChallenge($userId, $challenge)) {
// new player is joining
$userInChallenge = new UserInChallenge();
$userInChallenge->setUserId($userId);
$userInChallenge->setChallenge($challenge);
$userInChallenge->setTimeLeft($challenge->getTimeLimit());
\Container::$persistentDataManager->saveToDb($userInChallenge);
}
$map = $this->mapRepository->getByChallenge($challenge);
return new JsonContent([
'mapId' => $map->getId(),
'mapName' => $map->getName(),
'bounds' => $map->getBounds()->toArray()
]);
}
private function getMultiToken(string $roomId): string
{
$session = \Container::$request->session();
if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) {
$token = bin2hex(random_bytes(16));
$session->set('multiState', [
'roomId' => $roomId,
'token' => $token
]);
} else {
$token = $multiState['token'];
}
return $token;
} }
} }
+75 -389
View File
@@ -1,457 +1,143 @@
<?php namespace MapGuesser\Controller; <?php namespace MapGuesser\Controller;
use DateTime; use MapGuesser\Interfaces\Request\IRequest;
use SokoWeb\Interfaces\Authentication\IAuthenticationRequired;
use MapGuesser\Util\Geo\Position; use MapGuesser\Util\Geo\Position;
use SokoWeb\Response\JsonContent; use MapGuesser\Response\JsonContent;
use SokoWeb\Interfaces\Response\IContent; use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\Multi\MultiConnector;
use MapGuesser\PersistentData\Model\Challenge;
use MapGuesser\PersistentData\Model\Guess;
use MapGuesser\PersistentData\Model\Map;
use MapGuesser\PersistentData\Model\Place; use MapGuesser\PersistentData\Model\Place;
use MapGuesser\PersistentData\Model\PlaceInChallenge;
use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\Model\UserPlayedPlace;
use MapGuesser\Repository\GuessRepository;
use MapGuesser\Repository\MultiRoomRepository;
use MapGuesser\Repository\PlaceInChallengeRepository;
use MapGuesser\Repository\PlaceRepository; use MapGuesser\Repository\PlaceRepository;
use MapGuesser\Repository\UserInChallengeRepository;
use MapGuesser\Repository\UserPlayedPlaceRepository;
class GameFlowController implements IAuthenticationRequired class GameFlowController
{ {
const NUMBER_OF_ROUNDS = 5; const NUMBER_OF_ROUNDS = 5;
const MAX_SCORE = 1000; const MAX_SCORE = 1000;
private MultiConnector $multiConnector; private IRequest $request;
private MultiRoomRepository $multiRoomRepository;
private PlaceRepository $placeRepository; private PlaceRepository $placeRepository;
private UserPlayedPlaceRepository $userPlayedPlaceRepository; public function __construct(IRequest $request)
private UserInChallengeRepository $userInChallengeRepository;
private PlaceInChallengeRepository $placeInChallengeRepository;
private GuessRepository $guessRepository;
public function __construct()
{ {
$this->multiConnector = new MultiConnector(); $this->request = $request;
$this->multiRoomRepository = new MultiRoomRepository();
$this->placeRepository = new PlaceRepository(); $this->placeRepository = new PlaceRepository();
$this->userPlayedPlaceRepository = new UserPlayedPlaceRepository();
$this->userInChallengeRepository = new UserInChallengeRepository();
$this->placeInChallengeRepository = new PlaceInChallengeRepository();
$this->guessRepository = new GuessRepository();
} }
public function isAuthenticationRequired(): bool public function getNewPlace(): IContent
{ {
return empty($_ENV['ENABLE_GAME_FOR_GUESTS']); $mapId = (int) $this->request->query('mapId');
}
public function initialData(): IContent $session = $this->request->session();
{
$mapId = (int) \Container::$request->query('mapId');
$session = \Container::$request->session();
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) { if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
return new JsonContent(['error' => 'no_session_found']); return new JsonContent(['error' => 'no_session_found']);
} }
if (!isset($state['currentRound']) || $state['currentRound'] == -1 || $state['currentRound'] >= static::NUMBER_OF_ROUNDS) { if (count($state['rounds']) === 0) {
$this->startNewGame($state, $mapId); $placesWithoutPano = [];
$place = $this->placeRepository->getRandomForMapWithValidPano($mapId, [], $placesWithoutPano);
$this->addNewRoundToState($state, $place, $placesWithoutPano);
$session->set('state', $state); $session->set('state', $state);
}
$response = []; $data = [
'panoId' => $place->getPanoIdCached(),
$last = $state['rounds'][$state['currentRound']]; 'pov' => $place->getPov()->toArray()
$response['place'] = [
'panoId' => $last['panoId'],
'pov' => $last['pov']->toArray()
]; ];
} else {
$rounds = count($state['rounds']);
$last = $state['rounds'][$rounds - 1];
$response['history'] = []; $history = [];
for ($i = 0; $i < $state['currentRound']; ++$i) { for ($i = 0; $i < $rounds - 1; ++$i) {
$round = $state['rounds'][$i]; $round = $state['rounds'][$i];
$response['history'][] = [ $history[] = [
'position' => $round['position']->toArray(), 'position' => $round['position']->toArray(),
'result' => [
'guessPosition' => $round['guessPosition']->toArray(), 'guessPosition' => $round['guessPosition']->toArray(),
'distance' => $round['distance'], 'distance' => $round['distance'],
'score' => $round['score'] 'score' => $round['score']
]
]; ];
} }
return new JsonContent($response); $data = [
'history' => $history,
'panoId' => $last['panoId'],
'pov' => isset($last['pov']) ? // should be checked not to break with old sessions
$last['pov']->toArray() :
['heading' => 0.0, 'pitch' => 0.0, 'zoom' => 0.0],
];
} }
public function multiInitialData(): IContent return new JsonContent($data);
}
public function evaluateGuess(): IContent
{ {
$roomId = \Container::$request->query('roomId'); $mapId = (int) $this->request->query('mapId');
$session = \Container::$request->session();
if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) { $session = $this->request->session();
return new JsonContent(['error' => 'no_session_found']);
}
$room = $this->multiRoomRepository->getByRoomId($roomId);
$state = $room->getStateArray();
$members = $room->getMembersArray();
if ($members['owner'] !== $multiState['token']) {
return new JsonContent(['error' => 'not_owner_of_room']);
}
if ($state['currentRound'] == -1 || $state['currentRound'] >= static::NUMBER_OF_ROUNDS - 1) {
$this->startNewGame($state, $state['mapId']);
$room->setStateArray($state);
$room->setUpdatedDate(new DateTime());
\Container::$persistentDataManager->saveToDb($room);
}
$places = [];
foreach ($state['rounds'] as $round) {
$places[] = [
'position' => $round['position']->toArray(),
'panoId' => $round['panoId'],
'pov' => $round['pov']->toArray()
];
}
$this->multiConnector->sendMessage('start_game', ['roomId' => $roomId, 'places' => $places]);
return new JsonContent(['ok' => true]);
}
private function prepareChallengeResponse(int $userId, Challenge $challenge, int $currentRound, bool $withHistory = false): array
{
$currentPlace = $this->placeRepository->getByRoundInChallenge($challenge, $currentRound);
// if the last round was played ($currentPlace == null) or history is explicitly requested (for initializing)
if (!isset($currentPlace) || $withHistory) {
$withRelations = ['user', 'place_in_challange', 'place'];
foreach ($this->guessRepository->getAllInChallenge($challenge, $withRelations) as $guess) {
$round = $guess->getPlaceInChallenge()->getRound();
if ($guess->getUser()->getId() === $userId) {
$response['history'][$round]['position'] =
$guess->getPlaceInChallenge()->getPlace()->getPosition()->toArray();
$response['history'][$round]['result'] = [
'guessPosition' => $guess->getPosition()->toArray(),
'distance' => $guess->getDistance(),
'score' => $guess->getScore()
];
} else {
$response['history'][$round]['allResults'][] = [
'userName' => $guess->getUser()->getDisplayName(),
'guessPosition' => $guess->getPosition()->toArray(),
'distance' => $guess->getDistance(),
'score' => $guess->getScore()
];
}
}
// setting default values for rounds without guesses (because of timeout)
for ($i = 0; $i < $currentRound; ++$i) {
if (!isset($response['history'][$i]) || !isset($response['history'][$i]['result'])) {
$response['history'][$i]['result'] = [
'guessPosition' => null,
'distance' => null,
'score' => 0
];
$response['history'][$i]['position'] =
$this->placeRepository->getByRoundInChallenge($challenge, $i)->getPosition()->toArray();
}
}
$response['history']['length'] = $currentRound;
}
if (!isset($currentPlace)) { // game finished
$response['finished'] = true;
} else { // continue game
$response['place'] = [
'panoId' => $currentPlace->getPanoIdCached(),
'pov' => $currentPlace->getPov()->toArray()
];
$prevRound = $currentRound - 1;
if ($prevRound >= 0) {
foreach ($this->guessRepository->getAllInChallengeByRound($prevRound, $challenge, ['user']) as $guess) {
if ($guess->getUser()->getId() != $userId) {
$response['allResults'][] = [
'userName' => $guess->getUser()->getDisplayName(),
'guessPosition' => $guess->getPosition()->toArray(),
'distance' => $guess->getDistance(),
'score' => $guess->getScore()
];
}
}
}
}
$response['restrictions'] = [
'timeLimit' => $challenge->getTimeLimit() * 1000,
'timeLimitType' => $challenge->getTimeLimitType(),
'noMove' => $challenge->getNoMove(),
'noPan' => $challenge->getNoPan(),
'noZoom' => $challenge->getNoZoom()
];
return $response;
}
public function challengeInitialData(): IContent
{
$session = \Container::$request->session();
$userId = $session->get('userId');
$challengeToken_str = \Container::$request->query('challengeToken');
$userInChallenge = $this->userInChallengeRepository->getByUserIdAndToken($userId, $challengeToken_str, ['challenge']);
if (!isset($userInChallenge)) {
return new JsonContent(['error' => 'game_not_found']);
}
$challenge = $userInChallenge->getChallenge();
$currentRound = $userInChallenge->getCurrentRound();
$response = $this->prepareChallengeResponse($userId, $challenge, $currentRound, true);
if ($challenge->getTimeLimitType() === 'game' && $challenge->getTimeLimit() !== null && $userInChallenge->getCurrentRound() > 0) {
$timeLimit = max(10, $userInChallenge->getTimeLeft());
$response['restrictions']['timeLimit'] = $timeLimit * 1000;
}
return new JsonContent($response);
}
public function guess(): IContent
{
$mapId = (int) \Container::$request->query('mapId');
$session = \Container::$request->session();
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) { if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
return new JsonContent(['error' => 'no_session_found']); return new JsonContent(['error' => 'no_session_found']);
} }
$last = $state['rounds'][$state['currentRound']]; $last = $state['rounds'][count($state['rounds']) - 1];
$guessPosition = new Position((float) \Container::$request->post('lat'), (float) \Container::$request->post('lng'));
$result = $this->evaluateGuess($last['position'], $guessPosition, $state['area']); $position = $last['position'];
$guessPosition = new Position((float) $this->request->post('lat'), (float) $this->request->post('lng'));
$distance = $this->calculateDistance($position, $guessPosition);
$score = $this->calculateScore($distance, $state['area']);
$last['guessPosition'] = $guessPosition; $last['guessPosition'] = $guessPosition;
$last['distance'] = $result['distance']; $last['distance'] = $distance;
$last['score'] = $result['score']; $last['score'] = $score;
$state['rounds'][count($state['rounds']) - 1] = $last;
$response = [ if (count($state['rounds']) < static::NUMBER_OF_ROUNDS) {
'position' => $last['position']->toArray(), $exclude = [];
'result' => $result
];
$state['rounds'][$state['currentRound']] = $last; foreach ($state['rounds'] as $round) {
$state['currentRound'] += 1; $exclude = array_merge($exclude, $round['placesWithoutPano'], [$round['placeId']]);
if ($state['currentRound'] < static::NUMBER_OF_ROUNDS) { }
$next = $state['rounds'][$state['currentRound']];
$response['place'] = [ $placesWithoutPano = [];
'panoId' => $next['panoId'], $place = $this->placeRepository->getRandomForMapWithValidPano($mapId, $exclude, $placesWithoutPano);
'pov' => $next['pov']->toArray()
]; $this->addNewRoundToState($state, $place, $placesWithoutPano);
$panoId = $place->getPanoIdCached();
$pov = $place->getPov()->toArray();
} else {
$state['rounds'] = [];
$panoId = null;
$pov = null;
} }
$session->set('state', $state); $session->set('state', $state);
$this->saveVisit($last['placeId']); return new JsonContent([
'result' => [
return new JsonContent($response); 'position' => $position->toArray(),
} 'distance' => $distance,
'score' => $score
// save the selected place for the round in UserPlayedPlace ],
private function saveVisit($placeId): void 'panoId' => $panoId,
{ 'pov' => $pov
$session = \Container::$request->session();
$userId = $session->get('userId');
if (isset($userId)) {
$userPlayedPlace = $this->userPlayedPlaceRepository->getByUserIdAndPlaceId($userId, $placeId);
if (!$userPlayedPlace) {
$userPlayedPlace = new UserPlayedPlace();
$userPlayedPlace->setUserId($userId);
$userPlayedPlace->setPlaceId($placeId);
} else {
$userPlayedPlace->incrementOccurrences();
}
$userPlayedPlace->setLastTimeDate(new DateTime());
\Container::$persistentDataManager->saveToDb($userPlayedPlace);
}
}
public function multiGuess(): IContent
{
$roomId = \Container::$request->query('roomId');
$session = \Container::$request->session();
if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) {
return new JsonContent(['error' => 'no_session_found']);
}
$room = $this->multiRoomRepository->getByRoomId($roomId);
$state = $room->getStateArray();
$last = $state['rounds'][$state['currentRound']];
$guessPosition = new Position((float) \Container::$request->post('lat'), (float) \Container::$request->post('lng'));
$result = $this->evaluateGuess($last['position'], $guessPosition, $state['area']);
$responseFromMulti = $this->multiConnector->sendMessage('guess', [
'roomId' => $roomId,
'token' => $multiState['token'],
'guessPosition' => $guessPosition->toArray(),
'distance' => $result['distance'],
'score' => $result['score']
]); ]);
if (isset($responseFromMulti['error'])) {
return new JsonContent(['error' => $responseFromMulti['error']]);
} }
$response = [ private function addNewRoundToState(array &$state, Place $place, array $placesWithoutPano): void
'position' => $last['position']->toArray(),
'result' => $result,
'allResults' => $responseFromMulti['allResults']
];
return new JsonContent($response);
}
public function challengeGuess(): IContent
{ {
$session = \Container::$request->session();
$userId = $session->get('userId');
$challengeToken_str = \Container::$request->query('challengeToken');
$userInChallenge = $this->userInChallengeRepository->getByUserIdAndToken($userId, $challengeToken_str, ['challenge']);
if (!isset($userInChallenge)) {
return new JsonContent(['error' => 'game_not_found']);
}
$challenge = $userInChallenge->getChallenge();
$currentRound = $userInChallenge->getCurrentRound();
$currentPlaceInChallenge = $this->placeInChallengeRepository->getByRoundInChallenge($currentRound, $challenge, ['place', 'map']);
$currentPlace = $currentPlaceInChallenge->getPlace();
$map = $currentPlace->getMap();
// creating response
$nextRound = $currentRound + 1;
$response = $this->prepareChallengeResponse($userId, $challenge, $nextRound);
$response['position'] = $currentPlace->getPosition()->toArray();
if (\Container::$request->post('lat') && \Container::$request->post('lng')) {
$guessPosition = new Position((float) \Container::$request->post('lat'), (float) \Container::$request->post('lng'));
$result = $this->evaluateGuess($currentPlace->getPosition(), $guessPosition, $map->getArea());
// save guess
$guess = new Guess();
$guess->setUserId($userId);
$guess->setPlaceInChallenge($currentPlaceInChallenge);
$guess->setPosition($guessPosition);
$guess->setDistance($result['distance']);
$guess->setScore($result['score']);
\Container::$persistentDataManager->saveToDb($guess);
$response['result'] = $result;
} else {
// user didn't manage to guess in the round in the given timeframe
$response['result'] = ['distance' => null, 'score' => 0];
}
// save user relevant state of challenge
$userInChallenge->setCurrentRound($nextRound);
$timeLeft = \Container::$request->post('timeLeft');
if (isset($timeLeft)) {
$userInChallenge->setTimeLeft(intval($timeLeft));
}
\Container::$persistentDataManager->saveToDb($userInChallenge);
if ($challenge->getTimeLimitType() === 'game' && isset($timeLeft)) {
$timeLimit = max(10, intval($timeLeft));
$response['restrictions']['timeLimit'] = $timeLimit * 1000;
}
if (isset($response['history'][$currentRound]['allResults'])) {
$response['allResults'] = $response['history'][$currentRound]['allResults'];
}
$this->saveVisit($currentPlace->getId());
return new JsonContent($response);
}
public function multiNextRound(): IContent
{
$roomId = \Container::$request->query('roomId');
$session = \Container::$request->session();
if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) {
return new JsonContent(['error' => 'no_session_found']);
}
$room = $this->multiRoomRepository->getByRoomId($roomId);
$state = $room->getStateArray();
$members = $room->getMembersArray();
if ($members['owner'] !== $multiState['token']) {
return new JsonContent(['error' => 'not_owner_of_room']);
}
$state['currentRound'] += 1;
if ($state['currentRound'] < static::NUMBER_OF_ROUNDS) {
$this->multiConnector->sendMessage('next_round', ['roomId' => $roomId, 'currentRound' => $state['currentRound']]);
}
$room->setStateArray($state);
$room->setUpdatedDate(new DateTime());
\Container::$persistentDataManager->saveToDb($room);
return new JsonContent(['ok' => true]);
}
private function evaluateGuess(Position $realPosition, Position $guessPosition, float $area)
{
$distance = $this->calculateDistance($realPosition, $guessPosition);
$score = $this->calculateScore($distance, $area);
return ['distance' => $distance, 'score' => $score];
}
private function startNewGame(array &$state, int $mapId): void
{
$session = \Container::$request->session();
$userId = $session->get('userId');
$places = $this->placeRepository->getRandomNPlaces($mapId, static::NUMBER_OF_ROUNDS, $userId);
$state['rounds'] = [];
$state['currentRound'] = 0;
foreach ($places as $place) {
$state['rounds'][] = [ $state['rounds'][] = [
'placesWithoutPano' => $placesWithoutPano,
'placeId' => $place->getId(), 'placeId' => $place->getId(),
'position' => $place->getPosition(), 'position' => $place->getPosition(),
'panoId' => $place->getPanoIdCached(), 'panoId' => $place->getPanoIdCached(),
'pov' => $place->getPov() 'pov' => $place->getPov()
]; ];
} }
}
private function calculateDistance(Position $realPosition, Position $guessPosition): float private function calculateDistance(Position $realPosition, Position $guessPosition): float
{ {
+13 -5
View File
@@ -1,12 +1,20 @@
<?php namespace MapGuesser\Controller; <?php namespace MapGuesser\Controller;
use SokoWeb\Interfaces\Response\IContent; use MapGuesser\Interfaces\Request\IRequest;
use SokoWeb\Interfaces\Response\IRedirect; use MapGuesser\Interfaces\Response\IContent;
use SokoWeb\Response\JsonContent; use MapGuesser\Interfaces\Response\IRedirect;
use SokoWeb\Response\Redirect; use MapGuesser\Response\JsonContent;
use MapGuesser\Response\Redirect;
class HomeController class HomeController
{ {
private IRequest $request;
public function __construct(IRequest $request)
{
$this->request = $request;
}
public function getIndex(): IRedirect public function getIndex(): IRedirect
{ {
return new Redirect(\Container::$routeCollection->getRoute('maps')->generateLink(), IRedirect::TEMPORARY); return new Redirect(\Container::$routeCollection->getRoute('maps')->generateLink(), IRedirect::TEMPORARY);
@@ -16,6 +24,6 @@ class HomeController
{ {
// session starts with the request, this method just sends valid data to the client // session starts with the request, this method just sends valid data to the client
return new JsonContent(['antiCsrfToken' => \Container::$request->session()->get('anti_csrf_token')]); return new JsonContent(['antiCsrfToken' => $this->request->session()->get('anti_csrf_token')]);
} }
} }
+133 -167
View File
@@ -2,55 +2,52 @@
use DateInterval; use DateInterval;
use DateTime; use DateTime;
use SokoWeb\Http\Request; use MapGuesser\Http\Request;
use SokoWeb\Interfaces\Response\IContent; use MapGuesser\Interfaces\Request\IRequest;
use SokoWeb\Interfaces\Response\IRedirect; use MapGuesser\Interfaces\Response\IContent;
use SokoWeb\Mailing\Mail; use MapGuesser\Interfaces\Response\IRedirect;
use SokoWeb\OAuth\GoogleOAuth; use MapGuesser\Mailing\Mail;
use MapGuesser\OAuth\GoogleOAuth;
use MapGuesser\PersistentData\Model\User; use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\Model\UserConfirmation; use MapGuesser\PersistentData\Model\UserConfirmation;
use MapGuesser\PersistentData\Model\UserPasswordResetter; use MapGuesser\PersistentData\Model\UserPasswordResetter;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Repository\UserConfirmationRepository; use MapGuesser\Repository\UserConfirmationRepository;
use MapGuesser\Repository\UserPasswordResetterRepository; use MapGuesser\Repository\UserPasswordResetterRepository;
use MapGuesser\Repository\UserPlayedPlaceRepository;
use MapGuesser\Repository\UserRepository; use MapGuesser\Repository\UserRepository;
use SokoWeb\Response\HtmlContent; use MapGuesser\Response\HtmlContent;
use SokoWeb\Response\JsonContent; use MapGuesser\Response\JsonContent;
use SokoWeb\Response\Redirect; use MapGuesser\Response\Redirect;
use SokoWeb\Util\CaptchaValidator; use MapGuesser\Util\JwtParser;
use SokoWeb\Util\JwtParser;
class LoginController class LoginController
{ {
private IRequest $request;
private PersistentDataManager $pdm;
private UserRepository $userRepository; private UserRepository $userRepository;
private UserConfirmationRepository $userConfirmationRepository; private UserConfirmationRepository $userConfirmationRepository;
private UserPasswordResetterRepository $userPasswordResetterRepository; private UserPasswordResetterRepository $userPasswordResetterRepository;
private UserPlayedPlaceRepository $userPlayedPlaceRepository; public function __construct(IRequest $request)
private string $redirectUrl;
public function __construct()
{ {
$this->request = $request;
$this->pdm = new PersistentDataManager();
$this->userRepository = new UserRepository(); $this->userRepository = new UserRepository();
$this->userConfirmationRepository = new UserConfirmationRepository(); $this->userConfirmationRepository = new UserConfirmationRepository();
$this->userPasswordResetterRepository = new UserPasswordResetterRepository(); $this->userPasswordResetterRepository = new UserPasswordResetterRepository();
$this->userPlayedPlaceRepository = new UserPlayedPlaceRepository();
$this->redirectUrl = \Container::$request->session()->has('redirect_after_login') ?
\Container::$request->session()->get('redirect_after_login') :
\Container::$routeCollection->getRoute('index')->generateLink();
} }
public function getLoginForm() public function getLoginForm()
{ {
if (\Container::$request->user() !== null) { if ($this->request->user() !== null) {
$this->deleteRedirectUrl(); return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
} }
return new HtmlContent('login/login', ['redirectUrl' => $this->redirectUrl]); return new HtmlContent('login/login');
} }
public function getGoogleLoginRedirect(): IRedirect public function getGoogleLoginRedirect(): IRedirect
@@ -58,13 +55,13 @@ class LoginController
$state = bin2hex(random_bytes(16)); $state = bin2hex(random_bytes(16));
$nonce = bin2hex(random_bytes(16)); $nonce = bin2hex(random_bytes(16));
\Container::$request->session()->set('oauth_state', $state); $this->request->session()->set('oauth_state', $state);
\Container::$request->session()->set('oauth_nonce', $nonce); $this->request->session()->set('oauth_nonce', $nonce);
$oAuth = new GoogleOAuth(new Request()); $oAuth = new GoogleOAuth(new Request());
$url = $oAuth->getDialogUrl( $url = $oAuth->getDialogUrl(
$state, $state,
\Container::$request->getBase() . \Container::$routeCollection->getRoute('login-google-action')->generateLink(), $this->request->getBase() . '/' . \Container::$routeCollection->getRoute('login-google-action')->generateLink(),
$nonce $nonce
); );
@@ -73,13 +70,12 @@ class LoginController
public function getSignupForm() public function getSignupForm()
{ {
if (\Container::$request->user() !== null) { if ($this->request->user() !== null) {
$this->deleteRedirectUrl(); return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
} }
if (\Container::$request->session()->has('tmp_user_data')) { if ($this->request->session()->has('tmp_user_data')) {
$tmpUserData = \Container::$request->session()->get('tmp_user_data'); $tmpUserData = $this->request->session()->get('tmp_user_data');
$data = ['email' => $tmpUserData['email']]; $data = ['email' => $tmpUserData['email']];
} else { } else {
@@ -96,30 +92,28 @@ class LoginController
public function getSignupWithGoogleForm() public function getSignupWithGoogleForm()
{ {
if (\Container::$request->user() !== null) { if ($this->request->user() !== null) {
$this->deleteRedirectUrl(); return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
} }
if (!\Container::$request->session()->has('google_user_data')) { if (!$this->request->session()->has('google_user_data')) {
return new Redirect(\Container::$routeCollection->getRoute('login-google')->generateLink(), IRedirect::TEMPORARY); return new Redirect(\Container::$routeCollection->getRoute('login-google')->generateLink(), IRedirect::TEMPORARY);
} }
$userData = \Container::$request->session()->get('google_user_data'); $userData = $this->request->session()->get('google_user_data');
$user = $this->userRepository->getByEmail($userData['email']); $user = $this->userRepository->getByEmail($userData['email']);
return new HtmlContent('login/google_signup', ['found' => $user !== null, 'email' => $userData['email'], 'redirectUrl' => $this->redirectUrl]); return new HtmlContent('login/google_signup', ['found' => $user !== null, 'email' => $userData['email']]);
} }
public function getRequestPasswordResetForm() public function getRequestPasswordResetForm()
{ {
if (\Container::$request->user() !== null) { if ($this->request->user() !== null) {
$this->deleteRedirectUrl(); return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
} }
return new HtmlContent('login/password_reset_request', ['email' => \Container::$request->query('email')]); return new HtmlContent('login/password_reset_request', ['email' => $this->request->query('email')]);
} }
public function getRequestPasswordResetSuccess(): IContent public function getRequestPasswordResetSuccess(): IContent
@@ -129,12 +123,11 @@ class LoginController
public function getResetPasswordForm() public function getResetPasswordForm()
{ {
if (\Container::$request->user() !== null) { if ($this->request->user() !== null) {
$this->deleteRedirectUrl(); return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
} }
$token = \Container::$request->query('token'); $token = $this->request->query('token');
$resetter = $this->userPasswordResetterRepository->getByToken($token); $resetter = $this->userPasswordResetterRepository->getByToken($token);
if ($resetter === null || $resetter->getExpiresDate() < new DateTime()) { if ($resetter === null || $resetter->getExpiresDate() < new DateTime()) {
@@ -143,20 +136,19 @@ class LoginController
$user = $this->userRepository->getById($resetter->getUserId()); $user = $this->userRepository->getById($resetter->getUserId());
return new HtmlContent('login/reset_password', ['success' => true, 'token' => $token, 'email' => $user->getEmail(), 'redirectUrl' => $this->redirectUrl]); return new HtmlContent('login/reset_password', ['success' => true, 'token' => $token, 'email' => $user->getEmail()]);
} }
public function login(): IContent public function login(): IContent
{ {
if (\Container::$request->user() !== null) { if ($this->request->user() !== null) {
$this->deleteRedirectUrl();
return new JsonContent(['success' => true]); return new JsonContent(['success' => true]);
} }
$user = $this->userRepository->getByEmail(\Container::$request->post('email')); $user = $this->userRepository->getByEmail($this->request->post('email'));
if ($user === null) { if ($user === null) {
if (strlen(\Container::$request->post('password')) < 6) { if (strlen($this->request->post('password')) < 6) {
return new JsonContent([ return new JsonContent([
'error' => [ 'error' => [
'errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!' 'errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!'
@@ -165,16 +157,16 @@ class LoginController
} }
$tmpUser = new User(); $tmpUser = new User();
$tmpUser->setPlainPassword(\Container::$request->post('password')); $tmpUser->setPlainPassword($this->request->post('password'));
\Container::$request->session()->set('tmp_user_data', [ $this->request->session()->set('tmp_user_data', [
'email' => \Container::$request->post('email'), 'email' => $this->request->post('email'),
'password_hashed' => $tmpUser->getPassword() 'password_hashed' => $tmpUser->getPassword()
]); ]);
return new JsonContent([ return new JsonContent([
'redirect' => [ 'redirect' => [
'target' => \Container::$routeCollection->getRoute('signup')->generateLink() 'target' => '/' . \Container::$routeCollection->getRoute('signup')->generateLink()
] ]
]); ]);
} }
@@ -190,7 +182,7 @@ class LoginController
]); ]);
} }
if (!$user->checkPassword(\Container::$request->post('password'))) { if (!$user->checkPassword($this->request->post('password'))) {
return new JsonContent([ return new JsonContent([
'error' => [ 'error' => [
'errorText' => 'The given password is wrong. You can <a href="/password/requestReset?email=' . 'errorText' => 'The given password is wrong. You can <a href="/password/requestReset?email=' .
@@ -199,27 +191,25 @@ class LoginController
]); ]);
} }
\Container::$request->setUser($user); $this->request->setUser($user);
$this->deleteRedirectUrl();
return new JsonContent(['success' => true]); return new JsonContent(['success' => true]);
} }
public function loginWithGoogle() public function loginWithGoogle()
{ {
if (\Container::$request->user() !== null) { if ($this->request->user() !== null) {
$this->deleteRedirectUrl(); return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
} }
if (\Container::$request->query('state') !== \Container::$request->session()->get('oauth_state')) { if ($this->request->query('state') !== $this->request->session()->get('oauth_state')) {
return new HtmlContent('login/google_login'); return new HtmlContent('login/google_login');
} }
$oAuth = new GoogleOAuth(new Request()); $oAuth = new GoogleOAuth(new Request());
$tokenData = $oAuth->getToken( $tokenData = $oAuth->getToken(
\Container::$request->query('code'), $this->request->query('code'),
\Container::$request->getBase() . \Container::$routeCollection->getRoute('login-google-action')->generateLink() $this->request->getBase() . '/' . \Container::$routeCollection->getRoute('login-google-action')->generateLink()
); );
if (!isset($tokenData['id_token'])) { if (!isset($tokenData['id_token'])) {
@@ -229,7 +219,7 @@ class LoginController
$jwtParser = new JwtParser($tokenData['id_token']); $jwtParser = new JwtParser($tokenData['id_token']);
$idToken = $jwtParser->getPayload(); $idToken = $jwtParser->getPayload();
if ($idToken['nonce'] !== \Container::$request->session()->get('oauth_nonce')) { if ($idToken['nonce'] !== $this->request->session()->get('oauth_nonce')) {
return new HtmlContent('login/google_login'); return new HtmlContent('login/google_login');
} }
@@ -240,36 +230,34 @@ class LoginController
$user = $this->userRepository->getByGoogleSub($idToken['sub']); $user = $this->userRepository->getByGoogleSub($idToken['sub']);
if ($user === null) { if ($user === null) {
\Container::$request->session()->set('google_user_data', ['sub' => $idToken['sub'], 'email' => $idToken['email']]); $this->request->session()->set('google_user_data', ['sub' => $idToken['sub'], 'email' => $idToken['email']]);
return new Redirect(\Container::$routeCollection->getRoute('signup-google')->generateLink(), IRedirect::TEMPORARY); return new Redirect(\Container::$routeCollection->getRoute('signup-google')->generateLink(), IRedirect::TEMPORARY);
} }
\Container::$request->setUser($user); $this->request->setUser($user);
$this->deleteRedirectUrl(); return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
} }
public function logout(): IRedirect public function logout(): IRedirect
{ {
\Container::$request->setUser(null); $this->request->setUser(null);
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY); return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
} }
public function signup(): IContent public function signup(): IContent
{ {
if (\Container::$request->user() !== null) { if ($this->request->user() !== null) {
$this->deleteRedirectUrl(); return new JsonContent(['redirect' => ['target' => '/' . \Container::$routeCollection->getRoute('home')->generateLink()]]);
return new JsonContent(['redirect' => ['target' => $this->redirectUrl]]);
} }
$user = $this->userRepository->getByEmail(\Container::$request->post('email')); $user = $this->userRepository->getByEmail($this->request->post('email'));
if ($user !== null) { if ($user !== null) {
if ($user->getActive()) { if ($user->getActive()) {
if (!$user->checkPassword(\Container::$request->post('password'))) { if (!$user->checkPassword($this->request->post('password'))) {
return new JsonContent([ return new JsonContent([
'error' => [ 'error' => [
'errorText' => 'There is a user already registered with the given email address, ' . 'errorText' => 'There is a user already registered with the given email address, ' .
@@ -279,10 +267,9 @@ class LoginController
]); ]);
} }
\Container::$request->setUser($user); $this->request->setUser($user);
$this->deleteRedirectUrl(); $data = ['redirect' => ['target' => '/' . \Container::$routeCollection->getRoute('index')->generateLink()]];
$data = ['redirect' => ['target' => $this->redirectUrl]];
} else { } else {
$data = [ $data = [
'error' => [ 'error' => [
@@ -294,33 +281,21 @@ class LoginController
return new JsonContent($data); return new JsonContent($data);
} }
if (!empty($_ENV['RECAPTCHA_SITEKEY'])) { if (filter_var($this->request->post('email'), FILTER_VALIDATE_EMAIL) === false) {
if (!\Container::$request->post('g-recaptcha-response')) {
return new JsonContent(['error' => ['errorText' => 'Please check "I\'m not a robot" in the reCAPTCHA box!']]);
}
$captchaValidator = new CaptchaValidator();
$captchaResponse = $captchaValidator->validate(\Container::$request->post('g-recaptcha-response'));
if (!$captchaResponse['success']) {
return new JsonContent(['error' => ['errorText' => 'reCAPTCHA challenge failed. Please try again!']]);
}
}
if (filter_var(\Container::$request->post('email'), FILTER_VALIDATE_EMAIL) === false) {
return new JsonContent(['error' => ['errorText' => 'The given email address is not valid.']]); return new JsonContent(['error' => ['errorText' => 'The given email address is not valid.']]);
} }
if (\Container::$request->session()->has('tmp_user_data')) { if ($this->request->session()->has('tmp_user_data')) {
$tmpUserData = \Container::$request->session()->get('tmp_user_data'); $tmpUserData = $this->request->session()->get('tmp_user_data');
$tmpUser = new User(); $tmpUser = new User();
$tmpUser->setPassword($tmpUserData['password_hashed']); $tmpUser->setPassword($tmpUserData['password_hashed']);
if (!$tmpUser->checkPassword(\Container::$request->post('password'))) { if (!$tmpUser->checkPassword($this->request->post('password'))) {
return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]); return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]);
} }
} else { } else {
if (strlen(\Container::$request->post('password')) < 6) { if (strlen($this->request->post('password')) < 6) {
return new JsonContent([ return new JsonContent([
'error' => [ 'error' => [
'errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!' 'errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!'
@@ -328,17 +303,19 @@ class LoginController
]); ]);
} }
if (\Container::$request->post('password') !== \Container::$request->post('password_confirm')) { if ($this->request->post('password') !== $this->request->post('password_confirm')) {
return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]); return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]);
} }
} }
$user = new User(); $user = new User();
$user->setEmail(\Container::$request->post('email')); $user->setEmail($this->request->post('email'));
$user->setPlainPassword(\Container::$request->post('password')); $user->setPlainPassword($this->request->post('password'));
$user->setCreatedDate(new DateTime()); $user->setCreatedDate(new DateTime());
\Container::$persistentDataManager->saveToDb($user); \Container::$dbConnection->startTransaction();
$this->pdm->saveToDb($user);
$token = bin2hex(random_bytes(16)); $token = bin2hex(random_bytes(16));
@@ -347,23 +324,24 @@ class LoginController
$confirmation->setToken($token); $confirmation->setToken($token);
$confirmation->setLastSentDate(new DateTime()); $confirmation->setLastSentDate(new DateTime());
\Container::$persistentDataManager->saveToDb($confirmation); $this->pdm->saveToDb($confirmation);
\Container::$dbConnection->commit();
$this->sendConfirmationEmail($user->getEmail(), $token, $user->getCreatedDate()); $this->sendConfirmationEmail($user->getEmail(), $token, $user->getCreatedDate());
\Container::$request->session()->delete('tmp_user_data'); $this->request->session()->delete('tmp_user_data');
return new JsonContent(['success' => true]); return new JsonContent(['success' => true]);
} }
public function signupWithGoogle(): IContent public function signupWithGoogle(): IContent
{ {
if (\Container::$request->user() !== null) { if ($this->request->user() !== null) {
$this->deleteRedirectUrl();
return new JsonContent(['success' => true]); return new JsonContent(['success' => true]);
} }
$userData = \Container::$request->session()->get('google_user_data'); $userData = $this->request->session()->get('google_user_data');
$user = $this->userRepository->getByEmail($userData['email']); $user = $this->userRepository->getByEmail($userData['email']);
@@ -380,109 +358,96 @@ class LoginController
$user->setActive(true); $user->setActive(true);
$user->setGoogleSub($userData['sub']); $user->setGoogleSub($userData['sub']);
\Container::$persistentDataManager->saveToDb($user); $this->pdm->saveToDb($user);
if ($sendWelcomeEmail) { if ($sendWelcomeEmail) {
$this->sendWelcomeEmail($user->getEmail()); $this->sendWelcomeEmail($user->getEmail());
} }
\Container::$request->session()->delete('google_user_data'); $this->request->session()->delete('google_user_data');
\Container::$request->setUser($user); $this->request->setUser($user);
$this->deleteRedirectUrl();
return new JsonContent(['success' => true]); return new JsonContent(['success' => true]);
} }
public function resetSignup(): IContent public function resetSignup(): IContent
{ {
\Container::$request->session()->delete('tmp_user_data'); $this->request->session()->delete('tmp_user_data');
return new JsonContent(['success' => true]); return new JsonContent(['success' => true]);
} }
public function resetGoogleSignup(): IContent public function resetGoogleSignup(): IContent
{ {
\Container::$request->session()->delete('google_user_data'); $this->request->session()->delete('google_user_data');
return new JsonContent(['success' => true]); return new JsonContent(['success' => true]);
} }
public function activate() public function activate()
{ {
if (\Container::$request->user() !== null) { if ($this->request->user() !== null) {
$this->deleteRedirectUrl(); return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
} }
$confirmation = $this->userConfirmationRepository->getByToken(substr(\Container::$request->query('token'), 0, 32)); $confirmation = $this->userConfirmationRepository->getByToken(substr($this->request->query('token'), 0, 32));
if ($confirmation === null) { if ($confirmation === null) {
return new HtmlContent('login/activate'); return new HtmlContent('login/activate');
} }
\Container::$persistentDataManager->deleteFromDb($confirmation); \Container::$dbConnection->startTransaction();
$this->pdm->deleteFromDb($confirmation);
$user = $this->userRepository->getById($confirmation->getUserId()); $user = $this->userRepository->getById($confirmation->getUserId());
$user->setActive(true); $user->setActive(true);
\Container::$persistentDataManager->saveToDb($user); $this->pdm->saveToDb($user);
\Container::$request->setUser($user); \Container::$dbConnection->commit();
$this->deleteRedirectUrl(); $this->request->setUser($user);
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
} }
public function cancel() public function cancel()
{ {
if (\Container::$request->user() !== null) { if ($this->request->user() !== null) {
$this->deleteRedirectUrl(); return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
} }
$confirmation = $this->userConfirmationRepository->getByToken(substr(\Container::$request->query('token'), 0, 32)); $confirmation = $this->userConfirmationRepository->getByToken(substr($this->request->query('token'), 0, 32));
if ($confirmation === null) { if ($confirmation === null) {
return new HtmlContent('login/cancel', ['success' => false]); return new HtmlContent('login/cancel', ['success' => false]);
} }
\Container::$persistentDataManager->deleteFromDb($confirmation); \Container::$dbConnection->startTransaction();
$this->pdm->deleteFromDb($confirmation);
$user = $this->userRepository->getById($confirmation->getUserId()); $user = $this->userRepository->getById($confirmation->getUserId());
foreach ($this->userPlayedPlaceRepository->getAllByUser($user) as $userPlayedPlace) { $this->pdm->deleteFromDb($user);
\Container::$persistentDataManager->deleteFromDb($userPlayedPlace);
}
\Container::$persistentDataManager->deleteFromDb($user); \Container::$dbConnection->commit();
return new HtmlContent('login/cancel', ['success' => true]); return new HtmlContent('login/cancel', ['success' => true]);
} }
public function requestPasswordReset(): IContent public function requestPasswordReset(): IContent
{ {
if (\Container::$request->user() !== null) { if ($this->request->user() !== null) {
$this->deleteRedirectUrl();
return new JsonContent([ return new JsonContent([
'redirect' => [ 'redirect' => [
'target' => $this->redirectUrl 'target' => '/' . \Container::$routeCollection->getRoute('home')->generateLink()
] ]
]); ]);
} }
if (!empty($_ENV['RECAPTCHA_SITEKEY'])) { $user = $this->userRepository->getByEmail($this->request->post('email'));
if (!\Container::$request->post('g-recaptcha-response')) {
return new JsonContent(['error' => ['errorText' => 'Please check "I\'m not a robot" in the reCAPTCHA box!']]);
}
$captchaValidator = new CaptchaValidator();
$captchaResponse = $captchaValidator->validate(\Container::$request->post('g-recaptcha-response'));
if (!$captchaResponse['success']) {
return new JsonContent(['error' => ['errorText' => 'reCAPTCHA challenge failed. Please try again!']]);
}
}
$user = $this->userRepository->getByEmail(\Container::$request->post('email'));
if ($user === null) { if ($user === null) {
return new JsonContent([ return new JsonContent([
@@ -521,11 +486,15 @@ class LoginController
$passwordResetter->setToken($token); $passwordResetter->setToken($token);
$passwordResetter->setExpiresDate($expires); $passwordResetter->setExpiresDate($expires);
\Container::$dbConnection->startTransaction();
if ($existingResetter !== null) { if ($existingResetter !== null) {
\Container::$persistentDataManager->deleteFromDb($existingResetter); $this->pdm->deleteFromDb($existingResetter);
} }
\Container::$persistentDataManager->saveToDb($passwordResetter); $this->pdm->saveToDb($passwordResetter);
\Container::$dbConnection->commit();
$this->sendPasswordResetEmail($user->getEmail(), $token, $expires); $this->sendPasswordResetEmail($user->getEmail(), $token, $expires);
@@ -535,27 +504,26 @@ class LoginController
public function resetPassword(): IContent public function resetPassword(): IContent
{ {
if (\Container::$request->user() !== null) { if ($this->request->user() !== null) {
$this->deleteRedirectUrl();
return new JsonContent([ return new JsonContent([
'redirect' => [ 'redirect' => [
'target' => $this->redirectUrl 'target' => '/' . \Container::$routeCollection->getRoute('home')->generateLink()
] ]
]); ]);
} }
$token = \Container::$request->query('token'); $token = $this->request->query('token');
$resetter = $this->userPasswordResetterRepository->getByToken($token); $resetter = $this->userPasswordResetterRepository->getByToken($token);
if ($resetter === null || $resetter->getExpiresDate() < new DateTime()) { if ($resetter === null || $resetter->getExpiresDate() < new DateTime()) {
return new JsonContent([ return new JsonContent([
'redirect' => [ 'redirect' => [
'target' => \Container::$routeCollection->getRoute('password-reset')->generateLink(['token' => $token]) 'target' => '/' . \Container::$routeCollection->getRoute('password-reset')->generateLink(['token' => $token])
] ]
]); ]);
} }
if (strlen(\Container::$request->post('password')) < 6) { if (strlen($this->request->post('password')) < 6) {
return new JsonContent([ return new JsonContent([
'error' => [ 'error' => [
'errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!' 'errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!'
@@ -563,20 +531,23 @@ class LoginController
]); ]);
} }
if (\Container::$request->post('password') !== \Container::$request->post('password_confirm')) { if ($this->request->post('password') !== $this->request->post('password_confirm')) {
return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]); return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]);
} }
\Container::$persistentDataManager->deleteFromDb($resetter); \Container::$dbConnection->startTransaction();
$this->pdm->deleteFromDb($resetter);
$user = $this->userRepository->getById($resetter->getUserId()); $user = $this->userRepository->getById($resetter->getUserId());
$user->setPlainPassword(\Container::$request->post('password')); $user->setPlainPassword($this->request->post('password'));
\Container::$persistentDataManager->saveToDb($user); $this->pdm->saveToDb($user);
\Container::$request->setUser($user); \Container::$dbConnection->commit();
$this->request->setUser($user);
$this->deleteRedirectUrl();
return new JsonContent(['success' => true]); return new JsonContent(['success' => true]);
} }
@@ -587,9 +558,9 @@ class LoginController
$mail->setSubject('Welcome to ' . $_ENV['APP_NAME'] . ' - Activate your account'); $mail->setSubject('Welcome to ' . $_ENV['APP_NAME'] . ' - Activate your account');
$mail->setBodyFromTemplate('signup', [ $mail->setBodyFromTemplate('signup', [
'EMAIL' => $email, 'EMAIL' => $email,
'ACTIVATE_LINK' => \Container::$request->getBase() . 'ACTIVATE_LINK' => $this->request->getBase() . '/' .
\Container::$routeCollection->getRoute('signup.activate')->generateLink(['token' => $token]), \Container::$routeCollection->getRoute('signup.activate')->generateLink(['token' => $token]),
'CANCEL_LINK' => \Container::$request->getBase() . 'CANCEL_LINK' => $this->request->getBase() . '/' .
\Container::$routeCollection->getRoute('signup.cancel')->generateLink(['token' => $token]), \Container::$routeCollection->getRoute('signup.cancel')->generateLink(['token' => $token]),
'ACTIVATABLE_UNTIL' => (clone $created)->add(new DateInterval('P1D'))->format('Y-m-d H:i T') 'ACTIVATABLE_UNTIL' => (clone $created)->add(new DateInterval('P1D'))->format('Y-m-d H:i T')
]); ]);
@@ -606,7 +577,7 @@ class LoginController
$confirmation->setLastSentDate(new DateTime()); $confirmation->setLastSentDate(new DateTime());
\Container::$persistentDataManager->saveToDb($confirmation); $this->pdm->saveToDb($confirmation);
$this->sendConfirmationEmail($user->getEmail(), $confirmation->getToken(), $user->getCreatedDate()); $this->sendConfirmationEmail($user->getEmail(), $confirmation->getToken(), $user->getCreatedDate());
@@ -631,15 +602,10 @@ class LoginController
$mail->setSubject($_ENV['APP_NAME'] . ' - Password reset'); $mail->setSubject($_ENV['APP_NAME'] . ' - Password reset');
$mail->setBodyFromTemplate('password-reset', [ $mail->setBodyFromTemplate('password-reset', [
'EMAIL' => $email, 'EMAIL' => $email,
'RESET_LINK' => \Container::$request->getBase() . 'RESET_LINK' => $this->request->getBase() . '/' .
\Container::$routeCollection->getRoute('password-reset')->generateLink(['token' => $token]), \Container::$routeCollection->getRoute('password-reset')->generateLink(['token' => $token]),
'EXPIRES' => $expires->format('Y-m-d H:i T') 'EXPIRES' => $expires->format('Y-m-d H:i T')
]); ]);
$mail->send(); $mail->send();
} }
private function deleteRedirectUrl(): void
{
\Container::$request->session()->delete('redirect_after_login');
}
} }
+37 -81
View File
@@ -1,68 +1,50 @@
<?php namespace MapGuesser\Controller; <?php namespace MapGuesser\Controller;
use DateTime; use DateTime;
use SokoWeb\Interfaces\Authentication\IUser; use MapGuesser\Interfaces\Authentication\IUser;
use SokoWeb\Interfaces\Authentication\IAuthenticationRequired; use MapGuesser\Interfaces\Authorization\ISecured;
use SokoWeb\Interfaces\Authorization\ISecured; use MapGuesser\Interfaces\Request\IRequest;
use SokoWeb\Interfaces\Response\IContent; use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\PersistentData\Model\Challenge;
use MapGuesser\PersistentData\Model\Map; use MapGuesser\PersistentData\Model\Map;
use MapGuesser\PersistentData\Model\Place; use MapGuesser\PersistentData\Model\Place;
use MapGuesser\PersistentData\Model\PlaceInChallenge; use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Repository\ChallengeRepository;
use MapGuesser\Repository\GuessRepository;
use MapGuesser\Repository\MapRepository; use MapGuesser\Repository\MapRepository;
use MapGuesser\Repository\PlaceInChallengeRepository;
use MapGuesser\Repository\PlaceRepository; use MapGuesser\Repository\PlaceRepository;
use MapGuesser\Repository\UserInChallengeRepository; use MapGuesser\Response\HtmlContent;
use MapGuesser\Repository\UserPlayedPlaceRepository; use MapGuesser\Response\JsonContent;
use SokoWeb\Response\HtmlContent;
use SokoWeb\Response\JsonContent;
use MapGuesser\Util\Geo\Bounds; use MapGuesser\Util\Geo\Bounds;
use MapGuesser\Util\Panorama\Pov; use MapGuesser\Util\Panorama\Pov;
class MapAdminController implements IAuthenticationRequired, ISecured class MapAdminController implements ISecured
{ {
private static string $unnamedMapName = '[unnamed map]'; private static string $unnamedMapName = '[unnamed map]';
private IRequest $request;
private PersistentDataManager $pdm;
private MapRepository $mapRepository; private MapRepository $mapRepository;
private PlaceRepository $placeRepository; private PlaceRepository $placeRepository;
private UserPlayedPlaceRepository $userPlayedPlaceRepository; public function __construct(IRequest $request)
private ChallengeRepository $challengeRepository;
private GuessRepository $guessRepository;
private PlaceInChallengeRepository $placeInChallengeRepository;
private UserInChallengeRepository $userInChallengeRepository;
public function __construct()
{ {
$this->request = $request;
$this->pdm = new PersistentDataManager();
$this->mapRepository = new MapRepository(); $this->mapRepository = new MapRepository();
$this->placeRepository = new PlaceRepository(); $this->placeRepository = new PlaceRepository();
$this->userPlayedPlaceRepository = new UserPlayedPlaceRepository();
$this->challengeRepository = new ChallengeRepository();
$this->guessRepository = new GuessRepository();
$this->placeInChallengeRepository = new PlaceInChallengeRepository();
$this->userInChallengeRepository = new UserInChallengeRepository();
}
public function isAuthenticationRequired(): bool
{
return true;
} }
public function authorize(): bool public function authorize(): bool
{ {
return \Container::$request->user()->hasPermission(IUser::PERMISSION_ADMIN); $user = $this->request->user();
return $user !== null && $user->hasPermission(IUser::PERMISSION_ADMIN);
} }
public function getMapEditor(): IContent public function getMapEditor(): IContent
{ {
$mapId = (int) \Container::$request->query('mapId'); $mapId = (int) $this->request->query('mapId');
if ($mapId) { if ($mapId) {
$map = $this->mapRepository->getById($mapId); $map = $this->mapRepository->getById($mapId);
@@ -77,7 +59,6 @@ class MapAdminController implements IAuthenticationRequired, ISecured
'mapId' => $mapId, 'mapId' => $mapId,
'mapName' => $map->getName(), 'mapName' => $map->getName(),
'mapDescription' => str_replace('<br>', "\n", $map->getDescription()), 'mapDescription' => str_replace('<br>', "\n", $map->getDescription()),
'mapUnlisted' => $map->getUnlisted(),
'bounds' => $map->getBounds()->toArray(), 'bounds' => $map->getBounds()->toArray(),
'places' => &$places 'places' => &$places
]); ]);
@@ -85,7 +66,7 @@ class MapAdminController implements IAuthenticationRequired, ISecured
public function getPlace(): IContent public function getPlace(): IContent
{ {
$placeId = (int) \Container::$request->query('placeId'); $placeId = (int) $this->request->query('placeId');
$place = $this->placeRepository->getById($placeId); $place = $this->placeRepository->getById($placeId);
@@ -94,14 +75,16 @@ class MapAdminController implements IAuthenticationRequired, ISecured
public function saveMap(): IContent public function saveMap(): IContent
{ {
$mapId = (int) \Container::$request->query('mapId'); $mapId = (int) $this->request->query('mapId');
\Container::$dbConnection->startTransaction();
if ($mapId) { if ($mapId) {
$map = $this->mapRepository->getById($mapId); $map = $this->mapRepository->getById($mapId);
} else { } else {
$map = new Map(); $map = new Map();
$map->setName(self::$unnamedMapName); $map->setName(self::$unnamedMapName);
\Container::$persistentDataManager->saveToDb($map); $this->pdm->saveToDb($map);
} }
if (isset($_POST['added'])) { if (isset($_POST['added'])) {
@@ -123,7 +106,7 @@ class MapAdminController implements IAuthenticationRequired, ISecured
$place->setPanoIdCachedTimestampDate(new DateTime('-1 day')); $place->setPanoIdCachedTimestampDate(new DateTime('-1 day'));
} }
\Container::$persistentDataManager->saveToDb($place); $this->pdm->saveToDb($place);
$addedIds[] = ['tempId' => $placeRaw['id'], 'id' => $place->getId()]; $addedIds[] = ['tempId' => $placeRaw['id'], 'id' => $place->getId()];
} }
@@ -145,7 +128,7 @@ class MapAdminController implements IAuthenticationRequired, ISecured
)); ));
$place->setPanoIdCachedTimestampDate(new DateTime('-1 day')); $place->setPanoIdCachedTimestampDate(new DateTime('-1 day'));
\Container::$persistentDataManager->saveToDb($place); $this->pdm->saveToDb($place);
} }
} }
@@ -155,7 +138,7 @@ class MapAdminController implements IAuthenticationRequired, ISecured
$place = $this->placeRepository->getById((int) $placeRaw['id']); $place = $this->placeRepository->getById((int) $placeRaw['id']);
$this->deletePlace($place); $this->pdm->deleteFromDb($place);
} }
} }
@@ -170,65 +153,38 @@ class MapAdminController implements IAuthenticationRequired, ISecured
if (isset($_POST['description'])) { if (isset($_POST['description'])) {
$map->setDescription(str_replace(["\n", "\r\n"], '<br>', $_POST['description'])); $map->setDescription(str_replace(["\n", "\r\n"], '<br>', $_POST['description']));
} }
if (isset($_POST['unlisted'])) {
$map->setUnlisted((bool)$_POST['unlisted']);
}
\Container::$persistentDataManager->saveToDb($map); $this->pdm->saveToDb($map);
\Container::$dbConnection->commit();
return new JsonContent(['mapId' => $map->getId(), 'added' => $addedIds]); return new JsonContent(['mapId' => $map->getId(), 'added' => $addedIds]);
} }
public function deleteMap(): IContent public function deleteMap(): IContent
{ {
$mapId = (int) \Container::$request->query('mapId'); $mapId = (int) $this->request->query('mapId');
$map = $this->mapRepository->getById($mapId); $map = $this->mapRepository->getById($mapId);
\Container::$dbConnection->startTransaction();
$this->deletePlaces($map); $this->deletePlaces($map);
\Container::$persistentDataManager->deleteFromDb($map); $this->pdm->deleteFromDb($map);
\Container::$dbConnection->commit();
return new JsonContent(['success' => true]); return new JsonContent(['success' => true]);
} }
private function deletePlace(Place $place): void
{
foreach ($this->userPlayedPlaceRepository->getAllByPlace($place) as $userPlayedPlace) {
\Container::$persistentDataManager->deleteFromDb($userPlayedPlace);
}
foreach ($this->challengeRepository->getAllByPlace($place) as $challenge) {
$this->deleteChallenge($challenge);
}
\Container::$persistentDataManager->deleteFromDb($place);
}
private function deletePlaces(Map $map): void private function deletePlaces(Map $map): void
{ {
foreach ($this->placeRepository->getAllForMap($map) as $place) { foreach ($this->placeRepository->getAllForMap($map) as $place) {
$this->deletePlace($place); $this->pdm->deleteFromDb($place);
} }
} }
private function deleteChallenge(Challenge $challenge): void
{
foreach ($this->userInChallengeRepository->getAllByChallenge($challenge) as $userInChallenge) {
\Container::$persistentDataManager->deleteFromDb($userInChallenge);
}
foreach ($this->guessRepository->getAllInChallenge($challenge, ['place_in_challange']) as $guess) {
\Container::$persistentDataManager->deleteFromDb($guess);
}
foreach ($this->placeInChallengeRepository->getAllByChallenge($challenge) as $placeInChallenge) {
\Container::$persistentDataManager->deleteFromDb($placeInChallenge);
}
\Container::$persistentDataManager->deleteFromDb($challenge);
}
private function calculateMapBounds(Map $map): Bounds private function calculateMapBounds(Map $map): Bounds
{ {
$bounds = new Bounds(); $bounds = new Bounds();
+16 -15
View File
@@ -1,14 +1,22 @@
<?php namespace MapGuesser\Controller; <?php namespace MapGuesser\Controller;
use SokoWeb\Database\Query\Select; use MapGuesser\Database\Query\Select;
use SokoWeb\Database\RawExpression; use MapGuesser\Database\RawExpression;
use SokoWeb\Interfaces\Authentication\IUser; use MapGuesser\Interfaces\Authentication\IUser;
use SokoWeb\Interfaces\Database\IResultSet; use MapGuesser\Interfaces\Database\IResultSet;
use SokoWeb\Interfaces\Response\IContent; use MapGuesser\Interfaces\Request\IRequest;
use SokoWeb\Response\HtmlContent; use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\Response\HtmlContent;
class MapsController class MapsController
{ {
private IRequest $request;
public function __construct(IRequest $request)
{
$this->request = $request;
}
public function getMaps(): IContent public function getMaps(): IContent
{ {
//TODO: from repository - count should be added //TODO: from repository - count should be added
@@ -22,19 +30,12 @@ class MapsController
['maps', 'bound_north_lat'], ['maps', 'bound_north_lat'],
['maps', 'bound_east_lng'], ['maps', 'bound_east_lng'],
['maps', 'area'], ['maps', 'area'],
['maps', 'unlisted'],
new RawExpression('COUNT(places.id) AS num_places') new RawExpression('COUNT(places.id) AS num_places')
]); ]);
$select->leftJoin('places', ['places', 'map_id'], '=', ['maps', 'id']); $select->leftJoin('places', ['places', 'map_id'], '=', ['maps', 'id']);
$select->groupBy(['maps', 'id']); $select->groupBy(['maps', 'id']);
$select->orderBy('name'); $select->orderBy('name');
$user = \Container::$request->user();
$isAdmin = $user !== null && $user->hasPermission(IUser::PERMISSION_ADMIN);
if (!$isAdmin) {
$select->where(['maps', 'unlisted'], '=', false);
}
$result = $select->execute(); $result = $select->execute();
$maps = []; $maps = [];
@@ -44,10 +45,10 @@ class MapsController
$maps[] = $map; $maps[] = $map;
} }
$user = $this->request->user();
return new HtmlContent('maps', [ return new HtmlContent('maps', [
'maps' => $maps, 'maps' => $maps,
'isLoggedIn' => $user !== null, 'isAdmin' => $user !== null && $user->hasPermission(IUser::PERMISSION_ADMIN)
'isAdmin' => $isAdmin
]); ]);
} }
+55 -65
View File
@@ -1,46 +1,44 @@
<?php namespace MapGuesser\Controller; <?php namespace MapGuesser\Controller;
use DateTime; use DateTime;
use SokoWeb\Http\Request; use MapGuesser\Http\Request;
use SokoWeb\Interfaces\Authentication\IAuthenticationRequired; use MapGuesser\Interfaces\Authorization\ISecured;
use SokoWeb\Interfaces\Response\IContent; use MapGuesser\Interfaces\Request\IRequest;
use SokoWeb\Interfaces\Response\IRedirect; use MapGuesser\Interfaces\Response\IContent;
use SokoWeb\OAuth\GoogleOAuth; use MapGuesser\Interfaces\Response\IRedirect;
use MapGuesser\OAuth\GoogleOAuth;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\PersistentData\Model\User; use MapGuesser\PersistentData\Model\User;
use MapGuesser\Repository\GuessRepository;
use MapGuesser\Repository\UserConfirmationRepository; use MapGuesser\Repository\UserConfirmationRepository;
use MapGuesser\Repository\UserInChallengeRepository;
use MapGuesser\Repository\UserPasswordResetterRepository; use MapGuesser\Repository\UserPasswordResetterRepository;
use MapGuesser\Repository\UserPlayedPlaceRepository; use MapGuesser\Response\HtmlContent;
use SokoWeb\Response\HtmlContent; use MapGuesser\Response\JsonContent;
use SokoWeb\Response\JsonContent; use MapGuesser\Response\Redirect;
use SokoWeb\Response\Redirect; use MapGuesser\Util\JwtParser;
use SokoWeb\Util\JwtParser;
class UserController implements IAuthenticationRequired class UserController implements ISecured
{ {
private IRequest $request;
private PersistentDataManager $pdm;
private UserConfirmationRepository $userConfirmationRepository; private UserConfirmationRepository $userConfirmationRepository;
private UserPasswordResetterRepository $userPasswordResetterRepository; private UserPasswordResetterRepository $userPasswordResetterRepository;
private UserPlayedPlaceRepository $userPlayedPlaceRepository; public function __construct(IRequest $request)
private UserInChallengeRepository $userInChallengeRepository;
private GuessRepository $guessRepository;
public function __construct()
{ {
$this->request = $request;
$this->pdm = new PersistentDataManager();
$this->userConfirmationRepository = new UserConfirmationRepository(); $this->userConfirmationRepository = new UserConfirmationRepository();
$this->userPasswordResetterRepository = new UserPasswordResetterRepository(); $this->userPasswordResetterRepository = new UserPasswordResetterRepository();
$this->userPlayedPlaceRepository = new UserPlayedPlaceRepository();
$this->userInChallengeRepository = new UserInChallengeRepository();
$this->guessRepository = new GuessRepository();
} }
public function isAuthenticationRequired(): bool public function authorize(): bool
{ {
return true; $user = $this->request->user();
return $user !== null;
} }
public function getAccount(): IContent public function getAccount(): IContent
@@ -48,7 +46,7 @@ class UserController implements IAuthenticationRequired
/** /**
* @var User $user * @var User $user
*/ */
$user = \Container::$request->user(); $user = $this->request->user();
return new HtmlContent('account/account', ['user' => $user->toArray()]); return new HtmlContent('account/account', ['user' => $user->toArray()]);
} }
@@ -58,19 +56,19 @@ class UserController implements IAuthenticationRequired
/** /**
* @var User $user * @var User $user
*/ */
$user = \Container::$request->user(); $user = $this->request->user();
$state = bin2hex(random_bytes(16)); $state = bin2hex(random_bytes(16));
$nonce = bin2hex(random_bytes(16)); $nonce = bin2hex(random_bytes(16));
\Container::$request->session()->set('oauth_state', $state); $this->request->session()->set('oauth_state', $state);
\Container::$request->session()->set('oauth_nonce', $nonce); $this->request->session()->set('oauth_nonce', $nonce);
$oAuth = new GoogleOAuth(new Request()); $oAuth = new GoogleOAuth(new Request());
$url = $oAuth->getDialogUrl( $url = $oAuth->getDialogUrl(
$state, $state,
\Container::$request->getBase() . \Container::$routeCollection->getRoute('account.googleAuthenticate-action')->generateLink(), $this->request->getBase() . '/' . \Container::$routeCollection->getRoute('account.googleAuthenticate-action')->generateLink(),
$nonce, $nonce,
$user->getEmail() $user->getEmail()
); );
@@ -83,16 +81,16 @@ class UserController implements IAuthenticationRequired
/** /**
* @var User $user * @var User $user
*/ */
$user = \Container::$request->user(); $user = $this->request->user();
if (\Container::$request->query('state') !== \Container::$request->session()->get('oauth_state')) { if ($this->request->query('state') !== $this->request->session()->get('oauth_state')) {
return new HtmlContent('account/google_authenticate', ['success' => false]); return new HtmlContent('account/google_authenticate', ['success' => false]);
} }
$oAuth = new GoogleOAuth(new Request()); $oAuth = new GoogleOAuth(new Request());
$tokenData = $oAuth->getToken( $tokenData = $oAuth->getToken(
\Container::$request->query('code'), $this->request->query('code'),
\Container::$request->getBase() . \Container::$routeCollection->getRoute('account.googleAuthenticate-action')->generateLink() $this->request->getBase() . '/' . \Container::$routeCollection->getRoute('account.googleAuthenticate-action')->generateLink()
); );
if (!isset($tokenData['id_token'])) { if (!isset($tokenData['id_token'])) {
@@ -102,7 +100,7 @@ class UserController implements IAuthenticationRequired
$jwtParser = new JwtParser($tokenData['id_token']); $jwtParser = new JwtParser($tokenData['id_token']);
$idToken = $jwtParser->getPayload(); $idToken = $jwtParser->getPayload();
if ($idToken['nonce'] !== \Container::$request->session()->get('oauth_nonce')) { if ($idToken['nonce'] !== $this->request->session()->get('oauth_nonce')) {
return new HtmlContent('account/google_authenticate', ['success' => false]); return new HtmlContent('account/google_authenticate', ['success' => false]);
} }
@@ -114,7 +112,7 @@ class UserController implements IAuthenticationRequired
} }
$authenticatedWithGoogleUntil = new DateTime('+45 seconds'); $authenticatedWithGoogleUntil = new DateTime('+45 seconds');
\Container::$request->session()->set('authenticated_with_google_until', $authenticatedWithGoogleUntil); $this->request->session()->set('authenticated_with_google_until', $authenticatedWithGoogleUntil);
return new HtmlContent('account/google_authenticate', [ return new HtmlContent('account/google_authenticate', [
'success' => true, 'success' => true,
@@ -127,7 +125,7 @@ class UserController implements IAuthenticationRequired
/** /**
* @var User $user * @var User $user
*/ */
$user = \Container::$request->user(); $user = $this->request->user();
return new HtmlContent('account/delete', ['user' => $user->toArray()]); return new HtmlContent('account/delete', ['user' => $user->toArray()]);
} }
@@ -137,19 +135,19 @@ class UserController implements IAuthenticationRequired
/** /**
* @var User $user * @var User $user
*/ */
$user = \Container::$request->user(); $user = $this->request->user();
if (!$this->confirmUserIdentity( if (!$this->confirmUserIdentity(
$user, $user,
\Container::$request->session()->get('authenticated_with_google_until'), $this->request->session()->get('authenticated_with_google_until'),
\Container::$request->post('password'), $this->request->post('password'),
$error $error
)) { )) {
return new JsonContent(['error' => ['errorText' => $error]]); return new JsonContent(['error' => ['errorText' => $error]]);
} }
if (strlen(\Container::$request->post('password_new')) > 0) { if (strlen($this->request->post('password_new')) > 0) {
if (strlen(\Container::$request->post('password_new')) < 6) { if (strlen($this->request->post('password_new')) < 6) {
return new JsonContent([ return new JsonContent([
'error' => [ 'error' => [
'errorText' => 'The given new password is too short. Please choose a password that is at least 6 characters long!' 'errorText' => 'The given new password is too short. Please choose a password that is at least 6 characters long!'
@@ -157,7 +155,7 @@ class UserController implements IAuthenticationRequired
]); ]);
} }
if (\Container::$request->post('password_new') !== \Container::$request->post('password_new_confirm')) { if ($this->request->post('password_new') !== $this->request->post('password_new_confirm')) {
return new JsonContent([ return new JsonContent([
'error' => [ 'error' => [
'errorText' => 'The given new passwords do not match.' 'errorText' => 'The given new passwords do not match.'
@@ -165,12 +163,12 @@ class UserController implements IAuthenticationRequired
]); ]);
} }
$user->setPlainPassword(\Container::$request->post('password_new')); $user->setPlainPassword($this->request->post('password_new'));
} }
\Container::$persistentDataManager->saveToDb($user); $this->pdm->saveToDb($user);
\Container::$request->session()->delete('authenticated_with_google_until'); $this->request->session()->delete('authenticated_with_google_until');
return new JsonContent(['success' => true]); return new JsonContent(['success' => true]);
} }
@@ -180,47 +178,39 @@ class UserController implements IAuthenticationRequired
/** /**
* @var User $user * @var User $user
*/ */
$user = \Container::$request->user(); $user = $this->request->user();
if (!$this->confirmUserIdentity( if (!$this->confirmUserIdentity(
$user, $user,
\Container::$request->session()->get('authenticated_with_google_until'), $this->request->session()->get('authenticated_with_google_until'),
\Container::$request->post('password'), $this->request->post('password'),
$error $error
)) { )) {
return new JsonContent(['error' => ['errorText' => $error]]); return new JsonContent(['error' => ['errorText' => $error]]);
} }
\Container::$dbConnection->startTransaction();
$userConfirmation = $this->userConfirmationRepository->getByUser($user); $userConfirmation = $this->userConfirmationRepository->getByUser($user);
if ($userConfirmation !== null) { if ($userConfirmation !== null) {
\Container::$persistentDataManager->deleteFromDb($userConfirmation); $this->pdm->deleteFromDb($userConfirmation);
} }
$userPasswordResetter = $this->userPasswordResetterRepository->getByUser($user); $userPasswordResetter = $this->userPasswordResetterRepository->getByUser($user);
if ($userPasswordResetter !== null) { if ($userPasswordResetter !== null) {
\Container::$persistentDataManager->deleteFromDb($userPasswordResetter); $this->pdm->deleteFromDb($userPasswordResetter);
} }
foreach ($this->userPlayedPlaceRepository->getAllByUser($user) as $userPlayedPlace) { $this->pdm->deleteFromDb($user);
\Container::$persistentDataManager->deleteFromDb($userPlayedPlace);
}
foreach ($this->userInChallengeRepository->getAllByUser($user) as $userInChallenge) { \Container::$dbConnection->commit();
\Container::$persistentDataManager->deleteFromDb($userInChallenge);
}
foreach ($this->guessRepository->getAllByUser($user) as $guess) { $this->request->session()->delete('authenticated_with_google_until');
\Container::$persistentDataManager->deleteFromDb($guess);
}
\Container::$persistentDataManager->deleteFromDb($user);
\Container::$request->session()->delete('authenticated_with_google_until');
return new JsonContent(['success' => true]); return new JsonContent(['success' => true]);
} }
private function confirmUserIdentity(User $user, ?DateTime $authenticatedWithGoogleUntil, ?string $password, ?string &$error): bool private function confirmUserIdentity(User $user, ?DateTime $authenticatedWithGoogleUntil, ?string $password, string &$error): bool
{ {
if ($authenticatedWithGoogleUntil !== null && $authenticatedWithGoogleUntil > new DateTime()) { if ($authenticatedWithGoogleUntil !== null && $authenticatedWithGoogleUntil > new DateTime()) {
return true; return true;
+114
View File
@@ -0,0 +1,114 @@
<?php namespace MapGuesser\Database\Mysql;
use MapGuesser\Interfaces\Database\IConnection;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Interfaces\Database\IStatement;
use mysqli;
class Connection implements IConnection
{
private mysqli $connection;
public function __construct(string $host, string $user, string $password, string $db, int $port = -1, string $socket = null)
{
if ($port < 0) {
$port = (int) ini_get('mysqli.default_port');
}
if ($socket === null) {
$socket = (string) ini_get('mysqli.default_socket');
}
$this->connection = new mysqli($host, $user, $password, $db, $port, $socket);
if ($this->connection->connect_error) {
throw new \Exception('Connection failed: ' . $this->connection->connect_error);
}
if (!$this->connection->set_charset('utf8mb4')) {
throw new \Exception($this->connection->error);
}
}
public function __destruct()
{
$this->connection->close();
}
public function startTransaction(): void
{
if (!$this->connection->autocommit(false)) {
throw new \Exception($this->connection->error);
}
}
public function commit(): void
{
if (!$this->connection->commit() || !$this->connection->autocommit(true)) {
throw new \Exception($this->connection->error);
}
}
public function rollback(): void
{
if (!$this->connection->rollback() || !$this->connection->autocommit(true)) {
throw new \Exception($this->connection->error);
}
}
public function query(string $query): ?IResultSet
{
if (!($result = $this->connection->query($query))) {
throw new \Exception($this->connection->error . '. Query: ' . $query);
}
if ($result !== true) {
return new ResultSet($result);
}
return null;
}
public function multiQuery(string $query): array
{
if (!$this->connection->multi_query($query)) {
throw new \Exception($this->connection->error . '. Query: ' . $query);
}
$ret = [];
do {
if ($result = $this->connection->store_result()) {
$ret[] = new ResultSet($result);
} else {
$ret[] = null;
}
$this->connection->more_results();
} while ($this->connection->next_result());
if ($this->connection->error) {
throw new \Exception($this->connection->error . '. Query: ' . $query);
}
return $ret;
}
public function prepare(string $query): IStatement
{
if (!($stmt = $this->connection->prepare($query))) {
throw new \Exception($this->connection->error . '. Query: ' . $query);
}
return new Statement($stmt);
}
public function lastId(): int
{
return $this->connection->insert_id;
}
public function getAffectedRows(): int
{
return $this->connection->affected_rows;
}
}
+62
View File
@@ -0,0 +1,62 @@
<?php namespace MapGuesser\Database\Mysql;
use MapGuesser\Interfaces\Database\IResultSet;
use mysqli_result;
class ResultSet implements IResultSet
{
private mysqli_result $result;
public function __construct(mysqli_result $result)
{
$this->result = $result;
}
public function fetch(int $type = IResultSet::FETCH_ASSOC): ?array
{
return $this->result->fetch_array($this->convertFetchType($type));
}
public function fetchAll(int $type = IResultSet::FETCH_ASSOC): array
{
return $this->result->fetch_all($this->convertFetchType($type));
}
public function fetchOneColumn(string $valueName, string $keyName = null): array
{
$array = [];
while ($r = $this->fetch(IResultSet::FETCH_ASSOC)) {
if (isset($keyName)) {
$array[$r[$keyName]] = $r[$valueName];
} else {
$array[] = $r[$valueName];
}
}
return $array;
}
private function convertFetchType(int $type): int
{
switch ($type) {
case IResultSet::FETCH_ASSOC:
$internal_type = MYSQLI_ASSOC;
break;
case IResultSet::FETCH_BOTH:
$internal_type = MYSQLI_BOTH;
break;
case IResultSet::FETCH_NUM:
$internal_type = MYSQLI_NUM;
break;
default:
$internal_type = MYSQLI_BOTH;
break;
}
return $internal_type;
}
}
+79
View File
@@ -0,0 +1,79 @@
<?php namespace MapGuesser\Database\Mysql;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Interfaces\Database\IStatement;
use mysqli_stmt;
class Statement implements IStatement
{
private mysqli_stmt $stmt;
public function __construct(mysqli_stmt $stmt)
{
$this->stmt = $stmt;
}
public function __destruct()
{
$this->stmt->close();
}
public function execute(array $params = []): ?IResultSet
{
if ($params) {
$ref_params = [''];
foreach ($params as &$param) {
$type = gettype($param);
switch ($type) {
case 'integer':
case 'double':
case 'string':
$t = $type[0];
break;
case 'NULL':
$t = 's';
break;
case 'boolean':
$param = (string) (int) $param;
$t = 's';
break;
case 'array':
$param = json_encode($param);
$t = 's';
break;
}
if (!isset($t)) {
throw new \Exception('Data type ' . $type . ' not supported!');
}
$ref_params[] = &$param;
$ref_params[0] .= $t;
}
if (!call_user_func_array([$this->stmt, 'bind_param'], $ref_params)) {
throw new \Exception($this->stmt->error);
}
}
if (!$this->stmt->execute()) {
throw new \Exception($this->stmt->error);
}
if ($result_set = $this->stmt->get_result()) {
return new ResultSet($result_set);
}
return null;
}
public function getAffectedRows(): int
{
return $this->stmt->affected_rows;
}
}
+140
View File
@@ -0,0 +1,140 @@
<?php namespace MapGuesser\Database\Query;
use MapGuesser\Interfaces\Database\IConnection;
use MapGuesser\Database\Utils;
class Modify
{
private IConnection $connection;
private string $table;
private string $idName = 'id';
private array $attributes = [];
private ?string $externalId = null;
private bool $autoIncrement = true;
public function __construct(IConnection $connection, string $table)
{
$this->connection = $connection;
$this->table = $table;
}
public function setIdName(string $idName): Modify
{
$this->idName = $idName;
return $this;
}
public function setExternalId($id): Modify
{
$this->externalId = $id;
return $this;
}
public function setAutoIncrement(bool $autoIncrement = true): Modify
{
$this->autoIncrement = $autoIncrement;
return $this;
}
public function fill(array $attributes): Modify
{
$this->attributes = array_merge($this->attributes, $attributes);
return $this;
}
public function set(string $name, $value): Modify
{
$this->attributes[$name] = $value;
return $this;
}
public function setId($id): Modify
{
$this->attributes[$this->idName] = $id;
return $this;
}
public function getId()
{
return $this->attributes[$this->idName];
}
public function save(): void
{
if (isset($this->attributes[$this->idName])) {
$this->update();
} else {
$this->insert();
}
}
public function delete(): void
{
if (!isset($this->attributes[$this->idName])) {
throw new \Exception('No primary key specified!');
}
$query = 'DELETE FROM ' . Utils::backtick($this->table) . ' WHERE ' . Utils::backtick($this->idName) . '=?';
$stmt = $this->connection->prepare($query);
$stmt->execute([$this->idName => $this->attributes[$this->idName]]);
}
private function insert(): void
{
if ($this->externalId !== null) {
$this->attributes[$this->idName] = $this->externalId;
} elseif (!$this->autoIncrement) {
$this->attributes[$this->idName] = $this->generateKey();
}
$set = $this->generateColumnsWithBinding(array_keys($this->attributes));
$query = 'INSERT INTO ' . Utils::backtick($this->table) . ' SET ' . $set;
$stmt = $this->connection->prepare($query);
$stmt->execute($this->attributes);
if ($this->autoIncrement) {
$this->attributes[$this->idName] = $this->connection->lastId();
}
}
private function update(): void
{
$attributes = $this->attributes;
unset($attributes[$this->idName]);
$set = $this->generateColumnsWithBinding(array_keys($attributes));
$query = 'UPDATE ' . Utils::backtick($this->table) . ' SET ' . $set . ' WHERE ' . Utils::backtick($this->idName) . '=?';
$stmt = $this->connection->prepare($query);
$stmt->execute(array_merge($attributes, [$this->idName => $this->attributes[$this->idName]]));
}
public static function generateColumnsWithBinding(array $columns): string
{
array_walk($columns, function(&$value, $key) {
$value = Utils::backtick($value) . '=?';
});
return implode(',', $columns);
}
private function generateKey(): string
{
return substr(hash('sha256', serialize($this->attributes) . random_bytes(5) . microtime()), 0, 7);
}
}
+412
View File
@@ -0,0 +1,412 @@
<?php namespace MapGuesser\Database\Query;
use Closure;
use MapGuesser\Interfaces\Database\IConnection;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Database\RawExpression;
use MapGuesser\Database\Utils;
class Select
{
const CONDITION_WHERE = 0;
const CONDITION_HAVING = 1;
private IConnection $connection;
private string $table;
private string $idName = 'id';
private array $tableAliases = [];
private array $joins = [];
private array $columns = [];
private array $conditions = [self::CONDITION_WHERE => [], self::CONDITION_HAVING => []];
private array $groups = [];
private array $orders = [];
private ?array $limit;
public function __construct(IConnection $connection, ?string $table = null)
{
$this->connection = $connection;
if ($table !== null) {
$this->table = $table;
}
}
public function setIdName(string $idName): Select
{
$this->idName = $idName;
return $this;
}
public function setTableAliases(array $tableAliases): Select
{
$this->tableAliases = array_merge($this->tableAliases, $tableAliases);
return $this;
}
public function from(string $table): Select
{
$this->table = $table;
return $this;
}
public function columns(array $columns): Select
{
$this->columns = array_merge($this->columns, $columns);
return $this;
}
public function innerJoin($table, $column1, string $relation, $column2): Select
{
$this->addJoin('INNER', $table, $column1, $relation, $column2);
return $this;
}
public function leftJoin($table, $column1, string $relation, $column2): Select
{
$this->addJoin('LEFT', $table, $column1, $relation, $column2);
return $this;
}
public function whereId($value): Select
{
$this->addWhereCondition('AND', $this->idName, '=', $value);
return $this;
}
public function where($column, string $relation = null, $value = null): Select
{
$this->addWhereCondition('AND', $column, $relation, $value);
return $this;
}
public function orWhere($column, string $relation = null, $value = null): Select
{
$this->addWhereCondition('OR', $column, $relation, $value);
return $this;
}
public function having($column, string $relation = null, $value = null): Select
{
$this->addHavingCondition('AND', $column, $relation, $value);
return $this;
}
public function orHaving($column, string $relation = null, $value = null): Select
{
$this->addHavingCondition('OR', $column, $relation, $value);
return $this;
}
public function groupBy($column): Select
{
$this->groups[] = $column;
return $this;
}
public function orderBy($column, string $type = 'ASC'): Select
{
$this->orders[] = [$column, $type];
return $this;
}
public function limit(int $limit, int $offset = 0): Select
{
$this->limit = [$limit, $offset];
return $this;
}
public function resetLimit(): void
{
$this->limit = null;
}
public function paginate(int $page, int $itemsPerPage): Select
{
$this->limit($itemsPerPage, ($page - 1) * $itemsPerPage);
return $this;
}
public function execute(): IResultSet
{
list($query, $params) = $this->generateQuery();
return $this->connection->prepare($query)->execute($params);
}
public function count(): int
{
if (count($this->groups) > 0 || count($this->conditions[self::CONDITION_HAVING]) > 0) {
$orders = $this->orders;
$this->orders = [];
list($query, $params) = $this->generateQuery();
$result = $this->connection->prepare('SELECT COUNT(*) num_rows FROM (' . $query . ') x')
->execute($params)
->fetch(IResultSet::FETCH_NUM);
$this->orders = $orders;
return $result[0];
} else {
$columns = $this->columns;
$orders = $this->orders;
$this->columns = [new RawExpression('COUNT(*) num_rows')];
$this->orders = [];
list($query, $params) = $this->generateQuery();
$result = $this->connection->prepare($query)
->execute($params)
->fetch(IResultSet::FETCH_NUM);
$this->columns = $columns;
$this->orders = $orders;
return $result[0];
}
}
private function addJoin(string $type, $table, $column1, string $relation, $column2): void
{
$this->joins[] = [$type, $table, $column1, $relation, $column2];
}
private function addWhereCondition(string $logic, $column, string $relation, $value): void
{
$this->conditions[self::CONDITION_WHERE][] = [$logic, $column, $relation, $value];
}
private function addHavingCondition(string $logic, $column, string $relation, $value): void
{
$this->conditions[self::CONDITION_HAVING][] = [$logic, $column, $relation, $value];
}
private function generateQuery(): array
{
$queryString = 'SELECT ' . $this->generateColumns() . ' FROM ' . $this->generateTable($this->table, true);
if (count($this->joins) > 0) {
$queryString .= ' ' . $this->generateJoins();
}
if (count($this->conditions[self::CONDITION_WHERE]) > 0) {
list($wheres, $whereParams) = $this->generateConditions(self::CONDITION_WHERE);
$queryString .= ' WHERE ' . $wheres;
} else {
$whereParams = [];
}
if (count($this->groups) > 0) {
$queryString .= ' GROUP BY ' . $this->generateGroupBy();
}
if (count($this->conditions[self::CONDITION_HAVING]) > 0) {
list($havings, $havingParams) = $this->generateConditions(self::CONDITION_HAVING);
$queryString .= ' HAVING ' . $havings;
} else {
$havingParams = [];
}
if (count($this->orders) > 0) {
$queryString .= ' ORDER BY ' . $this->generateOrderBy();
}
if (isset($this->limit)) {
$queryString .= ' LIMIT ' . $this->limit[1] . ', ' . $this->limit[0];
}
return [$queryString, array_merge($whereParams, $havingParams)];
}
private function generateTable($table, bool $defineAlias = false): string
{
if ($table instanceof RawExpression) {
return (string) $table;
}
if (isset($this->tableAliases[$table])) {
return ($defineAlias ? Utils::backtick($this->tableAliases[$table]) . ' ' . Utils::backtick($table) : Utils::backtick($table));
}
return Utils::backtick($table);
}
private function generateColumn($column): string
{
if ($column instanceof RawExpression) {
return (string) $column;
}
if (is_array($column)) {
$out = '';
if ($column[0]) {
$out .= $this->generateTable($column[0]) . '.';
}
$out .= Utils::backtick($column[1]);
if (!empty($column[2])) {
$out .= ' ' . Utils::backtick($column[2]);
}
return $out;
} else {
return Utils::backtick($column);
}
}
private function generateColumns(): string
{
$columns = $this->columns;
array_walk($columns, function (&$value, $key) {
$value = $this->generateColumn($value);
});
return implode(',', $columns);
}
private function generateJoins(): string
{
$joins = $this->joins;
array_walk($joins, function (&$value, $key) {
$value = $value[0] . ' JOIN ' . $this->generateTable($value[1], true) . ' ON ' . $this->generateColumn($value[2]) . ' ' . $value[3] . ' ' . $this->generateColumn($value[4]);
});
return implode(' ', $joins);
}
private function generateConditions(int $type): array
{
$conditions = '';
$params = [];
foreach ($this->conditions[$type] as $condition) {
list($logic, $column, $relation, $value) = $condition;
if ($column instanceof Closure) {
list($conditionsStringFragment, $paramsFragment) = $this->generateComplexConditionFragment($type, $column);
} else {
list($conditionsStringFragment, $paramsFragment) = $this->generateConditionFragment($condition);
}
if ($conditions !== '') {
$conditions .= ' ' . $logic . ' ';
}
$conditions .= $conditionsStringFragment;
$params = array_merge($params, $paramsFragment);
}
return [$conditions, $params];
}
private function generateConditionFragment(array $condition): array
{
list($logic, $column, $relation, $value) = $condition;
if ($column instanceof RawExpression) {
return [(string) $column, []];
}
$conditionsString = $this->generateColumn($column) . ' ';
if ($value === null) {
return [$conditionsString . ($relation == '=' ? 'IS NULL' : 'IS NOT NULL'), []];
}
$conditionsString .= strtoupper($relation) . ' ';;
switch ($relation = strtolower($relation)) {
case 'between':
$params = [$value[0], $value[1]];
$conditionsString .= '? AND ?';
break;
case 'in':
case 'not in':
$params = $value;
if (count($value) > 0) {
$conditionsString .= '(' . implode(', ', array_fill(0, count($value), '?')) . ')';
} else {
$conditionsString = $relation == 'in' ? '0' : '1';
}
break;
default:
$params = [$value];
$conditionsString .= '?';
}
return [$conditionsString, $params];
}
private function generateComplexConditionFragment(int $type, Closure $conditionCallback): array
{
$instance = new self($this->connection, $this->table);
$instance->tableAliases = $this->tableAliases;
$conditionCallback($instance);
list($conditions, $params) = $instance->generateConditions($type);
return ['(' . $conditions . ')', $params];
}
private function generateGroupBy(): string
{
$groups = $this->groups;
array_walk($groups, function (&$value, $key) {
$value = $this->generateColumn($value);
});
return implode(',', $groups);
}
private function generateOrderBy(): string
{
$orders = $this->orders;
array_walk($orders, function (&$value, $key) {
$value = $this->generateColumn($value[0]) . ' ' . strtoupper($value[1]);
});
return implode(',', $orders);
}
}
+16
View File
@@ -0,0 +1,16 @@
<?php namespace MapGuesser\Database;
class RawExpression
{
private string $expression;
public function __construct(string $expression)
{
$this->expression = $expression;
}
public function __toString(): string
{
return $this->expression;
}
}
+8
View File
@@ -0,0 +1,8 @@
<?php namespace MapGuesser\Database;
class Utils {
public static function backtick(string $name): string
{
return '`' . $name . '`';
}
}
+102
View File
@@ -0,0 +1,102 @@
<?php namespace MapGuesser\Http;
use MapGuesser\Interfaces\Http\IRequest;
use MapGuesser\Interfaces\Http\IResponse;
class Request implements IRequest
{
private string $url;
private int $method;
private string $query = '';
private array $headers = [];
public function __construct(string $url = '', int $method = self::HTTP_GET)
{
$this->url = $url;
$this->method = $method;
}
public function setUrl(string $url): void
{
$this->url = $url;
}
public function setMethod(int $method): void
{
$this->method = $method;
}
public function setQuery($query): void
{
if (is_string($query)) {
$this->query = $query;
} else {
$this->query = http_build_query($query);
}
}
public function setHeaders(array $headers): void
{
$this->headers = array_merge($this->headers, $headers);
}
public function send(): IResponse
{
$ch = curl_init();
if ($this->method === self::HTTP_POST) {
$url = $this->url;
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $this->query);
} else {
$url = $this->url . '?' . $this->query;
}
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_TIMEOUT, 20);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($ch, CURLOPT_USERAGENT, 'MapGuesser cURL/1.0');
if (count($this->headers) > 0) {
curl_setopt($ch, CURLOPT_HTTPHEADER, $this->headers);
}
$responseHeaders = [];
curl_setopt(
$ch,
CURLOPT_HEADERFUNCTION,
function ($ch, $header) use (&$responseHeaders) {
$len = strlen($header);
$header = explode(':', $header, 2);
if (count($header) < 2) {
return $len;
}
$responseHeaders[strtolower(trim($header[0]))][] = trim($header[1]);
return $len;
}
);
$responseBody = curl_exec($ch);
if ($responseBody === false) {
$error = curl_error($ch);
curl_close($ch);
throw new \Exception($error);
}
curl_close($ch);
return new Response($responseBody, $responseHeaders);
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php namespace MapGuesser\Http;
use MapGuesser\Interfaces\Http\IResponse;
class Response implements IResponse
{
private string $body;
private array $headers;
public function __construct(string $body, array $headers)
{
$this->body = $body;
$this->headers = $headers;
}
public function getBody(): string
{
return $this->body;
}
public function getHeaders(): array
{
return $this->headers;
}
}
+16
View File
@@ -0,0 +1,16 @@
<?php namespace MapGuesser\Interfaces\Authentication;
interface IUser
{
const PERMISSION_NORMAL = 0;
const PERMISSION_ADMIN = 1;
public function hasPermission(int $permission): bool;
public function getUniqueId();
public function getDisplayName(): string;
public function checkPassword(string $password): bool;
}
@@ -0,0 +1,6 @@
<?php namespace MapGuesser\Interfaces\Authorization;
interface ISecured
{
public function authorize(): bool;
}
+20
View File
@@ -0,0 +1,20 @@
<?php namespace MapGuesser\Interfaces\Database;
interface IConnection
{
public function startTransaction(): void;
public function commit(): void;
public function rollback(): void;
public function query(string $query): ?IResultSet;
public function multiQuery(string $query): array;
public function prepare(string $query): IStatement;
public function lastId(): int;
public function getAffectedRows(): int;
}
+16
View File
@@ -0,0 +1,16 @@
<?php namespace MapGuesser\Interfaces\Database;
interface IResultSet
{
const FETCH_ASSOC = 0;
const FETCH_NUM = 1;
const FETCH_BOTH = 2;
public function fetch(int $type): ?array;
public function fetchAll(int $type): array;
public function fetchOneColumn(string $valueName, string $keyName): array;
}
+8
View File
@@ -0,0 +1,8 @@
<?php namespace MapGuesser\Interfaces\Database;
interface IStatement
{
public function execute(array $params): ?IResultSet;
public function getAffectedRows(): int;
}
+18
View File
@@ -0,0 +1,18 @@
<?php namespace MapGuesser\Interfaces\Http;
interface IRequest
{
const HTTP_GET = 0;
const HTTP_POST = 1;
public function setUrl(string $url): void;
public function setMethod(int $method): void;
public function setQuery($query): void;
public function setHeaders(array $headers): void;
public function send(): IResponse;
}
+8
View File
@@ -0,0 +1,8 @@
<?php namespace MapGuesser\Interfaces\Http;
interface IResponse
{
public function getBody(): string;
public function getHeaders(): array;
}
+20
View File
@@ -0,0 +1,20 @@
<?php namespace MapGuesser\Interfaces\Request;
use MapGuesser\Interfaces\Authentication\IUser;
interface IRequest
{
public function setParsedRouteParams(array &$routeParams): void;
public function getBase(): string;
public function query(string $key);
public function post(string $key);
public function session(): ISession;
public function setUser(?IUser $user): void;
public function user(): ?IUser;
}
+12
View File
@@ -0,0 +1,12 @@
<?php namespace MapGuesser\Interfaces\Request;
interface ISession
{
public function has(string $key): bool;
public function get(string $key);
public function set(string $key, $value): void;
public function delete(string $key): void;
}
+12
View File
@@ -0,0 +1,12 @@
<?php namespace MapGuesser\Interfaces\Response;
interface IContent
{
public function setData(array $data): void;
public function getData(): array;
public function render(): void;
public function getContentType(): string;
}
+12
View File
@@ -0,0 +1,12 @@
<?php namespace MapGuesser\Interfaces\Response;
interface IRedirect
{
const PERMANENT = 1;
const TEMPORARY = 2;
public function getUrl(): string;
public function getHttpCode(): int;
}
@@ -0,0 +1,9 @@
<?php namespace MapGuesser\Interfaces\Session;
use SessionHandlerInterface;
use SessionIdInterface;
use SessionUpdateTimestampHandlerInterface;
interface ISessionHandler extends SessionHandlerInterface, SessionIdInterface, SessionUpdateTimestampHandlerInterface
{
}
+87
View File
@@ -0,0 +1,87 @@
<?php namespace MapGuesser\Mailing;
use PHPMailer\PHPMailer\PHPMailer;
class Mail
{
private array $recipients = [];
public string $subject = '';
public string $body = '';
public function addRecipient(string $mail, ?string $name = null): void
{
$this->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 . '.html');
$baseParameters = [
'APP_NAME' => $_ENV['APP_NAME'],
'BASE_URL' => \Container::$request->getBase(),
];
$params = array_merge($baseParameters, $params);
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'], $_ENV['APP_NAME']);
$mailer->addReplyTo($_ENV['MAIL_FROM'], $_ENV['APP_NAME']);
$mailer->Sender = !empty($_ENV['MAIL_BOUNCE']) ? $_ENV['MAIL_BOUNCE'] : $_ENV['MAIL_FROM'];
$mailer->Subject = $this->subject;
$mailer->msgHTML($this->body);
foreach ($this->recipients as $recipient) {
$this->sendMail($mailer, $recipient);
}
}
private function sendMail(PHPMailer $mailer, array $recipient): void
{
$mailer->clearAddresses();
$mailer->addAddress($recipient[0], $recipient[1]);
$mailer->send();
}
}
-22
View File
@@ -1,22 +0,0 @@
<?php namespace MapGuesser\Multi;
class MultiConnector
{
public function sendMessage(string $func, array $args = [])
{
$message = json_encode([
'func' => $func,
'args' => $args
]);
$connection = fsockopen($_ENV['MULTI_INTERNAL_HOST'], $_ENV['MULTI_INTERNAL_PORT']);
fwrite($connection, $message);
$response = '';
while (!feof($connection)) {
$response .= fgets($connection);
}
fclose($connection);
return json_decode($response, true);
}
}
+56
View File
@@ -0,0 +1,56 @@
<?php namespace MapGuesser\OAuth;
use MapGuesser\Interfaces\Http\IRequest;
class GoogleOAuth
{
private static string $dialogUrlBase = 'https://accounts.google.com/o/oauth2/v2/auth';
private static string $tokenUrlBase = 'https://oauth2.googleapis.com/token';
private IRequest $request;
public function __construct(IRequest $request)
{
$this->request = $request;
}
public function getDialogUrl(string $state, string $redirectUrl, ?string $nonce = null, ?string $loginHint = null): string
{
$oauthParams = [
'response_type' => 'code',
'client_id' => $_ENV['GOOGLE_OAUTH_CLIENT_ID'],
'scope' => 'openid email',
'redirect_uri' => $redirectUrl,
'state' => $state,
];
if ($nonce !== null) {
$oauthParams['nonce'] = $nonce;
}
if ($loginHint !== null) {
$oauthParams['login_hint'] = $loginHint;
}
return self::$dialogUrlBase . '?' . http_build_query($oauthParams);
}
public function getToken(string $code, string $redirectUrl): array
{
$tokenParams = [
'code' => $code,
'client_id' => $_ENV['GOOGLE_OAUTH_CLIENT_ID'],
'client_secret' => $_ENV['GOOGLE_OAUTH_CLIENT_SECRET'],
'redirect_uri' => $redirectUrl,
'grant_type' => 'authorization_code',
];
$this->request->setUrl(self::$tokenUrlBase);
$this->request->setMethod(IRequest::HTTP_POST);
$this->request->setQuery($tokenParams);
$response = $this->request->send();
return json_decode($response->getBody(), true);
}
}
-113
View File
@@ -1,113 +0,0 @@
<?php namespace MapGuesser\PersistentData\Model;
use DateTime;
use SokoWeb\PersistentData\Model\Model;
class Challenge extends Model
{
protected static string $table = 'challenges';
protected static array $fields = ['token', 'time_limit', 'time_limit_type', 'no_move', 'no_pan', 'no_zoom', 'created'];
protected static array $relations = [];
private int $token;
private ?int $timeLimit = null;
private static array $timeLimitTypes = ['game', 'round'];
private string $timeLimitType = 'game';
private bool $noMove = false;
private bool $noPan = false;
private bool $noZoom = false;
private DateTime $created;
public function setToken(int $token): void
{
$this->token = $token;
}
public function setTimeLimit(?int $timeLimit): void
{
if (isset($timeLimit)) {
$this->timeLimit = $timeLimit;
}
}
public function setTimeLimitType(string $timeLimitType): void
{
if (in_array($timeLimitType, self::$timeLimitTypes)) {
$this->timeLimitType = $timeLimitType;
}
}
public function setNoMove(bool $noMove): void
{
$this->noMove = $noMove;
}
public function setNoPan(bool $noPan): void
{
$this->noPan = $noPan;
}
public function setNoZoom(bool $noZoom): void
{
$this->noZoom = $noZoom;
}
public function setCreatedDate(DateTime $created): void
{
$this->created = $created;
}
public function setCreated(string $created): void
{
$this->created = new DateTime($created);
}
public function getToken(): int
{
return $this->token;
}
public function getTimeLimit(): ?int
{
return $this->timeLimit;
}
public function getTimeLimitType(): string
{
return $this->timeLimitType;
}
public function getNoMove(): bool
{
return $this->noMove;
}
public function getNoPan(): bool
{
return $this->noPan;
}
public function getNoZoom(): bool
{
return $this->noZoom;
}
public function getCreatedDate(): DateTime
{
return $this->created;
}
public function getCreated(): string
{
return $this->created->format('Y-m-d H:i:s');
}
}
-134
View File
@@ -1,134 +0,0 @@
<?php namespace MapGuesser\PersistentData\Model;
use SokoWeb\PersistentData\Model\Model;
use MapGuesser\Util\Geo\Position;
class Guess extends Model
{
protected static string $table = 'guesses';
protected static array $fields = ['user_id', 'place_in_challenge_id', 'lat', 'lng', 'score', 'distance', 'time_spent'];
protected static array $relations = ['user' => User::class, 'place_in_challenge' => PlaceInChallenge::class];
private ?User $user = null;
private ?int $userId = null;
private ?PlaceInChallenge $placeInChallenge = null;
private ?int $placeInChallengeId = null;
private Position $position;
private int $score = 0;
private int $distance = 0;
private int $timeSpent = 0;
public function __construct()
{
$this->position = new Position(0.0, 0.0);
}
public function setUser(User $user): void
{
$this->user = $user;
}
public function setUserId(int $userId): void
{
$this->userId = $userId;
}
public function setPlaceInChallenge(PlaceInChallenge $placeInChallenge): void
{
$this->placeInChallenge = $placeInChallenge;
}
public function setPlaceInChallengeId(int $placeInChallengeId): void
{
$this->placeInChallengeId = $placeInChallengeId;
}
public function setPosition(Position $position): void
{
$this->position = $position;
}
public function setLat(float $lat): void
{
$this->position->setLat($lat);
}
public function setLng(float $lng): void
{
$this->position->setLng($lng);
}
public function setScore(int $score): void
{
$this->score = $score;
}
public function setDistance(int $distance): void
{
$this->distance = $distance;
}
public function setTimeSpent(int $timeSpent): void
{
$this->timeSpent = $timeSpent;
}
public function getUser(): ?User
{
return $this->user;
}
public function getUserId(): ?int
{
return $this->userId;
}
public function getPlaceInChallenge(): ?PlaceInChallenge
{
return $this->placeInChallenge;
}
public function getPlaceInChallengeId(): ?int
{
return $this->placeInChallengeId;
}
public function getPosition(): Position
{
return $this->position;
}
public function getLat(): float
{
return $this->position->getLat();
}
public function getLng(): float
{
return $this->position->getLng();
}
public function getScore(): int
{
return $this->score;
}
public function getDistance(): int
{
return $this->distance;
}
public function getTimeSpent(): ?int
{
return $this->timeSpent;
}
}
+1 -14
View File
@@ -1,13 +1,12 @@
<?php namespace MapGuesser\PersistentData\Model; <?php namespace MapGuesser\PersistentData\Model;
use SokoWeb\PersistentData\Model\Model;
use MapGuesser\Util\Geo\Bounds; use MapGuesser\Util\Geo\Bounds;
class Map extends Model class Map extends Model
{ {
protected static string $table = 'maps'; protected static string $table = 'maps';
protected static array $fields = ['name', 'description', 'bound_south_lat', 'bound_west_lng', 'bound_north_lat', 'bound_east_lng', 'area', 'unlisted']; protected static array $fields = ['name', 'description', 'bound_south_lat', 'bound_west_lng', 'bound_north_lat', 'bound_east_lng', 'area'];
private string $name = ''; private string $name = '';
@@ -17,8 +16,6 @@ class Map extends Model
private float $area = 0.0; private float $area = 0.0;
private bool $unlisted = false;
public function __construct() public function __construct()
{ {
$this->bounds = Bounds::createDirectly(-90.0, -180.0, 90.0, 180.0); $this->bounds = Bounds::createDirectly(-90.0, -180.0, 90.0, 180.0);
@@ -64,11 +61,6 @@ class Map extends Model
$this->area = $area; $this->area = $area;
} }
public function setUnlisted(bool $unlisted): void
{
$this->unlisted = $unlisted;
}
public function getName(): string public function getName(): string
{ {
return $this->name; return $this->name;
@@ -108,9 +100,4 @@ class Map extends Model
{ {
return $this->area; return $this->area;
} }
public function getUnlisted(): bool
{
return $this->unlisted;
}
} }
+69
View File
@@ -0,0 +1,69 @@
<?php namespace MapGuesser\PersistentData\Model;
abstract class Model
{
protected static string $table;
protected static array $fields;
protected static array $relations = [];
protected $id = null;
private array $snapshot = [];
public static function getTable(): string
{
return static::$table;
}
public static function getFields(): array
{
return array_merge(['id'], static::$fields);
}
public static function getRelations(): array
{
return static::$relations;
}
public function setId($id): void
{
$this->id = $id;
}
public function getId()
{
return $this->id;
}
public function toArray(): array
{
$array = [];
foreach (self::getFields() as $key) {
$method = 'get' . str_replace('_', '', ucwords($key, '_'));
if (method_exists($this, $method)) {
$array[$key] = $this->$method();
}
}
return $array;
}
public function saveSnapshot(): void
{
$this->snapshot = $this->toArray();
}
public function resetSnapshot(): void
{
$this->snapshot = [];
}
public function getSnapshot(): array
{
return $this->snapshot;
}
}
-89
View File
@@ -1,89 +0,0 @@
<?php namespace MapGuesser\PersistentData\Model;
use DateTime;
use SokoWeb\PersistentData\Model\Model;
class MultiRoom extends Model
{
protected static string $table = 'multi_rooms';
protected static array $fields = ['room_id', 'state', 'members', 'updated'];
private string $roomId = '';
private array $state = [];
private array $members = [];
private DateTime $updated;
public function setRoomId(string $roomId): void
{
$this->roomId = $roomId;
}
public function setStateArray(array $state): void
{
$this->state = $state;
}
public function setMembersArray(array $members): void
{
$this->members = $members;
}
public function setState(string $state): void
{
$this->state = unserialize($state);
}
public function setMembers(string $members): void
{
$this->members = unserialize($members);
}
public function setUpdatedDate(DateTime $updated): void
{
$this->updated = $updated;
}
public function setUpdated(string $updated): void
{
$this->updated = new DateTime($updated);
}
public function getRoomId(): string
{
return $this->roomId;
}
public function getStateArray(): array
{
return $this->state;
}
public function getState(): string
{
return serialize($this->state);
}
public function getMembersArray(): array
{
return $this->members;
}
public function getMembers(): string
{
return serialize($this->members);
}
public function getUpdatedDate(): DateTime
{
return $this->updated;
}
public function getUpdated(): string
{
return $this->updated->format('Y-m-d H:i:s');
}
}
+11 -5
View File
@@ -2,8 +2,8 @@
use DateInterval; use DateInterval;
use DateTime; use DateTime;
use SokoWeb\PersistentData\Model\Model; use MapGuesser\Http\Request;
use SokoWeb\Http\Request; use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Util\Geo\Position; use MapGuesser\Util\Geo\Position;
use MapGuesser\Util\Panorama\Pov; use MapGuesser\Util\Panorama\Pov;
@@ -140,7 +140,7 @@ class Place extends Model
return $this->pov->getZoom(); return $this->pov->getZoom();
} }
public function getFreshPanoId(): ?string public function getFreshPanoId(bool $canBeIndoor = false): ?string
{ {
if ( if (
$this->panoIdCachedTimestamp !== null && $this->panoIdCachedTimestamp !== null &&
@@ -152,17 +152,23 @@ class Place extends Model
$request = new Request('https://maps.googleapis.com/maps/api/streetview/metadata', Request::HTTP_GET); $request = new Request('https://maps.googleapis.com/maps/api/streetview/metadata', Request::HTTP_GET);
$request->setQuery([ $request->setQuery([
'key' => $_ENV['GOOGLE_MAPS_SERVER_API_KEY'], 'key' => $_ENV['GOOGLE_MAPS_SERVER_API_KEY'],
'location' => $this->position->getLat() . ',' . $this->position->getLng() 'location' => $this->position->getLat() . ',' . $this->position->getLng(),
'source' => $canBeIndoor ? 'default' : 'outdoor'
]); ]);
$response = $request->send(); $response = $request->send();
$panoData = json_decode($response->getBody(), true); $panoData = json_decode($response->getBody(), true);
$panoId = $panoData['status'] === 'OK' ? $panoData['pano_id'] : null; $panoId = $panoData['status'] === 'OK' ? $panoData['pano_id'] : null;
// enable indoor panos if no outdoor found
if ($panoId === null && !$canBeIndoor) {
return $this->getFreshPanoId(true);
}
$this->panoIdCached = $panoId; $this->panoIdCached = $panoId;
$this->panoIdCachedTimestamp = new DateTime(); $this->panoIdCachedTimestamp = new DateTime();
\Container::$persistentDataManager->saveToDb($this); (new PersistentDataManager())->saveToDb($this);
return $panoId; return $panoId;
} }
@@ -1,72 +0,0 @@
<?php namespace MapGuesser\PersistentData\Model;
use SokoWeb\PersistentData\Model\Model;
class PlaceInChallenge extends Model
{
protected static string $table = 'place_in_challenge';
protected static array $fields = ['place_id', 'challenge_id', 'round'];
protected static array $relations = ['place' => Place::class, 'challenge' => Challenge::class];
private ?Place $place = null;
private ?int $placeId = null;
private ?Challenge $challenge = null;
private ?int $challengeId = null;
private int $round;
public function setPlace(Place $place): void
{
$this->place = $place;
}
public function setPlaceId(int $placeId): void
{
$this->placeId = $placeId;
}
public function setChallenge(Challenge $challenge): void
{
$this->challenge = $challenge;
}
public function setChallengeId(int $challengeId): void
{
$this->challengeId = $challengeId;
}
public function setRound(int $round): void
{
$this->round = $round;
}
public function getPlace(): ?Place
{
return $this->place;
}
public function getPlaceId(): ?int
{
return $this->placeId;
}
public function getChallenge(): ?Challenge
{
return $this->challenge;
}
public function getChallengeId(): ?int
{
return $this->challengeId;
}
public function getRound(): int
{
return $this->round;
}
}
+1 -2
View File
@@ -1,8 +1,7 @@
<?php namespace MapGuesser\PersistentData\Model; <?php namespace MapGuesser\PersistentData\Model;
use DateTime; use DateTime;
use SokoWeb\PersistentData\Model\Model; use MapGuesser\Interfaces\Authentication\IUser;
use SokoWeb\Interfaces\Authentication\IUser;
class User extends Model implements IUser class User extends Model implements IUser
{ {
@@ -1,7 +1,6 @@
<?php namespace MapGuesser\PersistentData\Model; <?php namespace MapGuesser\PersistentData\Model;
use DateTime; use DateTime;
use SokoWeb\PersistentData\Model\Model;
class UserConfirmation extends Model class UserConfirmation extends Model
{ {
@@ -1,98 +0,0 @@
<?php namespace MapGuesser\PersistentData\Model;
use SokoWeb\PersistentData\Model\Model;
class UserInChallenge extends Model
{
protected static string $table = 'user_in_challenge';
protected static array $fields = ['user_id', 'challenge_id', 'current_round', 'time_left', 'is_owner'];
protected static array $relations = ['user' => User::class, 'challenge' => Challenge::class];
private ?User $user = null;
private ?int $userId = null;
private ?Challenge $challenge = null;
private ?int $challengeId = null;
private int $currentRound = 0;
private ?int $timeLeft = null;
private bool $isOwner = false;
public function setUser(User $user): void
{
$this->user = $user;
}
public function setUserId(int $userId): void
{
$this->userId = $userId;
}
public function setChallenge(Challenge $challenge): void
{
$this->challenge = $challenge;
}
public function setChallengeId(int $challengeId): void
{
$this->challengeId = $challengeId;
}
public function setCurrentRound(int $currentRound): void
{
$this->currentRound = $currentRound;
}
public function setTimeLeft(?int $timeLeft): void
{
if (isset($timeLeft)) {
$this->timeLeft = max(0, $timeLeft);
}
}
public function setIsOwner(bool $isOwner): void
{
$this->isOwner = $isOwner;
}
public function getUser(): ?User
{
return $this->user;
}
public function getUserId(): ?int
{
return $this->userId;
}
public function getChallenge(): ?Challenge
{
return $this->challenge;
}
public function getChallengeId(): ?int
{
return $this->challengeId;
}
public function getCurrentRound(): int
{
return $this->currentRound;
}
public function getTimeLeft(): ?int
{
return $this->timeLeft;
}
public function getIsOwner(): bool
{
return $this->isOwner;
}
}
@@ -1,7 +1,6 @@
<?php namespace MapGuesser\PersistentData\Model; <?php namespace MapGuesser\PersistentData\Model;
use DateTime; use DateTime;
use SokoWeb\PersistentData\Model\Model;
class UserPasswordResetter extends Model class UserPasswordResetter extends Model
{ {
@@ -1,100 +0,0 @@
<?php namespace MapGuesser\PersistentData\Model;
use DateTime;
use SokoWeb\PersistentData\Model\Model;
class UserPlayedPlace extends Model
{
protected static string $table = 'user_played_place';
protected static array $fields = ['user_id', 'place_id', 'last_time', 'occurrences'];
protected static array $relations = ['user' => User::class, 'place' => Place::class];
private ?User $user = null;
private ?int $userId = null;
private ?Place $place = null;
private ?int $placeId = null;
private DateTime $lastTime;
private int $occurrences = 1;
public function setUser(User $user): void
{
$this->user = $user;
}
public function setUserId(int $userId): void
{
$this->userId = $userId;
}
public function setPlace(Place $place): void
{
$this->place = $place;
}
public function setPlaceId(int $placeId): void
{
$this->placeId = $placeId;
}
public function setLastTimeDate(DateTime $lastTime): void
{
$this->lastTime = $lastTime;
}
public function setLastTime(string $lastTime): void
{
$this->lastTime = new DateTime($lastTime);
}
public function setOccurrences(int $occurrences): void
{
$this->occurrences = $occurrences;
}
public function incrementOccurrences(): void
{
$this->occurrences++;
}
public function getUser(): ?User
{
return $this->user;
}
public function getUserId(): ?int
{
return $this->userId;
}
public function getPlace(): ?Place
{
return $this->place;
}
public function getPlaceId(): ?int
{
return $this->placeId;
}
public function getLastTimeDate(): DateTime
{
return $this->lastTime;
}
public function getLastTime(): string
{
return $this->lastTime->format('Y-m-d H:i:s');
}
public function getOccurrences(): int
{
return $this->occurrences;
}
}
@@ -0,0 +1,226 @@
<?php namespace MapGuesser\PersistentData;
use Generator;
use MapGuesser\Database\Query\Modify;
use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\PersistentData\Model\Model;
class PersistentDataManager
{
public function selectFromDb(Select $select, string $type, bool $withRelations = false)
{
$select = $this->createSelect($select, $type, $withRelations);
$data = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
if ($data === null) {
return null;
}
$model = new $type();
$this->fillWithData($data, $model);
return $model;
}
public function selectMultipleFromDb(Select $select, string $type, bool $withRelations = false): Generator
{
$select = $this->createSelect($select, $type, $withRelations);
$result = $select->execute();
while ($data = $result->fetch(IResultSet::FETCH_ASSOC)) {
$model = new $type();
$this->fillWithData($data, $model);
yield $model;
}
}
public function selectFromDbById($id, string $type, bool $withRelations = false)
{
$select = new Select(\Container::$dbConnection);
$select->whereId($id);
return $this->selectFromDb($select, $type, $withRelations);
}
public function fillWithData(array $data, Model $model): void
{
$relations = $model::getRelations();
$relationData = [];
foreach ($data as $key => $value) {
if ($this->extractRelationData($key, $value, $relationData, $relations)) {
continue;
}
$method = 'set' . str_replace('_', '', ucwords($key, '_'));
if (method_exists($model, $method)) {
$model->$method($value);
}
}
$this->setRelations($model, $relationData);
$model->saveSnapshot();
}
public function loadRelationsFromDb(Model $model, bool $recursive): void
{
foreach ($model::getRelations() as $relation => $relationType) {
$camel = str_replace('_', '', ucwords($relation, '_'));
$methodGet = 'get' . $camel . 'Id';
$methodSet = 'set' . $camel;
$relationId = $model->$methodGet();
if ($relationId !== null) {
$relationModel = $this->selectFromDbById($relationId, $relationType, $recursive);
$model->$methodSet($relationModel);
}
}
}
public function saveToDb(Model $model): void
{
$this->syncRelations($model);
$modified = $model->toArray();
$id = $model->getId();
$modify = new Modify(\Container::$dbConnection, $model::getTable());
if ($id !== null) {
$original = $model->getSnapshot();
foreach ($original as $key => $value) {
if ($value === $modified[$key]) {
unset($modified[$key]);
}
}
if (count($modified) > 0) {
$modify->setId($id);
$modify->fill($modified);
$modify->save();
}
} else {
$modify->fill($modified);
$modify->save();
$model->setId($modify->getId());
}
$model->saveSnapshot();
}
public function deleteFromDb(Model $model): void
{
$modify = new Modify(\Container::$dbConnection, $model::getTable());
$modify->setId($model->getId());
$modify->delete();
$model->setId(null);
$model->resetSnapshot();
}
private function createSelect(Select $select, string $type, bool $withRelations = false): Select
{
$table = call_user_func([$type, 'getTable']);
$fields = call_user_func([$type, 'getFields']);
$select->from($table);
//TODO: only with some relations?
if ($withRelations) {
$relations = call_user_func([$type, 'getRelations']);
$columns = [];
foreach ($fields as $field) {
$columns[] = [$table, $field];
}
$columns = array_merge($columns, $this->getRelationColumns($relations));
$this->leftJoinRelations($select, $table, $relations);
$select->columns($columns);
} else {
$select->columns($fields);
}
return $select;
}
private function getRelationColumns(array $relations): array
{
$columns = [];
foreach ($relations as $relation => $relationType) {
$relationTable = call_user_func([$relationType, 'getTable']);
foreach (call_user_func([$relationType, 'getFields']) as $relationField) {
$columns[] = [$relationTable, $relationField, $relation . '__' . $relationField];
}
}
return $columns;
}
private function leftJoinRelations(Select $select, string $table, array $relations): void
{
foreach ($relations as $relation => $relationType) {
$relationTable = call_user_func([$relationType, 'getTable']);
$select->leftJoin($relationTable, [$relationTable, 'id'], '=', [$table, $relation . '_id']);
}
}
private function extractRelationData(string $key, $value, array &$relationData, array $relations): bool
{
$found = false;
foreach ($relations as $relation => $relationType) {
if (substr($key, 0, strlen($relation . '__')) === $relation . '__') {
$found = true;
$relationData[$relation][substr($key, strlen($relation . '__'))] = $value;
break;
}
}
return $found;
}
private function setRelations(Model $model, array &$relations): void
{
foreach ($model::getRelations() as $relation => $relationType) {
if (isset($relations[$relation])) {
$object = new $relationType();
$this->fillWithData($relations[$relation], $object);
$method = 'set' . str_replace('_', '', ucwords($relation, '_'));
$model->$method($object);
}
}
}
private function syncRelations(Model $model): void
{
foreach ($model::getRelations() as $relation => $relationType) {
$camel = str_replace('_', '', ucwords($relation, '_'));
$methodGet = 'get' . $camel;
$methodSet = 'set' . $camel . 'Id';
$relationModel = $model->$methodGet();
if ($relationModel !== null) {
$model->$methodSet($relationModel->getId());
}
}
}
}
-67
View File
@@ -1,67 +0,0 @@
<?php namespace MapGuesser\Repository;
use Generator;
use SokoWeb\Database\Query\Select;
use MapGuesser\PersistentData\Model\Challenge;
use MapGuesser\PersistentData\Model\Place;
use MapGuesser\PersistentData\Model\User;
class ChallengeRepository
{
public function getById(int $challengeId): ?Challenge
{
return \Container::$persistentDataManager->selectFromDbById($challengeId, Challenge::class);
}
public function getByToken(int $token): ?Challenge
{
$select = new Select(\Container::$dbConnection);
$select->where('token', '=', $token);
return \Container::$persistentDataManager->selectFromDb($select, Challenge::class);
}
public function getByTokenStr(string $token_str): ?Challenge
{
// validate token string
foreach (str_split($token_str) as $char) {
if (!(('0' <= $char && $char <= '9') || ('a' <= $char && $char <= 'f'))) {
return null;
}
}
// convert token to int
$token = hexdec($token_str);
return $this->getByToken($token);
}
public function getAllByParticipant(User $user): Generator
{
$select = new Select(\Container::$dbConnection);
$select->innerJoin('user_in_challenge', ['challenge', 'id'], '=', ['user_in_challenge', 'challenge_id']);
$select->innerJoin('users', ['users', 'id'], '=', ['user_in_challenge', 'user_id']);
$select->where('user_id', '=', $user->getId());
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, Challenge::class);
}
public function getAllByOwner(User $user): Generator
{
$select = new Select(\Container::$dbConnection);
$select->innerJoin('user_in_challenge', ['challenge', 'id'], '=', ['user_in_challenge', 'challenge_id']);
$select->innerJoin('users', ['users', 'id'], '=', ['user_in_challenge', 'user_id']);
$select->where('user_id', '=', $user->getId());
$select->where('is_owner', '=', true);
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, Challenge::class);
}
public function getAllByPlace(Place $place): Generator
{
$select = new Select(\Container::$dbConnection);
$select->innerJoin('place_in_challenge', ['challenges', 'id'], '=', ['place_in_challenge', 'challenge_id']);
$select->where('place_id', '=', $place->getId());
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, Challenge::class);
}
}
-89
View File
@@ -1,89 +0,0 @@
<?php namespace MapGuesser\Repository;
use Generator;
use SokoWeb\Database\Query\Select;
use MapGuesser\PersistentData\Model\Challenge;
use MapGuesser\PersistentData\Model\Guess;
use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\Model\Place;
use MapGuesser\PersistentData\Model\PlaceInChallenge;
class GuessRepository
{
public function getAllByUser(User $user): Generator
{
$select = new Select(\Container::$dbConnection);
$select->where('user_id', '=', $user->getId());
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, Guess::class);
}
public function getAllByUserAndChallenge(User $user, Challenge $challenge): Generator
{
$select = new Select(\Container::$dbConnection);
$select->innerJoin('place_in_challenge', ['place_in_challenge', 'id'], '=', ['guesses', 'place_in_challenge_id']);
$select->where('user_id', '=', $user->getId());
$select->where('challenge_id', '=', $challenge->getId());
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, Guess::class);
}
public function getByUserAndPlaceInChallenge(User $user, Challenge $challenge, Place $place): ?Guess
{
$select = new Select(\Container::$dbConnection);
$select->innerJoin('place_in_challenge', ['place_in_challenge', 'id'], '=', ['guesses', 'place_in_challenge_id']);
$select->where('user_id', '=', $user->getId());
$select->where('challenge_id', '=', $challenge->getId());
$select->where('place_id', '=', $place->getId());
return \Container::$persistentDataManager->selectFromDb($select, Guess::class);
}
public function getAllInChallengeByUser(int $userId, Challenge $challenge): Generator
{
$select = new Select(\Container::$dbConnection);
$select->innerJoin('place_in_challenge', ['place_in_challenge', 'id'], '=', ['guesses', 'place_in_challenge_id']);
$select->where('user_id', '=', $userId);
$select->where('challenge_id', '=', $challenge->getId());
$select->orderBy('round');
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, Guess::class);
}
public function getAllInChallenge(Challenge $challenge, array $withRelations = []): Generator
{
if (count($withRelations)) {
$necessaryRelations = ['place_in_challenge'];
$withRelations = array_unique(array_merge($withRelations, $necessaryRelations));
}
$select = new Select(\Container::$dbConnection);
$select->where('challenge_id', '=', $challenge->getId());
$select->orderBy('round');
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, Guess::class, true, $withRelations);
}
public function getAllInChallengeByRound(int $round, Challenge $challenge, array $withRelations = []): Generator
{
if (count($withRelations)) {
$necessaryRelations = ['place_in_challenge'];
$withRelations = array_unique(array_merge($withRelations, $necessaryRelations));
}
$select = new Select(\Container::$dbConnection);
$select->where('challenge_id', '=', $challenge->getId());
$select->where('round', '=', $round);
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, Guess::class, true, $withRelations);
}
public function getAllByPlace(Place $place): Generator
{
$select = new Select(\Container::$dbConnection);
$select->innerJoin('place_in_challenge', ['place_in_challenge', 'id'], '=', ['guesses', 'place_in_challenge_id']);
$select->where('place_id', '=', $place->getId());
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, Guess::class);
}
}
+9 -20
View File
@@ -1,30 +1,19 @@
<?php namespace MapGuesser\Repository; <?php namespace MapGuesser\Repository;
use SokoWeb\Database\Query\Select;
use MapGuesser\PersistentData\Model\Challenge;
use MapGuesser\PersistentData\Model\Map; use MapGuesser\PersistentData\Model\Map;
use MapGuesser\PersistentData\Model\Place; use MapGuesser\PersistentData\PersistentDataManager;
class MapRepository class MapRepository
{ {
private PersistentDataManager $pdm;
public function __construct()
{
$this->pdm = new PersistentDataManager();
}
public function getById(int $mapId): ?Map public function getById(int $mapId): ?Map
{ {
return \Container::$persistentDataManager->selectFromDbById($mapId, Map::class); return $this->pdm->selectFromDbById($mapId, Map::class);
}
public function getByPlace(Place $place): ?Map
{
return $this->getById($place->getMapId());
}
public function getByChallenge(Challenge $challenge): ?Map
{
$select = new Select(\Container::$dbConnection);
$select->innerJoin('places', ['maps', 'id'], '=', ['places', 'map_id']);
$select->innerJoin('place_in_challenge', ['places', 'id'], '=', ['place_in_challenge', 'place_id']);
$select->where('challenge_id', '=', $challenge->getId());
$select->limit(1);
return \Container::$persistentDataManager->selectFromDb($select, Map::class);
} }
} }
-30
View File
@@ -1,30 +0,0 @@
<?php namespace MapGuesser\Repository;
use DateTime;
use Generator;
use SokoWeb\Database\Query\Select;
use MapGuesser\PersistentData\Model\MultiRoom;
class MultiRoomRepository
{
public function getById(int $id): ?MultiRoom
{
return \Container::$persistentDataManager->selectFromDbById($id, MultiRoom::class);
}
public function getByRoomId(string $roomId): ?MultiRoom
{
$select = new Select(\Container::$dbConnection);
$select->where('room_id', '=', $roomId);
return \Container::$persistentDataManager->selectFromDb($select, MultiRoom::class);
}
public function getAllExpired(): Generator
{
$select = new Select(\Container::$dbConnection);
$select->where('updated', '<', (new DateTime('-7 day'))->format('Y-m-d H:i:s'));
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, MultiRoom::class);
}
}
@@ -1,45 +0,0 @@
<?php namespace MapGuesser\Repository;
use Generator;
use SokoWeb\Database\Query\Select;
use MapGuesser\PersistentData\Model\Challenge;
use MapGuesser\PersistentData\Model\Place;
use MapGuesser\PersistentData\Model\PlaceInChallenge;
class PlaceInChallengeRepository
{
public function getAllByPlace(Place $place, array $withRelations = []) : Generator
{
$select = new Select(\Container::$dbConnection);
$select->where('place_id', '=', $place->getId());
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, PlaceInChallenge::class, true, $withRelations);
}
public function getAllByChallenge(Challenge $challenge) : Generator
{
$select = new Select(\Container::$dbConnection);
$select->where('challenge_id', '=', $challenge->getId());
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, PlaceInChallenge::class);
}
public function getByPlaceAndChallenge(Place $place, Challenge $challenge) : ?PlaceInChallenge
{
$select = new Select(\Container::$dbConnection);
$select->where('place_id', '=', $place->getId());
$select->where('challenge_id', '=', $challenge->getId());
return \Container::$persistentDataManager->selectFromDb($select, PlaceInChallenge::class);
}
public function getByRoundInChallenge(int $round, Challenge $challenge, array $withRelations = []): ?PlaceInChallenge
{
$select = new Select(\Container::$dbConnection);
$select->where('challenge_id', '=', $challenge->getId());
$select->orderBy('round');
$select->limit(1, $round);
return \Container::$persistentDataManager->selectFromDb($select, PlaceInChallenge::class, true, $withRelations);
}
}
+30 -154
View File
@@ -1,16 +1,23 @@
<?php namespace MapGuesser\Repository; <?php namespace MapGuesser\Repository;
use Generator; use Generator;
use SokoWeb\Database\Query\Select; use MapGuesser\Database\Query\Select;
use MapGuesser\PersistentData\Model\Challenge;
use MapGuesser\PersistentData\Model\Map; use MapGuesser\PersistentData\Model\Map;
use MapGuesser\PersistentData\Model\Place; use MapGuesser\PersistentData\Model\Place;
use MapGuesser\PersistentData\PersistentDataManager;
class PlaceRepository class PlaceRepository
{ {
private PersistentDataManager $pdm;
public function __construct()
{
$this->pdm = new PersistentDataManager();
}
public function getById(int $placeId): ?Place public function getById(int $placeId): ?Place
{ {
return \Container::$persistentDataManager->selectFromDbById($placeId, Place::class); return $this->pdm->selectFromDbById($placeId, Place::class);
} }
public function getAllForMap(Map $map): Generator public function getAllForMap(Map $map): Generator
@@ -18,177 +25,46 @@ class PlaceRepository
$select = new Select(\Container::$dbConnection); $select = new Select(\Container::$dbConnection);
$select->where('map_id', '=', $map->getId()); $select->where('map_id', '=', $map->getId());
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, Place::class); yield from $this->pdm->selectMultipleFromDb($select, Place::class);
}
//TODO: use Map and User instead of id
public function getRandomNPlaces(int $mapId, int $n, ?int $userId): array
{
if (!isset($userId)) { // anonymous single player
return $this->getRandomNForMapWithValidPano($mapId, $n);
} else { // authorized user or multiplayer game with selection based on what the host played before
$unvisitedPlaces = $this->getRandomUnvisitedNForMapWithValidPano($mapId, $n, $userId);
if (count($unvisitedPlaces) == $n) {
return $unvisitedPlaces;
}
$oldPlaces = $this->getRandomOldNForMapWithValidPano($mapId, $n - count($unvisitedPlaces), $userId);
return array_merge($unvisitedPlaces, $oldPlaces);
}
} }
//TODO: use Map instead of id //TODO: use Map instead of id
private function getRandomNForMapWithValidPano(int $mapId, int $n): array public function getRandomForMapWithValidPano(int $mapId, array $exclude = [], array &$placesWithoutPano): Place
{ {
$places = []; $placesWithoutPano = [];
$select = new Select(\Container::$dbConnection, 'places');
$select->where('map_id', '=', $mapId);
$numberOfPlaces = $select->count();
$exclude = [];
for ($i = 1; $i <= $n; ++$i) {
$place = $this->getRandomForMapWithValidPano($numberOfPlaces, $select, $exclude);
$places[] = $place;
$exclude[] = $place->getId();
}
return $places;
}
private function getRandomForMapWithValidPano(int $numberOfPlaces, Select $select, array &$exclude, ?callable $pickRandomInt = null): ?Place
{
do { do {
$numberOfPlacesLeft = $numberOfPlaces - count($exclude); $place = $this->selectRandomFromDbForMap($mapId, $exclude);
$place = $this->selectRandomFromDbForMap($numberOfPlacesLeft, $select, $exclude, $pickRandomInt);
if ($place === null) {
// there is no more never visited place left
return null;
}
$panoId = $place->getFreshPanoId(); $panoId = $place->getFreshPanoId();
if ($panoId === null) { if ($panoId === null) {
$exclude[] = $place->getId(); $placesWithoutPano[] = $exclude[] = $place->getId();
} }
} while ($panoId === null); } while ($panoId === null);
return $place; return $place;
} }
private function selectRandomFromDbForMap(int $numberOfPlacesLeft, Select $select, array $exclude, ?callable $pickRandomInt): ?Place private function selectRandomFromDbForMap(int $mapId, array $exclude): Place
{ {
if ($numberOfPlacesLeft <= 0) //TODO: omit table name here
return null; $select = new Select(\Container::$dbConnection, 'places');
$select->where('id', 'NOT IN', $exclude);
$select->where('map_id', '=', $mapId);
if (!isset($pickRandomInt)) { $numberOfPlaces = $select->count();
$randomOffset = random_int(0, $numberOfPlacesLeft - 1);
} else { //TODO: prevent this
$randomOffset = $pickRandomInt($numberOfPlacesLeft); if ($numberOfPlaces === 0) {
throw new \Exception('There is no selectable place (count was 0).');
} }
$select->where('id', 'NOT IN', $exclude); $randomOffset = random_int(0, $numberOfPlaces - 1);
$select->orderBy('id');
$select->limit(1, $randomOffset); $select->limit(1, $randomOffset);
return \Container::$persistentDataManager->selectFromDb($select, Place::class); return $this->pdm->selectFromDb($select, Place::class);
} }
// Never visited places
private function getRandomUnvisitedNForMapWithValidPano(int $mapId, int $n, int $userId): array
{
$places = [];
$exclude = [];
// list of places visited by user
$selectPlacesByCurrentUser = new Select(\Container::$dbConnection, 'user_played_place');
$selectPlacesByCurrentUser->columns(['place_id', 'last_time']);
$selectPlacesByCurrentUser->where('user_id', '=', $userId);
$selectPlacesByCurrentUser->setDerivedTableAlias('places_by_current_user');
// count the places never visited
$selectUnvisited = new Select(\Container::$dbConnection, 'places');
$selectUnvisited->leftJoin($selectPlacesByCurrentUser, ['places', 'id'], '=', ['places_by_current_user', 'place_id']);
$selectUnvisited->where('map_id', '=', $mapId);
$selectUnvisited->where('last_time', '=', null);
$numberOfUnvisitedPlaces = $selectUnvisited->count();
// look for as many new places as possible but maximum $n
do {
$place = $this->getRandomForMapWithValidPano($numberOfUnvisitedPlaces, $selectUnvisited, $exclude);
if (isset($place)) {
$places[] = $place;
$exclude[] = $place->getId();
}
} while (count($places) < $n && isset($place));
return $places;
}
// Places visited in the longest time
private function getRandomOldNForMapWithValidPano(int $mapId, int $n, int $userId): array
{
$places = [];
$exclude = [];
// list of places visited by user
$selectPlacesByCurrentUser = new Select(\Container::$dbConnection, 'user_played_place');
$selectPlacesByCurrentUser->columns(['place_id', 'last_time']);
$selectPlacesByCurrentUser->where('user_id', '=', $userId);
$selectPlacesByCurrentUser->setDerivedTableAlias('places_by_current_user');
// count places that were visited at least once
$selectOldPlaces = new Select(\Container::$dbConnection, 'places');
$selectOldPlaces->innerJoin($selectPlacesByCurrentUser, ['places', 'id'], '=', ['places_by_current_user', 'place_id']);
$selectOldPlaces->where('map_id', '=', $mapId);
$numberOfOldPlaces = $selectOldPlaces->count();
// set order by datetime, oldest first
$selectOldPlaces->orderBy('last_time');
// selection algorithm with preference (weighting) for older places using Box-Muller transform
$pickGaussianRandomInt = function($numberOfPlaces) {
$stdev = 0.2;
$avg = 0.0;
$x = mt_rand() / mt_getrandmax();
$y = mt_rand() / mt_getrandmax();
$randomNum = abs(sqrt(-2 * log($x)) * cos(2 * pi() * $y) * $stdev + $avg);
return (int) min($randomNum * $numberOfPlaces, $numberOfPlaces - 1);
};
// look for n - numberOfUnvisitedPlaces places
while (count($places) < $n)
{
$place = $this->getRandomForMapWithValidPano($numberOfOldPlaces, $selectOldPlaces, $exclude, $pickGaussianRandomInt);
if (isset($place))
{
$places[] = $place;
$exclude[] = $place->getId();
}
}
return $places;
}
public function getByRoundInChallenge(Challenge $challenge, int $round): ?Place
{
$select = new Select(\Container::$dbConnection);
$select->innerJoin('place_in_challenge', ['places', 'id'], '=', ['place_in_challenge', 'place_id']);
$select->where('challenge_id', '=', $challenge->getId());
$select->orderBy('round');
$select->limit(1, $round);
return \Container::$persistentDataManager->selectFromDb($select, Place::class);
}
public function getAllInChallenge(Challenge $challenge): Generator
{
$select = new Select(\Container::$dbConnection);
$select->innerJoin('place_in_challenge', ['places', 'id'], '=', ['place_in_challenge', 'place_id']);
$select->where('challenge_id', '=', $challenge->getId());
$select->orderBy('round');
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, Place::class);
}
} }
+12 -4
View File
@@ -1,14 +1,22 @@
<?php namespace MapGuesser\Repository; <?php namespace MapGuesser\Repository;
use SokoWeb\Database\Query\Select; use MapGuesser\Database\Query\Select;
use MapGuesser\PersistentData\Model\User; use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\Model\UserConfirmation; use MapGuesser\PersistentData\Model\UserConfirmation;
use MapGuesser\PersistentData\PersistentDataManager;
class UserConfirmationRepository class UserConfirmationRepository
{ {
private PersistentDataManager $pdm;
public function __construct()
{
$this->pdm = new PersistentDataManager();
}
public function getById(int $userConfirmationId): ?UserConfirmation public function getById(int $userConfirmationId): ?UserConfirmation
{ {
return \Container::$persistentDataManager->selectFromDbById($userConfirmationId, UserConfirmation::class); return $this->pdm->selectFromDbById($userConfirmationId, UserConfirmation::class);
} }
public function getByToken(string $token): ?UserConfirmation public function getByToken(string $token): ?UserConfirmation
@@ -16,7 +24,7 @@ class UserConfirmationRepository
$select = new Select(\Container::$dbConnection); $select = new Select(\Container::$dbConnection);
$select->where('token', '=', $token); $select->where('token', '=', $token);
return \Container::$persistentDataManager->selectFromDb($select, UserConfirmation::class); return $this->pdm->selectFromDb($select, UserConfirmation::class);
} }
public function getByUser(User $user): ?UserConfirmation public function getByUser(User $user): ?UserConfirmation
@@ -24,6 +32,6 @@ class UserConfirmationRepository
$select = new Select(\Container::$dbConnection); $select = new Select(\Container::$dbConnection);
$select->where('user_id', '=', $user->getId()); $select->where('user_id', '=', $user->getId());
return \Container::$persistentDataManager->selectFromDb($select, UserConfirmation::class); return $this->pdm->selectFromDb($select, UserConfirmation::class);
} }
} }
Loaded 100 of 142 files, more files were not shown because too many files have changed in this diff. Show more