Compare commits

..
Author SHA1 Message Date
bence 7c96ba24d6 MAPG-146 [WIP] 2020-06-14 22:10:36 +02:00
163 changed files with 1505 additions and 9153 deletions

No files matched your search

-9
View File
@@ -1,4 +1,3 @@
APP_NAME=MapGuesser
DEV=1
DB_HOST=mariadb
DB_USER=mapguesser
@@ -7,16 +6,8 @@ DB_NAME=mapguesser
GOOGLE_MAPS_SERVER_API_KEY=your_google_maps_server_api_key
GOOGLE_MAPS_JS_API_KEY=your_google_maps_js_api_key
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
STATIC_ROOT=/static
MAIL_FROM=mapguesser@mapguesser-dev.ch
MAIL_HOST=mail
MAIL_PORT=2500
GOOGLE_OAUTH_CLIENT_ID=your_google_oauth_client_id
GOOGLE_OAUTH_CLIENT_SECRET=your_google_oauth_client_secret
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
-1
View File
@@ -1,4 +1,3 @@
.env
installed
vendor
node_modules
-8
View File
@@ -9,14 +9,6 @@
"pathMappings": {
"/var/www/mapguesser": "${workspaceRoot}",
}
},
{
"name": "Listen for NodeJS Inspector in Docker",
"type": "node",
"request": "attach",
"port": 9229,
"localRoot": "${workspaceFolder}",
"remoteRoot": "/var/www/mapguesser"
}
]
}
+2 -101
View File
@@ -1,104 +1,5 @@
# MapGuesser
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.
## Installation
### Clone the Git repository
The first step is obviously cloning the repository to your machine:
```
git clone https://gitea.e5tv.hu/esoko/mapguesser.git
```
All the commands listed here should be executed from the repository root.
### Setup Docker stack (recommended)
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`.
All you have to do is executing the following command:
```
docker-compose up -d
```
Attach shell to the container of `mapguesser_app`:
```
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
This command installes all of the Composer requirements and creates a copy of the example `.env` file.
```
composer create-project
```
### Set environment variables
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.
**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.
#### API keys
**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
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:
```
0 * * * * /path/to/your/installation/mapg db:maintain >>/var/log/cron-mapguesser.log 2>&1
```
### Finalize installation
After you followed the above steps, execute the following command:
```
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. 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.
---
*License: **GNU AGPL 3.0**. Full license text can be found in file `LICENSE`.*
License: GNU AGPL 3.0
-26
View File
@@ -1,26 +0,0 @@
image: php:7.4.7-cli-buster
pipelines:
default:
- step:
name: Unit Testing
caches:
- composer
artifacts:
- unit_test_results.xml
script:
- apt-get update && apt-get install -y unzip
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
- composer install
- vendor/bin/phpunit --log-junit unit_test_results.xml --testdox tests
- step:
name: Static Code Analysis
caches:
- composer
artifacts:
- static_code_analysis_results.json
script:
- apt-get update && apt-get install -y unzip
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
- composer install
- php -d memory_limit=1G vendor/bin/phpstan analyse -c phpstan.neon --error-format=prettyJson > static_code_analysis_results.json
-2
View File
@@ -1,2 +0,0 @@
*
!.gitignore
+2 -6
View File
@@ -6,13 +6,9 @@
"require": {
"vlucas/phpdotenv": "^4.1",
"symfony/console": "^5.1",
"phpmailer/phpmailer": "^6.1",
"fzaninotto/faker": "^1.9"
},
"require-dev": {
"phpunit/phpunit": "^9",
"phpstan/phpstan": "^0.12.32"
"phpmailer/phpmailer": "^6.1"
},
"require-dev": {},
"autoload": {
"psr-4": {
"MapGuesser\\": "src"
Generated
+3 -1813
View File
File diff suppressed because it is too large. Load diff
@@ -1,8 +0,0 @@
ALTER TABLE
`users`
ADD
`google_sub` varchar(255) CHARACTER SET ascii COLLATE ascii_bin NULL DEFAULT NULL,
ADD
UNIQUE `google_sub` (`google_sub`),
MODIFY
`password` varchar(60) NULL DEFAULT NULL;
@@ -1,8 +0,0 @@
ALTER TABLE
`places`
ADD
`pov_heading` decimal(6, 3) NOT NULL DEFAULT 0.0,
ADD
`pov_pitch` decimal(5, 3) NOT NULL DEFAULT 0.0,
ADD
`pov_zoom` decimal(5, 4) NOT NULL DEFAULT 0.0;
@@ -1,10 +0,0 @@
CREATE TABLE `user_password_resetters` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL,
`token` varchar(32) CHARACTER SET ascii NOT NULL,
`expires` timestamp NOT NULL,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
KEY `token` (`token`),
CONSTRAINT `user_password_resetters_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
@@ -1,5 +0,0 @@
UPDATE `user_confirmations` SET token=SUBSTRING(token, 1, 32);
ALTER TABLE `user_confirmations`
ADD `last_sent` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `token` varchar(32) CHARACTER SET ascii NOT NULL;
@@ -1,2 +0,0 @@
ALTER TABLE `sessions`
MODIFY `updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP;
@@ -1,4 +0,0 @@
UPDATE `sessions` SET id=SUBSTRING(id, 1, 32);
ALTER TABLE `sessions`
MODIFY `id` varchar(32) CHARACTER SET ascii NOT NULL;
@@ -1,2 +0,0 @@
ALTER TABLE `users`
ADD `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP;
@@ -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;
+5 -16
View File
@@ -1,27 +1,16 @@
version: '3'
services:
app:
build:
context: ./docker
dockerfile: Dockerfile-app
build: ./docker
ports:
- 80:80
volumes:
- .:/var/www/mapguesser
multi:
build:
context: ./docker
dockerfile: Dockerfile-multi
ports:
- 5000:5000
- 8090:8090
- 9229:9229
volumes:
- .:/var/www/mapguesser
links:
- 'mariadb'
- 'mail'
mariadb:
image: mariadb:10.3
ports:
- 3306:3306
image: mariadb:10.1
volumes:
- mysql:/var/lib/mysql
environment:
+3 -3
View File
@@ -4,8 +4,8 @@ ENV DEBIAN_FRONTEND noninteractive
# Install Nginx, PHP and further necessary packages
RUN apt update --fix-missing
RUN apt install -y curl git unzip mariadb-client nginx \
php-apcu php-xdebug php7.4-cli php7.4-curl php7.4-fpm php7.4-mbstring php7.4-mysql php7.4-zip php7.4-xml
RUN apt install -y curl git mariadb-client nginx \
php-apcu php-xdebug php7.4-cli php7.4-curl php7.4-fpm php7.4-mbstring php7.4-mysql php7.4-zip
# Configure Nginx with PHP
RUN mkdir -p /run/php
@@ -21,7 +21,7 @@ RUN ./install-composer.sh
# Install Node.js and required packages
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash -
RUN apt install -y nodejs
RUN npm install -g uglify-js clean-css-cli svgo yarn
RUN npm install -g uglify-js clean-css-cli svgo
EXPOSE 80
VOLUME /var/www/mapguesser
-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
-13
View File
@@ -1,13 +0,0 @@
Hi,
<br><br>
You recently requested password reset on {{APP_NAME}} with this email address ({{EMAIL}}).
To reset the password to your account, please click on the following link:<br>
<a href="{{RESET_LINK}}" title="Reset password">{{RESET_LINK}}</a>
<br><br>
You can reset your password with this link util {{EXPIRES}}.
<br><br>
If you did not requested password reset, no further action is required, your account is not touched.
<br><br>
Regards,<br>
{{APP_NAME}}<br>
<a href="{{BASE_URL}}" title="{{APP_NAME}}">{{BASE_URL}}</a>
-9
View File
@@ -1,9 +0,0 @@
Hi,
<br><br>
You recently signed up on {{APP_NAME}} with this Google account ({{EMAIL}}).
<br><br>
Have fun on {{APP_NAME}}!
<br><br>
Regards,<br>
{{APP_NAME}}<br>
<a href="{{BASE_URL}}" title="{{APP_NAME}}">{{BASE_URL}}</a>
+5 -10
View File
@@ -1,18 +1,13 @@
Hi,
<br><br>
You recently signed up on {{APP_NAME}} with this email address ({{EMAIL}}).
To activate your account, please click on the following link:<br>
You recently signed up on MapGuesser with this email address ({{EMAIL}}). To activate your account, please click on the following link:<br>
<a href="{{ACTIVATE_LINK}}" title="Account activation">{{ACTIVATE_LINK}}</a>
<br><br>
You can activate your account until {{ACTIVATABLE_UNTIL}}.
If you don't activate your account, your email address will be permanently deleted after this point of time.
<br><br>
If you did not sign up on {{APP_NAME}} or changed your mind, no further action is required.
However if you want to immediately delete your email address, please click on the following link:<br>
If you did not sign up on MapGuesser or changed your mind, no further action is required, your email address will be deleted soon.<br>
However if you want to immediately delete it, please click on the following link:<br>
<a href="{{CANCEL_LINK}}" title="Sign up cancellation">{{CANCEL_LINK}}</a>
<br><br>
Have fun on {{APP_NAME}}!
Have fun on MapGuesser!
<br><br>
Regards,<br>
{{APP_NAME}}<br>
<a href="{{BASE_URL}}" title="{{APP_NAME}}">{{BASE_URL}}</a>
MapGuesser
+1 -3
View File
@@ -1,7 +1,5 @@
<?php
define('SCRIPT_STARTED', hrtime(true));
require 'vendor/autoload.php';
const ROOT = __DIR__;
@@ -16,7 +14,7 @@ class Container
{
static MapGuesser\Interfaces\Database\IConnection $dbConnection;
static MapGuesser\Routing\RouteCollection $routeCollection;
static MapGuesser\Interfaces\Session\ISessionHandler $sessionHandler;
static \SessionHandlerInterface $sessionHandler;
static MapGuesser\Interfaces\Request\IRequest $request;
}
+1 -3
View File
@@ -5,9 +5,7 @@ require 'main.php';
$app = new Symfony\Component\Console\Application('MapGuesser Console', '');
$app->add(new MapGuesser\Cli\MigrateDatabaseCommand());
$app->add(new MapGuesser\Cli\DatabaseMigration());
$app->add(new MapGuesser\Cli\AddUserCommand());
$app->add(new MapGuesser\Cli\LinkViewCommand());
$app->add(new \MapGuesser\Cli\MaintainDatabaseCommand());
$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"
}
-9
View File
@@ -1,9 +0,0 @@
parameters:
level: 5
checkMissingIterableValueType: false
paths:
- main.php
- mapg
- web.php
- src
- tests
+5 -7
View File
@@ -9,7 +9,7 @@ if (($pos = strpos($url, '?')) !== false) {
}
$url = rawurldecode($url);
$match = Container::$routeCollection->match($method, $url == '' ? [] : explode('/', $url));
$match = Container::$routeCollection->match($method, explode('/', $url));
if ($match !== null) {
list($route, $params) = $match;
@@ -26,9 +26,8 @@ if ($match !== null) {
}
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();
echo json_encode(['error' => 'no_valid_anti_csrf_token']);
return;
}
@@ -37,17 +36,16 @@ if ($match !== null) {
if ($response instanceof MapGuesser\Interfaces\Response\IContent) {
header('Content-Type: ' . $response->getContentType() . '; charset=UTF-8');
$response->render();
echo $response->render();
return;
} elseif ($response instanceof MapGuesser\Interfaces\Response\IRedirect) {
header('Location: ' . $response->getUrl(), true, $response->getHttpCode());
header('Location: ' . Container::$request->getBase() . '/' . $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();
require ROOT . '/views/error/404.php';
+10 -76
View File
@@ -1,15 +1,12 @@
#panorama {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
width: 100%;
height: calc(100% - 40px);
z-index: 1;
}
#panoCover {
#guessCover {
position: absolute;
top: 0;
top: 40px;
left: 0;
bottom: 0;
right: 0;
@@ -79,7 +76,7 @@
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;
}
@@ -102,70 +99,19 @@
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;
}
@media screen and (max-width: 599px) {
#mapName {
display: none;
}
#showGuessButtonContainer {
position: absolute;
left: 65px;
left: 20px;
bottom: 30px;
right: 20px;
z-index: 2;
}
#guess {
top: 10px;
top: 50px;
left: 20px;
opacity: 0.95;
visibility: hidden;
@@ -176,10 +122,6 @@
#scoreBarBase {
width: 100%;
}
#navigation {
bottom: 25px;
left: 10px;
}
}
@media screen and (min-width: 600px) {
@@ -215,7 +157,7 @@
#guess.result {
width: initial;
height: initial;
top: 10px;
top: 50px;
left: 50px;
right: 50px;
bottom: 50px;
@@ -223,17 +165,13 @@
#scoreBarBase {
width: 60%;
}
#navigation {
bottom: 50px;
left: 20px;
}
@media screen and (max-height: 424px) {
#guess {
top: 10px;
top: 50px;
height: initial;
}
#guess.adapt:hover {
top: 10px;
top: 50px;
height: initial;
}
#guess.result {
@@ -241,9 +179,5 @@
right: 20px;
bottom: 30px;
}
#navigation {
bottom: 30px;
left: 10px;
}
}
}
+10 -35
View File
@@ -1,20 +1,9 @@
.map {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
#map {
width: 100%;
height: calc(100% - 40px);
z-index: 1;
}
#mapSelection img {
display: inline;
width: 1em;
height: 1em;
vertical-align: -0.15em;
}
/* modify the cursor for the Leaflet map */
.leaflet-container {
cursor: crosshair;
}
@@ -27,8 +16,6 @@
#noPano {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
z-index: 2;
visibility: hidden;
@@ -36,12 +23,12 @@
}
#noPano>p {
text-align: center;
margin: auto;
}
#control {
position: absolute;
top: 10px;
top: 50px;
right: 10px;
width: 125px;
z-index: 3;
@@ -55,49 +42,37 @@
visibility: hidden;
}
#overlayControl {
bottom: 20px;
right: 10px;
z-index: 3;
}
#deleteButton {
display: none;
}
@media screen and (max-width: 999px) and (min-height: 600px) {
#map.selected {
height: 50%;
height: calc(50% - 20px);
}
#panorama, #noPano {
left: 0;
bottom: 0;
right: 0;
height: 50%;
height: calc(50% - 20px);
}
#placeControl {
top: calc(50% + 10px);
}
.hideOnMobile {
display: none;
top: calc(50% + 30px);
}
}
@media screen and (min-width: 1000px), (max-height: 599px) {
#map.selected {
top: 0;
bottom: 0;
left: 0;
width: 50%;
}
#panorama, #noPano {
top: 0;
top: 40px;
bottom: 0;
right: 0;
width: 50%;
}
#placeControl {
top: 10px;
top: 50px;
}
#modified.selected {
right: calc(50% + 10px);
+26 -233
View File
@@ -12,25 +12,11 @@ html, body {
padding: 0;
}
body {
background-color: #cccccc;
}
button::-moz-focus-inner, input::-moz-focus-inner {
padding: 0;
border: 0;
}
/* to be compatible with browsers that don't know <main> */
main {
display: block;
}
::selection {
background-color: #28a745;
color: #ffffff;
}
p, h1, h2, input, textarea, select, button, a {
font-family: 'Roboto', sans-serif;
}
@@ -51,7 +37,7 @@ h1>a:hover, h1>a:focus {
text-decoration: none;
}
h2, header.small h1 {
h2, div.header.small h1 {
font-size: 24px;
}
@@ -92,8 +78,8 @@ hr {
font-weight: 500;
}
p.small, span.small {
font-size: 14px;
.small {
font-size: 12px;
}
.justify {
@@ -116,10 +102,6 @@ p.small, span.small {
margin-right: 10px;
}
.center {
text-align: center;
}
.right {
text-align: right;
}
@@ -128,7 +110,6 @@ svg.inline, img.inline {
display: inline;
width: 1em;
height: 1em;
margin-right: 0.3em;
vertical-align: -0.15em;
}
@@ -157,27 +138,6 @@ button, a.button {
line-height: 35px;
}
button.small, div.inputWithButton>button {
font-size: 14px;
padding: 0 12px;
height: 32px;
line-height: 32px;
}
button.small {
height: 32px;
line-height: 32px;
}
div.inputWithButton>button {
border-radius: 2px;
height: 27px;
line-height: 27px;
width: 75px;
margin-left: -79px;
vertical-align: 2px;
}
button:enabled:hover, button:enabled:focus, a.button:hover, a.button:focus {
background-color: #29457f;
outline: none;
@@ -209,7 +169,7 @@ button.gray, a.button.gray {
background-color: #808080;
}
button.gray:enabled:hover, button.gray:enabled:focus, a.button.gray:hover, a.button.gray:focus {
button.gray:hover, button.gray:focus, a.button.gray:hover, a.button.gray:focus {
background-color: #555555;
}
@@ -217,7 +177,7 @@ button.red, a.button.red {
background-color: #aa5e5e;
}
button.red:enabled:hover, button.red:enabled:focus, a.button.red:hover, a.button.red:focus {
button.red:hover, button.red:focus, a.button.red:hover, a.button.red:focus {
background-color: #7f2929;
}
@@ -225,7 +185,7 @@ button.yellow, a.button.yellow {
background-color: #e8a349;
}
button.yellow:enabled:hover, button.yellow:enabled:focus, a.button.yellow:hover, a.button.yellow:focus {
button.yellow:hover, button.yellow:focus, a.button.yellow:hover, a.button.yellow:focus {
background-color: #c37713;
}
@@ -233,7 +193,7 @@ button.green, a.button.green {
background-color: #28a745;
}
button.green:enabled:hover, button.green:enabled:focus, a.button.green:hover, a.button.green:focus {
button.green:hover, button.green:focus, a.button.green:hover, a.button.green:focus {
background-color: #1b7d31;
}
@@ -241,36 +201,22 @@ input, select, textarea {
background-color: #f9fafb;
border: solid #c8d2e1 1px;
border-radius: 2px;
padding: 4px;
box-sizing: border-box;
font-size: 15px;
font-weight: 300;
}
input, select {
height: 30px;
line-height: 30px;
padding: 0 5px;
}
textarea {
padding: 5px;
font-size: 13px;
resize: none;
}
input.big, select.big, textarea.big, div.inputWithButton>input {
input.big, select.big, textarea.big {
padding: 5px;
font-size: 18px;
}
input.big, select.big, div.inputWithButton>input {
height: 35px;
line-height: 35px;
padding: 0 6px;
}
textarea.big {
padding: 6px;
}
input.fullWidth, select.fullWidth, textarea.fullWidth {
display: block;
width: 100%;
@@ -285,41 +231,22 @@ input:disabled, select:disabled, textarea:disabled {
input:focus, select:focus, textarea:focus {
background-color: #ffffff;
border: solid #29457f 2px;
padding: 3px;
outline: none;
}
input:focus, select:focus {
padding: 0 4px;
}
textarea:focus {
input.big:focus, select.big:focus, textarea.big:focus {
padding: 4px;
}
input.big:focus, select.big:focus {
padding: 0 5px;
}
div.inputWithButton>input {
width: 100%;
padding: 0 83px 0 6px;
}
div.inputWithButton>input:focus {
padding: 0 82px 0 5px;
}
textarea.big:focus {
padding: 5px;
}
div.modal {
position: fixed;
top: 100px;
background-color: #ffffff;
border-radius: 3px;
padding: 20px;
box-sizing: border-box;
overflow-y: auto;
z-index: 6;
z-index: 3;
visibility: hidden;
}
@@ -331,7 +258,7 @@ div.modal {
right: 0;
background-color: #000000;
opacity: 0.5;
z-index: 5;
z-index: 2;
visibility: hidden;
}
@@ -344,7 +271,7 @@ p.formError {
display: none;
}
header {
div.header {
display: grid;
grid-template-columns: auto auto;
background-color: #333333;
@@ -354,52 +281,33 @@ header {
color: white;
}
header.small {
div.header.small {
height: 40px;
line-height: 40px;
}
header>p {
div.header>p.header {
line-height: inherit;
text-align: right;
}
header>p>span {
div.header>p.header>span {
padding-left: 6px;
}
header>p>span>a:link, header>p>span>a:visited {
div.header>p.header>span>a:link, div.header>p.header>span>a:visited {
color: inherit;
}
header>p>span:not(:last-child) {
div.header>p.header>span:not(:last-child) {
border-right: solid white 1px;
padding-right: 6px;
}
main {
background-color: #ffffff;
div.main {
padding: 6px 12px;
}
main.full {
position: relative;
width: 100%;
height: calc(100% - 40px);
padding: 0;
}
footer {
background-color: #444444;
padding: 6px 12px;
color: white;
text-align: center;
}
footer>p {
font-size: 13px;
}
div.buttonContainer {
height: 35px;
}
@@ -408,20 +316,6 @@ div.buttonContainer>button {
margin: 0 auto;
}
#cookiesNotice {
position: fixed;
left: 0;
bottom: 0;
right: 0;
margin: 20px;
background-color: #eeeeee;
border: solid #888888 1px;
border-radius: 3px;
padding: 10px;
text-align: center;
z-index: 10;
}
#loading {
position: fixed;
width: 64px;
@@ -430,7 +324,7 @@ div.buttonContainer>button {
left: 50%;
margin-top: -32px;
margin-left: -32px;
z-index: 7;
z-index: 5;
visibility: hidden;
}
@@ -443,128 +337,27 @@ div.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) {
header h1 span {
div.header h1 span {
display: none;
}
footer>p:not(:first-child) {
margin-top: 4px;
}
button, a.button {
padding: 0;
width: 100%;
}
button.marginLeft, a.button.marginLeft {
margin-left: 0;
}
button.marginRight, a.button.marginRight {
margin-right: 0;
}
div.modal {
left: 20px;
right: 20px;
padding-left: 15px;
padding-right: 15px;
}
div.box {
width: initial;
}
.circleControl {
width: 45px;
}
.circleControl .controlItem {
height: 45px;
}
}
@media screen and (min-width: 600px) {
footer>p {
display: inline;
}
footer>p:not(:first-child) {
padding-left: 6px;
}
footer>p:not(:last-child) {
border-right: solid white 1px;
padding-right: 6px;
}
div.modal {
width: 540px;
left: 50%;
margin-left: -270px;
padding-left: 20px;
padding-right: 20px;
}
}
@media screen and (max-height: 399px) {
div.modal {
top: 20px;
max-height: calc(100% - 40px);
padding-top: 10px;
padding-bottom: 10px;
}
.circleControl {
width: 45px;
}
.circleControl .controlItem {
height: 45px;
}
}
@media screen and (min-height: 400px) and (max-height: 499px) {
div.modal {
top: 50px;
max-height: calc(100% - 100px);
padding-top: 15px;
padding-bottom: 15px;
}
}
@media screen and (min-height: 500px) {
div.modal {
top: 75px;
max-height: calc(100% - 150px);
padding-top: 15px;
padding-bottom: 15px;
}
}
+7 -21
View File
@@ -18,16 +18,7 @@ div.mapItem>div.title {
color: white;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
padding: 0 8px;
height: 35px;
line-height: 35px;
}
div.mapItem>div.title>p {
line-height: inherit;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 4px 8px;
}
div.mapItem>div.title>p.title {
@@ -38,16 +29,17 @@ div.mapItem>div.title>p.title {
div.mapItem>div.imgContainer {
width: 100%;
padding-top: 50%;
background-color: #cccccc;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
background: #cccccc;
}
div.mapItem>div.imgContainer>img {
width: 100%;
margin-top: -50%
}
div.mapItem>div.inner {
background-color: #eeeeee;
padding: 10px 8px;
box-sizing: border-box;
}
div.mapItem>div.inner>div.info {
@@ -55,12 +47,6 @@ div.mapItem>div.inner>div.info {
grid-template-columns: auto auto;
}
div.mapItem>div.inner>div.description {
display: flex;
justify-content: center;
align-items: center;
}
div.mapItem>div.inner>div.info>p:nth-child(1) {
text-align: left;
}
-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

-69
View File
@@ -1,69 +0,0 @@
var Account = {
original: null,
countdown: null,
openGoogleAuthenticate: function () {
window.open('/account/googleAuthenticate', 'googleAuthenticate', 'height=600,width=600')
},
authenticatedWithGoogleCallback: function (authenticatedWithGoogleUntil) {
var password = document.getElementsByTagName('form')[0].elements.password;
var button = document.getElementById('authenticateWithGoogleButton');
Account.original = {
type: password.type,
placeholder: password.placeholder,
required: password.required,
disabled: password.disabled
};
password.type = 'text';
password.placeholder = 'Authenticated!'
password.value = '';
password.required = false;
password.disabled = true;
button.disabled = true;
Account.countdown = setInterval(function () {
var timeLeft = Math.ceil((authenticatedWithGoogleUntil.getTime() - new Date().getTime()) / 1000);
if (timeLeft > 30) {
return;
}
if (timeLeft <= 0) {
Account.resetGoogleAuthentication();
return;
}
password.placeholder = 'Authenticated! ' + timeLeft + ' seconds left...';
}, 1000);
},
resetGoogleAuthentication: function () {
if (Account.countdown !== null) {
clearInterval(Account.countdown);
}
var password = document.getElementsByTagName('form')[0].elements.password;
var button = document.getElementById('authenticateWithGoogleButton');
password.type = Account.original.type;
password.placeholder = Account.original.placeholder
password.required = Account.original.required;
password.disabled = Account.original.disabled;
button.disabled = false;
}
};
(function () {
document.getElementById('authenticateWithGoogleButton').onclick = function () {
Account.openGoogleAuthenticate();
};
document.getElementsByTagName('form')[0].onreset = function () {
Account.resetGoogleAuthentication();
};
})();
@@ -1,10 +0,0 @@
(function () {
if (success) {
window.opener.Account.authenticatedWithGoogleCallback(authenticatedWithGoogleUntil);
window.close();
} else {
document.getElementById('closeWindowButton').onclick = function () {
window.close();
}
}
})();
+95 -604
View File
@@ -5,293 +5,49 @@
NUMBER_OF_ROUNDS: 5,
MAX_SCORE: 1000,
mapBounds: null,
multi: { token: null, owner: false },
rounds: [],
scoreSum: 0,
panoId: null,
pov: null,
panorama: null,
map: null,
guessMarker: null,
adaptGuess: false,
googleLink: null,
readyToContinue: true,
timeoutEnd: null,
countdownHandler: null,
MultiConnector: {
connection: null,
reconnectCounter: 0,
connect: function () {
if (Game.MultiConnector.connection && Game.MultiConnector.connection.readyState !== WebSocket.CLOSED) {
return;
}
Game.MultiConnector.connection = new WebSocket((MapGuesser.isSecure ? 'wss' : 'ws') + '://' + multiUrl);
Game.MultiConnector.connection.onopen = function () {
document.getElementById('loading').style.visibility = 'hidden';
Game.MultiConnector.reconnectCounter = 0;
Game.MultiConnector.connection.send(JSON.stringify({ func: 'connect_to_room', args: { roomId: roomId, token: Game.multi.token } }));
};
Game.MultiConnector.connection.onclose = Game.MultiConnector.noConnection;
Game.MultiConnector.connection.onerror = function (event) {
console.error('WebSocket error in Game.MultiConnector:', event);
};
Game.MultiConnector.connection.onmessage = function (message) {
var json;
try {
json = JSON.parse(message.data);
} catch (e) {
console.error('Cannot parse message!');
console.error(message.data);
return;
}
switch (json.type) {
case 'initialize':
Game.MultiConnector.initialize(json.data);
break;
case 'member_joined':
Game.MultiConnector.memberJoined(json.data);
break;
case 'new_round':
Game.MultiConnector.newRound(json.data);
break;
case 'guess':
Game.MultiConnector.guess(json.data);
break;
case 'timeout_changed':
Game.MultiConnector.timeoutChanged(json.data);
break;
case 'end_round':
Game.MultiConnector.endRound(json.data);
break;
}
};
},
noConnection: function () {
if (Game.MultiConnector.reconnectCounter === 2) {
console.error('Could not reconnect WebSocket for Game.MultiConnector...')
}
setTimeout(function () {
Game.MultiConnector.reconnectCounter++;
console.log('Reconnecting WebSocket for Game.MultiConnector... ' + Game.MultiConnector.reconnectCounter);
Game.MultiConnector.connect();
}, 1000 + Math.min(Game.MultiConnector.reconnectCounter * 500, 9000));
},
initialize: function (data) {
Game.readyToContinue = false;
if (data.history.length === 0 && !data.place) {
var div = document.getElementById('players');
for (var i = 0; i < data.members.length; ++i) {
var member = data.members[i];
var p = document.createElement('p');
p.innerHTML = member.userName + (member.me ? ' (me)' : '');
div.appendChild(p);
}
}
for (var i = 0; i < data.history.length; ++i) {
var round = data.history[i];
Game.rounds.push({ position: round.position, guessPosition: round.result.guessPosition, realMarker: null, guessMarkers: [] });
Game.addPositionToResultMap(true);
if (round.result.guessPosition) {
Game.addGuessPositionToResultMap(round.result.guessPosition, null, true);
}
Game.scoreSum += round.result.score;
for (var j = 0; j < round.allResults.length; ++j) {
var result = round.allResults[j];
if (result.guessPosition) {
Game.addGuessPositionToResultMap(result.guessPosition, result, true);
}
}
}
document.getElementById('currentRound').innerHTML = String(Game.rounds.length) + '/' + String(Game.NUMBER_OF_ROUNDS);
document.getElementById('currentScoreSum').innerHTML = String(Game.scoreSum) + '/' + String(Game.rounds.length * Game.MAX_SCORE);
if (data.timeout) {
Game.startCountdown(data.timeout);
}
if (data.place) {
Game.readyToContinue = data.readyToContinue;
Game.panoId = data.place.panoId;
Game.pov = data.place.pov;
document.getElementById('multi').style.visibility = 'hidden';
if (Game.rounds.length === 0 || !Game.rounds[Game.rounds.length - 1].position) {
document.getElementById('panoCover').style.visibility = 'hidden';
Game.startNewRound();
} else {
Game.loadPano(Game.panoId, Game.pov);
Game.showResultFromHistory(data.history[data.history.length - 1].result);
}
}
document.getElementById('loading').style.visibility = 'hidden';
},
memberJoined: function (data) {
var div = document.getElementById('players');
var p = document.createElement('p');
p.innerHTML = data.userName;
div.appendChild(p);
},
newRound: function (data) {
if (Game.rounds.length === Game.NUMBER_OF_ROUNDS) {
Game.reset();
}
Game.readyToContinue = false;
Game.panoId = data.place.panoId;
Game.pov = data.place.pov;
document.getElementById('multi').style.visibility = 'hidden';
Game.resetRound();
Game.startNewRound();
Game.startCountdown(data.timeout);
},
guess: function (data) {
var resultBounds = Game.map.getBounds();
Game.addGuessPositionToResultMap(data.guessPosition, data);
resultBounds.extend(data.guessPosition);
Game.map.fitBounds(resultBounds);
},
timeoutChanged: function (data) {
Game.startCountdown(data.timeout);
},
endRound: function (data) {
Game.readyToContinue = true;
Game.startCountdown(0);
if (Game.rounds[Game.rounds.length - 1].guessPosition || Game.rounds[Game.rounds.length - 1].position) {
if (Game.multi.owner) {
document.getElementById('continueButton').disabled = false;
document.getElementById('startNewGameButton').disabled = false;
}
return;
}
document.getElementById('guessButton').disabled = true;
document.getElementById('panoCover').style.visibility = 'visible';
Game.receiveResult(data.position, data.result.guessPosition, { distance: data.result.distance, score: data.result.score }, data.allResults);
}
},
prepare: function () {
var data = new FormData();
var userNames;
if (roomId) {
var userNames = localStorage.userNames ? JSON.parse(localStorage.userNames) : {};
if (!userNames.hasOwnProperty(roomId)) {
userNames[roomId] = prompt('Your name: ');
localStorage.userNames = JSON.stringify(userNames);
}
data.append('userName', userNames[roomId]);
}
document.getElementById('loading').style.visibility = 'visible';
var url = roomId ? '/multiGame/' + roomId + '/prepare.json' : '/game/' + mapId + '/prepare.json';
MapGuesser.httpRequest('POST', url, function () {
document.getElementById('loading').style.visibility = 'hidden';
if (this.response.error) {
Game.handleErrorResponse(this.response.error);
return;
}
document.getElementById('mapName').innerHTML = this.response.mapName;
Game.mapBounds = this.response.bounds;
Game.initialize();
if (roomId) {
Game.multi.token = this.response.token;
Game.multi.owner = this.response.owner;
document.getElementById('multi').style.visibility = 'visible';
if (Game.multi.owner) {
document.getElementById('startMultiGameButton').style.display = 'block';
}
document.getElementById('loading').style.visibility = 'visible';
Game.MultiConnector.connect();
}
}, data);
},
initialize: function () {
document.getElementById('panoCover').style.visibility = 'visible';
document.getElementById('loading').style.visibility = 'visible';
document.getElementById('guessCover').style.visibility = 'visible';
document.getElementById('currentRound').innerHTML = '1/' + String(Game.NUMBER_OF_ROUNDS);
document.getElementById('currentScoreSum').innerHTML = '0/0';
Game.map.setOptions({
draggableCursor: 'crosshair'
});
Game.map.fitBounds(Game.mapBounds);
Game.map.fitBounds(mapBounds);
if (roomId) {
// if it is multiplayer mode, data is sent via WS
return;
}
document.getElementById('loading').style.visibility = 'visible';
MapGuesser.httpRequest('POST', '/game/' + mapId + '/initialData.json', function () {
MapGuesser.httpRequest('GET', '/game/' + mapId + '/newPlace.json', function () {
document.getElementById('loading').style.visibility = 'hidden';
document.getElementById('panoCover').style.visibility = 'hidden';
document.getElementById('guessCover').style.visibility = 'hidden';
if (this.response.error) {
Game.handleErrorResponse(this.response.error);
//TODO: handle this error
return;
}
Game.panoId = this.response.place.panoId;
Game.pov = this.response.place.pov;
Game.panoId = this.response.panoId;
for (var i = 0; i < this.response.history.length; ++i) {
var round = this.response.history[i];
Game.rounds.push({ position: round.position, guessPosition: round.result.guessPosition, realMarker: null, guessMarkers: [] });
Game.addPositionToResultMap(true);
Game.addGuessPositionToResultMap(round.result.guessPosition, null, true);
Game.scoreSum += round.result.score;
if (this.response.history) {
for (var i = 0; i < this.response.history.length; ++i) {
var round = this.response.history[i];
Game.rounds.push({ position: round.position, guessPosition: round.guessPosition, realMarker: null, guessMarker: null, line: null });
Game.addRealGuessPair(round.position, round.guessPosition, true);
Game.scoreSum += round.score;
}
document.getElementById('currentRound').innerHTML = String(Game.rounds.length) + '/' + String(Game.NUMBER_OF_ROUNDS);
document.getElementById('currentScoreSum').innerHTML = String(Game.scoreSum) + '/' + String(Game.rounds.length * Game.MAX_SCORE);
}
document.getElementById('currentRound').innerHTML = String(Game.rounds.length) + '/' + String(Game.NUMBER_OF_ROUNDS);
document.getElementById('currentScoreSum').innerHTML = String(Game.scoreSum) + '/' + String(Game.rounds.length * Game.MAX_SCORE);
Game.startNewRound();
});
},
@@ -305,16 +61,10 @@
for (var i = 0; i < Game.rounds.length; ++i) {
var round = Game.rounds[i];
if (round.realMarker) {
if (round.realMarker && round.guessMarker && round.line) {
round.realMarker.setMap(null);
}
for (var j = 0; j < round.guessMarkers.length; ++j) {
var guessMarker = round.guessMarkers[j];
guessMarker.marker.setMap(null);
guessMarker.line.setMap(null);
if (guessMarker.info) {
guessMarker.info.close();
}
round.guessMarker.setMap(null);
round.line.setMap(null);
}
}
@@ -324,21 +74,16 @@
var distanceInfo = document.getElementById('distanceInfo');
distanceInfo.children[0].style.display = null;
distanceInfo.children[1].style.display = null;
distanceInfo.children[2].style.display = null;
var scoreInfo = document.getElementById('scoreInfo');
scoreInfo.children[0].style.display = null;
scoreInfo.children[1].style.display = null;
document.getElementById('continueButton').style.display = null;
document.getElementById('showSummaryButton').style.display = null;
document.getElementById('startNewGameButton').style.display = null;
document.getElementById('showGuessButton').style.visibility = null;
document.getElementById('guess').style.visibility = null;
document.getElementById('guess').classList.remove('result');
// needs to be set visible after the show guess map hid it in mobile view
document.getElementById("navigation").style.visibility = 'visible';
Game.initialize();
},
@@ -349,187 +94,52 @@
var lastRound = Game.rounds[Game.rounds.length - 1];
lastRound.realMarker.setVisible(false);
for (var i = 0; i < lastRound.guessMarkers.length; ++i) {
var guessMarker = lastRound.guessMarkers[i];
guessMarker.marker.setVisible(false);
guessMarker.line.setVisible(false);
if (guessMarker.info) {
guessMarker.info.close();
}
}
lastRound.guessMarker.setVisible(false);
lastRound.line.setVisible(false);
}
document.getElementById('panoCover').style.visibility = 'hidden';
document.getElementById('guessCover').style.visibility = 'hidden';
document.getElementById('showGuessButton').style.visibility = null;
document.getElementById('guess').style.visibility = null;
document.getElementById('guess').classList.remove('result');
// needs to be set visible after the show guess map hid it in mobile view
document.getElementById("navigation").style.visibility = 'visible';
document.getElementById('guess').classList.remove('result')
Game.map.setOptions({
draggableCursor: 'crosshair'
});
Game.map.fitBounds(Game.mapBounds);
if (roomId) {
// if it is multiplayer mode, data is sent via WS
return;
}
Game.map.fitBounds(mapBounds);
Game.startNewRound();
},
startNewRound: function () {
Game.rounds.push({ position: null, guessPosition: null, realMarker: null, guessMarkers: [] });
Game.rounds.push({ position: null, guessPosition: null, realMarker: null, guessMarker: null, line: null });
document.getElementById('currentRound').innerHTML = String(Game.rounds.length) + '/' + String(Game.NUMBER_OF_ROUNDS);
Game.loadPano(Game.panoId, Game.pov);
// update the compass
const heading = Game.panorama.getPov().heading;
document.getElementById("compass").style.transform = "translateY(-50%) rotate(" + heading + "deg)";
Game.loadPano(Game.panoId);
},
handleErrorResponse: function (error) {
switch (error) {
case 'no_session_found':
MapGuesser.showModalWithContent('Error', 'Your session is invalid!', [{
type: 'button',
classNames: [],
text: 'Restart game',
onclick: function () {
window.location.reload();
}
}]);
break;
// for the time being we only handle the "no_session_found" error and reset the game
case 'game_already_started':
MapGuesser.showModalWithContent('Error', 'This game is already started, you cannot join.');
break;
MapGuesser.httpRequest('GET', '/game/' + mapId + '/json', function () {
mapBounds = this.response.bounds;
case 'game_not_found':
MapGuesser.showModalWithContent('Error', 'The game room was not found by this ID. Please check the link.');
break;
default:
MapGuesser.showModalWithContent('Error', 'Error code: \'' + error + '\'');
break
}
Game.reset();
});
},
loadPano: function (panoId, pov) {
loadPano: function (panoId) {
if (Game.adaptGuess) {
document.getElementById('guess').classList.add('adapt');
}
Game.panorama.setPov({ heading: pov.heading, pitch: pov.pitch });
Game.panorama.setZoom(pov.zoom);
Game.panorama.setPov({ heading: 0, pitch: 0 });
Game.panorama.setZoom(0);
Game.panorama.setPano(panoId);
},
receiveResult: function (position, guessPosition, result, allResults) {
Game.scoreSum += result.score;
document.getElementById('currentScoreSum').innerHTML = String(Game.scoreSum) + '/' + String(Game.rounds.length * Game.MAX_SCORE);
var resultBounds = new google.maps.LatLngBounds();
Game.rounds[Game.rounds.length - 1].position = position;
Game.addPositionToResultMap();
resultBounds.extend(position);
if (guessPosition) {
Game.addGuessPositionToResultMap(guessPosition);
resultBounds.extend(guessPosition);
}
if (allResults) {
for (var i = 0; i < allResults.length; ++i) {
var currentResult = allResults[i];
if (currentResult.guessPosition) {
Game.addGuessPositionToResultMap(currentResult.guessPosition, currentResult);
resultBounds.extend(currentResult.guessPosition);
}
}
}
Game.showResultMap(result, resultBounds);
},
showResultFromHistory: function (result) {
var round = Game.rounds[Game.rounds.length - 1];
var resultBounds = new google.maps.LatLngBounds();
round.realMarker.setVisible(true);
resultBounds.extend(round.position);
for (var j = 0; j < round.guessMarkers.length; ++j) {
var guessMarker = round.guessMarkers[j];
guessMarker.marker.setVisible(true);
guessMarker.line.setVisible(true);
resultBounds.extend(guessMarker.marker.getPosition());
}
Game.showResultMap(result, resultBounds);
},
showResultMap: function (result, resultBounds) {
// TODO: refactor - it is necessary for mobile
if (window.getComputedStyle(document.getElementById('guess')).visibility === 'hidden') {
document.getElementById('showGuessButton').click();
}
if (Game.adaptGuess) {
document.getElementById('guess').classList.remove('adapt');
}
if (Game.guessMarker) {
Game.guessMarker.setMap(null);
Game.guessMarker = null;
}
document.getElementById('guess').classList.add('result');
Game.map.setOptions({
draggableCursor: 'grab'
});
Game.map.fitBounds(resultBounds);
var distanceInfo = document.getElementById('distanceInfo');
if (result.distance === null) {
distanceInfo.children[0].style.display = 'none';
distanceInfo.children[1].style.display = 'block';
} else {
distanceInfo.children[0].style.display = 'block';
distanceInfo.children[1].style.display = 'none';
document.getElementById('distance').innerHTML = Util.printDistanceForHuman(result.distance);
}
document.getElementById('score').innerHTML = result.score;
var scoreBarProperties = Game.calculateScoreBarProperties(result.score, Game.MAX_SCORE);
var scoreBar = document.getElementById('scoreBar');
scoreBar.style.backgroundColor = scoreBarProperties.backgroundColor;
scoreBar.style.width = scoreBarProperties.width;
if (Game.rounds.length === Game.NUMBER_OF_ROUNDS) {
document.getElementById('continueButton').style.display = 'none';
document.getElementById('showSummaryButton').style.display = 'block';
} else if (roomId) {
if (Game.multi.owner) {
if (!Game.readyToContinue) {
document.getElementById('continueButton').disabled = true;
}
} else {
document.getElementById('continueButton').style.display = 'none';
}
}
},
guess: function () {
evaluateGuess: function () {
if (!Game.guessMarker) {
return;
}
@@ -538,34 +148,62 @@
Game.rounds[Game.rounds.length - 1].guessPosition = guessPosition;
document.getElementById('guessButton').disabled = true;
document.getElementById('panoCover').style.visibility = 'visible';
if (Game.adaptGuess) {
document.getElementById('guess').classList.remove('adapt');
}
document.getElementById('loading').style.visibility = 'visible';
document.getElementById('guessCover').style.visibility = 'visible';
var data = new FormData();
data.append('lat', String(guessPosition.lat));
data.append('lng', String(guessPosition.lng));
document.getElementById('loading').style.visibility = 'visible';
var url = roomId ? '/multiGame/' + roomId + '/guess.json' : '/game/' + mapId + '/guess.json';
MapGuesser.httpRequest('POST', url, function () {
document.getElementById('loading').style.visibility = 'hidden';
MapGuesser.httpRequest('POST', '/game/' + mapId + '/guess.json', function () {
if (this.response.error) {
Game.handleErrorResponse(this.response.error);
return;
}
Game.receiveResult(this.response.position, guessPosition, this.response.result, this.response.allResults);
Game.guessMarker.setMap(null);
Game.guessMarker = null;
if (this.response.place) {
Game.panoId = this.response.place.panoId;
Game.pov = this.response.place.pov;
document.getElementById('loading').style.visibility = 'hidden';
document.getElementById('guess').classList.add('result');
Game.scoreSum += this.response.result.score;
document.getElementById('currentScoreSum').innerHTML = String(Game.scoreSum) + '/' + String(Game.rounds.length * Game.MAX_SCORE);
Game.rounds[Game.rounds.length - 1].position = this.response.result.position;
Game.addRealGuessPair(this.response.result.position, guessPosition);
var resultBounds = new google.maps.LatLngBounds();
resultBounds.extend(this.response.result.position);
resultBounds.extend(guessPosition);
Game.map.setOptions({
draggableCursor: 'grab'
});
Game.map.fitBounds(resultBounds);
document.getElementById('distance').innerHTML = Util.printDistanceForHuman(this.response.result.distance);
document.getElementById('score').innerHTML = this.response.result.score;
var scoreBarProperties = Game.calculateScoreBarProperties(this.response.result.score, Game.MAX_SCORE);
var scoreBar = document.getElementById('scoreBar');
scoreBar.style.backgroundColor = scoreBarProperties.backgroundColor;
scoreBar.style.width = scoreBarProperties.width;
if (Game.rounds.length === Game.NUMBER_OF_ROUNDS) {
document.getElementById('continueButton').style.display = 'none';
document.getElementById('showSummaryButton').style.display = 'block';
}
Game.panoId = this.response.panoId;
}, data);
},
addPositionToResultMap: function (hidden) {
addRealGuessPair: function (position, guessPosition, hidden) {
var round = Game.rounds[Game.rounds.length - 1];
var position = round.position;
round.realMarker = new google.maps.Marker({
map: Game.map,
@@ -586,25 +224,16 @@
round.realMarker.addListener('click', function () {
window.open('https://www.google.com/maps/search/?api=1&query=' + this.getPosition().toUrlValue(), '_blank');
});
},
addGuessPositionToResultMap: function (guessPosition, result, hidden) {
var round = Game.rounds[Game.rounds.length - 1];
var position = round.position;
var guessMarker = { marker: null, line: null, info: null };
var markerSvg = result ? 'marker-gray-empty.svg' : 'marker-blue-empty.svg';
var markerLabel = result ? result.userName.charAt(0).toUpperCase() : '?';
guessMarker.marker = new google.maps.Marker({
round.guessMarker = new google.maps.Marker({
map: Game.map,
visible: !hidden,
position: guessPosition,
zIndex: Game.rounds.length,
clickable: !!result,
clickable: false,
draggable: false,
icon: {
url: STATIC_ROOT + '/img/markers/' + markerSvg + '?rev=' + REVISION,
url: STATIC_ROOT + '/img/markers/marker-gray-empty.svg?rev=' + REVISION,
size: new google.maps.Size(24, 32),
scaledSize: new google.maps.Size(24, 32),
anchor: new google.maps.Point(12, 32),
@@ -615,11 +244,11 @@
fontFamily: 'Roboto',
fontSize: '16px',
fontWeight: '500',
text: markerLabel
text: '?'
}
});
guessMarker.line = new google.maps.Polyline({
round.line = new google.maps.Polyline({
map: Game.map,
visible: !hidden,
path: [
@@ -642,19 +271,6 @@
draggable: false,
editable: false
});
if (result) {
guessMarker.info = new google.maps.InfoWindow({
content: '<p class="small bold">' + result.userName + '</p>' +
'<p class="small">' + Util.printDistanceForHuman(result.distance) + ' | ' + result.score + ' points</p>',
});
guessMarker.marker.addListener('click', function () {
guessMarker.info.open(Game.map, this);
});
}
round.guessMarkers.push(guessMarker);
},
calculateScoreBarProperties: function (score, maxScore) {
@@ -675,19 +291,12 @@
showSummary: function () {
var distanceInfo = document.getElementById('distanceInfo');
distanceInfo.children[0].style.display = 'none';
distanceInfo.children[1].style.display = 'none';
distanceInfo.children[2].style.display = 'block';
distanceInfo.children[1].style.display = 'block';
var scoreInfo = document.getElementById('scoreInfo');
scoreInfo.children[0].style.display = 'none';
scoreInfo.children[1].style.display = 'block';
document.getElementById('showSummaryButton').style.display = null;
if (!roomId || Game.multi.owner) {
document.getElementById('startNewGameButton').style.display = 'block';
if (!Game.readyToContinue) {
document.getElementById('startNewGameButton').disabled = true;
}
}
document.getElementById('startNewGameButton').style.display = 'block';
var resultBounds = new google.maps.LatLngBounds();
@@ -708,17 +317,12 @@
fontWeight: '500',
text: String(i + 1)
});
round.realMarker.setVisible(true);
round.guessMarker.setVisible(true);
round.line.setVisible(true);
resultBounds.extend(round.position);
for (var j = 0; j < round.guessMarkers.length; ++j) {
var guessMarker = round.guessMarkers[j];
guessMarker.marker.setVisible(true);
guessMarker.line.setVisible(true);
resultBounds.extend(guessMarker.marker.getPosition());
}
resultBounds.extend(round.guessPosition);
}
Game.map.fitBounds(resultBounds);
@@ -749,52 +353,6 @@
Game.googleLink.href = 'https://maps.google.com/maps';
}
}, 1);
},
startCountdown: function (timeout) {
if (Game.countdownHandler) {
clearInterval(Game.countdownHandler);
}
Game.countdownElement = document.getElementById('countdown');
Game.countdownTimeElement = document.getElementById('countdownTime');
Game.setCountdownTime(Math.round(timeout / 1000));
if (timeout <= 0) {
return;
}
Game.timeoutEnd = new Date(new Date().getTime() + timeout);
Game.countdownHandler = setInterval(function () {
var timeLeft = Math.round((Game.timeoutEnd - new Date()) / 1000);
Game.setCountdownTime(timeLeft);
if (timeLeft <= 0) {
document.getElementById('panoCover').style.visibility = 'visible';
clearInterval(Game.countdownHandler);
}
}, 1000);
},
setCountdownTime: function (time) {
if (time <= 0) {
Game.countdownElement.style.visibility = 'hidden';
return;
}
if (time <= 15) {
Game.countdownElement.className = 'red';
} else if (time <= 30) {
Game.countdownElement.className = 'yellow';
} else {
Game.countdownElement.className = '';
}
Game.countdownElement.style.visibility = 'visible';
Game.countdownTimeElement.innerHTML = time;
}
};
@@ -812,8 +370,6 @@
}
};
MapGuesser.sessionAvailableHooks.reinitializeGame = Game.prepare;
if (!('ontouchstart' in document.documentElement)) {
Game.adaptGuess = true;
}
@@ -825,7 +381,7 @@
});
Game.map.addListener('click', function (e) {
if (Game.rounds[Game.rounds.length - 1].guessPosition || Game.rounds[Game.rounds.length - 1].position) {
if (Game.rounds[Game.rounds.length - 1].guessPosition) {
return;
}
@@ -840,7 +396,7 @@
clickable: false,
draggable: true,
icon: {
url: STATIC_ROOT + '/img/markers/marker-blue-empty.svg?rev=' + REVISION,
url: STATIC_ROOT + '/img/markers/marker-gray-empty.svg?rev=' + REVISION,
size: new google.maps.Size(24, 32),
scaledSize: new google.maps.Size(24, 32),
anchor: new google.maps.Point(12, 32),
@@ -871,49 +427,26 @@
Game.panorama.addListener('pov_changed', function () {
Game.rewriteGoogleLink();
const heading = Game.panorama.getPov().heading;
document.getElementById("compass").style.transform = "translateY(-50%) rotate(" + heading + "deg)";
});
if (COOKIES_CONSENT) {
Game.prepare();
}
Game.initialize();
document.getElementById('showGuessButton').onclick = function () {
this.style.visibility = 'hidden';
document.getElementById('guess').style.visibility = 'visible';
document.getElementById('navigation').style.visibility = 'hidden';
}
document.getElementById('closeGuessButton').onclick = function () {
document.getElementById('showGuessButton').style.visibility = null;
document.getElementById('guess').style.visibility = null;
document.getElementById('navigation').style.visibility = 'visible';
}
document.getElementById('guessButton').onclick = function () {
Game.guess();
Game.evaluateGuess();
}
document.getElementById('continueButton').onclick = function () {
if (roomId) {
if (!Game.multi.owner || !Game.readyToContinue) {
return;
}
document.getElementById('loading').style.visibility = 'visible';
MapGuesser.httpRequest('POST', '/multiGame/' + roomId + '/nextRound.json', function () {
document.getElementById('loading').style.visibility = 'hidden';
if (this.response.error) {
Game.handleErrorResponse(this.response.error);
return;
}
});
} else {
Game.resetRound();
}
Game.resetRound();
}
document.getElementById('showSummaryButton').onclick = function () {
@@ -921,48 +454,6 @@
}
document.getElementById('startNewGameButton').onclick = function () {
if (roomId) {
if (!Game.multi.owner) {
return;
}
document.getElementById('loading').style.visibility = 'visible';
MapGuesser.httpRequest('POST', '/multiGame/' + roomId + '/initialData.json', function () {
document.getElementById('loading').style.visibility = 'hidden';
if (this.response.error) {
Game.handleErrorResponse(this.response.error);
return;
}
});
} else {
Game.reset();
}
}
document.getElementById('startMultiGameButton').onclick = function () {
if (!roomId || !Game.multi.owner) {
return;
}
document.getElementById('multi').style.visibility = 'hidden';
document.getElementById('loading').style.visibility = 'visible';
MapGuesser.httpRequest('POST', '/multiGame/' + roomId + '/initialData.json', function () {
document.getElementById('loading').style.visibility = 'hidden';
if (this.response.error) {
Game.handleErrorResponse(this.response.error);
return;
}
});
}
document.getElementById('returnToStart').onclick = function () {
Game.loadPano(Game.panoId, Game.pov);
}
document.getElementById('compassContainer').onclick = function () {
Game.panorama.setPov({ heading: 0, pitch: Game.panorama.getPov().pitch });
Game.reset();
}
})();
+40
View File
@@ -0,0 +1,40 @@
(function () {
var form = document.getElementById('loginForm');
form.onsubmit = function (e) {
document.getElementById('loading').style.visibility = 'visible';
e.preventDefault();
var formData = new FormData(form);
MapGuesser.httpRequest('POST', form.action, function () {
if (this.response.error) {
var errorText;
switch (this.response.error) {
case 'user_not_found':
errorText = 'No user found with the given email address. You can <a href="/signup" title="Sign up">sign up here</a>!';
break;
case 'user_not_active':
errorText = 'User found with the given email address, but the account is not activated. Please check your email and click on the activation link!';
break;
case 'password_not_match':
errorText = 'The given password is wrong.'
break;
}
document.getElementById('loading').style.visibility = 'hidden';
var loginFormError = document.getElementById('loginFormError');
loginFormError.style.display = 'block';
loginFormError.innerHTML = errorText;
form.elements.email.select();
return;
}
window.location.replace('/');
}, formData);
};
})();
-9
View File
@@ -1,9 +0,0 @@
(function () {
document.getElementById('cancelGoogleSignupButton').onclick = function () {
document.getElementById('loading').style.visibility = 'visible';
MapGuesser.httpRequest('POST', '/signup/google/reset', function () {
window.location.replace('/signup');
});
};
})();
-12
View File
@@ -1,12 +0,0 @@
(function () {
var resetSignupButton = document.getElementById('resetSignupButton');
if (resetSignupButton) {
resetSignupButton.onclick = function () {
document.getElementById('loading').style.visibility = 'visible';
MapGuesser.httpRequest('POST', '/signup/reset', function () {
window.location.reload();
});
};
}
})();
+89 -371
View File
@@ -7,11 +7,13 @@
description: null
},
map: null,
markers: null,
panorama: null,
selectedMarker: null,
added: {},
edited: {},
deleted: {},
saved: true,
editMetadata: function () {
var form = document.getElementById('metadataForm');
@@ -23,6 +25,7 @@
MapGuesser.hideModal();
MapEditor.saved = false;
document.getElementById('saveButton').disabled = false;
},
@@ -86,7 +89,7 @@
sv.getPanorama({
location: location,
preference: google.maps.StreetViewPreference.NEAREST,
radius: MapEditor.map.getSearchRadius(location),
radius: 100,
source: canBeIndoor ? google.maps.StreetViewSource.DEFAULT : google.maps.StreetViewSource.OUTDOOR
}, function (data, status) {
var panoLocationData = status === google.maps.StreetViewStatus.OK ? data.location : null;
@@ -117,13 +120,16 @@
MapEditor.resetSelected();
MapEditor.selectedMarker = marker;
MapEditor.map.resize();
MapEditor.map.invalidateSize(true);
MapEditor.map.panTo(marker.getLatLng());
MapEditor.panorama.setVisible(false);
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';
@@ -161,13 +167,13 @@
var placeId = MapEditor.selectedMarker.placeId
if (places[placeId].id && !del) {
MapEditor.map.changeMarkerIcon(
MapEditor.selectedMarker,
places[placeId].noPano ? MapEditor.map.iconCollection.iconRed : MapEditor.map.iconCollection.iconGreen
);
MapEditor.map.removeLayer(MapEditor.selectedMarker);
MapEditor.markers.addLayer(MapEditor.selectedMarker);
MapEditor.selectedMarker.setIcon(places[placeId].noPano ? IconCollection.iconRed : IconCollection.iconGreen);
MapEditor.selectedMarker.setZIndexOffset(1000);
} else {
delete places[placeId];
MapEditor.map.removeMarker(MapEditor.selectedMarker);
MapEditor.map.removeLayer(MapEditor.selectedMarker);
}
document.getElementById('deleteButton').style.display = 'none';
@@ -206,6 +212,7 @@
MapEditor.selectedMarker.setLatLng({ lat: places[placeId].lat, lng: places[placeId].lng });
MapEditor.saved = false;
document.getElementById('saveButton').disabled = false;
},
@@ -219,7 +226,7 @@
MapEditor.resetSelected(del);
MapEditor.selectedMarker = null;
MapEditor.map.resize();
MapEditor.map.invalidateSize(true);
},
deletePlace: function () {
@@ -235,11 +242,11 @@
delete MapEditor.added[placeId];
delete MapEditor.edited[placeId];
delete places[placeId];
document.getElementById('added').innerHTML = String(Object.keys(MapEditor.added).length);
document.getElementById('edited').innerHTML = String(Object.keys(MapEditor.edited).length);
MapEditor.saved = false;
document.getElementById('saveButton').disabled = false;
},
@@ -297,6 +304,7 @@
document.getElementById('edited').innerHTML = '0';
document.getElementById('deleted').innerHTML = '0';
MapEditor.saved = true;
document.getElementById('saveButton').disabled = true;
}, data);
},
@@ -310,346 +318,91 @@
}
};
var IconCollection = {
iconGreen: L.icon({
iconUrl: STATIC_ROOT + '/img/markers/marker-green.svg?rev' + REVISION,
iconSize: [24, 32],
iconAnchor: [12, 32]
}),
iconRed: L.icon({
iconUrl: STATIC_ROOT + '/img/markers/marker-red.svg?rev=' + REVISION,
iconSize: [24, 32],
iconAnchor: [12, 32]
}),
iconBlue: L.icon({
iconUrl: STATIC_ROOT + '/img/markers/marker-blue.svg?rev=' + REVISION,
iconSize: [24, 32],
iconAnchor: [12, 32]
}),
};
var Util = {
getHighResData: function () {
if (window.devicePixelRatio >= 4) {
return { ppi: 320, tileSize: 128, zoomOffset: 1, minZoom: 0, maxZoom: 18 };
return { ppi: 320, tileSize: 128, zoomOffset: 1 };
} else if (window.devicePixelRatio >= 2) {
return { ppi: 250, tileSize: 256, zoomOffset: 0, minZoom: 1, maxZoom: 19 };
return { ppi: 250, tileSize: 256, zoomOffset: 0 };
} else {
return { ppi: 72, tileSize: 512, zoomOffset: -1, minZoom: 2, maxZoom: 20 };
return { ppi: 72, tileSize: 512, zoomOffset: -1 };
}
},
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({
iconUrl: STATIC_ROOT + '/img/markers/marker-green.svg?rev' + REVISION,
iconSize: [24, 32],
iconAnchor: [12, 32]
}),
iconRed: L.icon({
iconUrl: STATIC_ROOT + '/img/markers/marker-red.svg?rev=' + REVISION,
iconSize: [24, 32],
iconAnchor: [12, 32]
}),
iconBlue: L.icon({
iconUrl: STATIC_ROOT + '/img/markers/marker-blue.svg?rev=' + REVISION,
iconSize: [24, 32],
iconAnchor: [12, 32]
})
},
MapEditor.map = L.map('map', {
zoomControl: false
});
init: function (divId, places) {
document.getElementById(divId).style.display = "block";
if (!LMapWrapper.map) {
LMapWrapper.divId = divId;
LMapWrapper.map = L.map(LMapWrapper.divId, {
center: { lat: 0., lng: 0. },
zoom: 2
});
LMapWrapper.map.on('click', function (e) {
LMapWrapper.placeMarker(e.latlng);
});
var highResData = Util.getHighResData();
L.tileLayer(tileUrl, {
attribution: tileAttribution,
subdomains: tileSubdomains,
ppi: highResData.ppi,
tileSize: highResData.tileSize,
zoomOffset: highResData.zoomOffset,
minZoom: highResData.minZoom,
maxZoom: highResData.maxZoom
}).addTo(LMapWrapper.map);
if (mapId) {
LMapWrapper.map.fitBounds(L.latLngBounds({ lat: mapBounds.south, lng: mapBounds.west }, { lat: mapBounds.north, lng: mapBounds.east }));
}
}
LMapWrapper.loadMarkers(places);
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
});
} else {
LMapWrapper.markers.clearLayers();
}
for (var placeId in places) {
if (!places.hasOwnProperty(placeId)) {
continue;
}
var place = places[placeId];
var marker = L.marker({ lat: place.lat, lng: place.lng }, {
icon: place.noPano ? LMapWrapper.iconCollection.iconRed : LMapWrapper.iconCollection.iconGreen,
zIndexOffset: 1000
})
.addTo(LMapWrapper.markers)
.on('click', function () {
MapEditor.select(this);
});
marker.placeId = placeId;
}
LMapWrapper.map.addLayer(LMapWrapper.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.map.on('click', function (e) {
var marker = L.marker(e.latlng, {
icon: IconCollection.iconBlue,
zIndexOffset: 2000
})
.addTo(MapEditor.map)
.on('click', function () {
MapEditor.select(this);
});
GMapWrapper.markers.addMarker(marker);
MapEditor.select(marker);
});
MapEditor.select(marker);
},
var highResData = Util.getHighResData();
panTo: function (latLng) {
GMapWrapper.map.panTo(latLng);
},
L.tileLayer(tileUrl, {
attribution: tileAttribution,
subdomains: '1234',
ppi: highResData.ppi,
tileSize: highResData.tileSize,
zoomOffset: highResData.zoomOffset,
minZoom: 2,
maxZoom: 20
}).addTo(MapEditor.map);
resize: function () {
google.maps.event.trigger(GMapWrapper.map, 'resize');
},
MapEditor.map.fitBounds(L.latLngBounds({ lat: mapBounds.south, lng: mapBounds.west }, { lat: mapBounds.north, lng: mapBounds.east }));
changeMarkerIcon: function (marker, icon) {
marker.setIcon(icon);
},
MapEditor.markers = L.markerClusterGroup({
maxClusterRadius: 50
});
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;
for (var placeId in places) {
if (!places.hasOwnProperty(placeId)) {
continue;
}
};
// initialize content of #map with google maps
MapEditor.map = GMapWrapper;
MapEditor.map.init('gmap', places);
var place = places[placeId];
var marker = L.marker({ lat: place.lat, lng: place.lng }, {
icon: place.noPano ? IconCollection.iconRed : IconCollection.iconGreen,
zIndexOffset: 1000
})
.addTo(MapEditor.markers)
.on('click', function () {
MapEditor.select(this);
});
marker.placeId = place.id;
}
MapEditor.map.addLayer(MapEditor.markers);
MapEditor.panorama = new google.maps.StreetViewPanorama(document.getElementById('panorama'), {
// switch off fullscreenControl because positioning doesn't work
@@ -693,47 +446,12 @@
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);
window.addEventListener('beforeunload', function (e) {
if (MapEditor.saved) {
return;
}
};
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();
}
e.preventDefault();
e.returnValue = '';
});
})();
+4 -155
View File
@@ -1,59 +1,12 @@
var MapGuesser = {
isSecure: window.location.protocol === 'https:',
cookiesAgreed: false,
sessionAvailableHooks: {},
initGoogleAnalitics: function () {
if (typeof GOOGLE_ANALITICS_ID === 'undefined') {
return;
}
// Global site tag (gtag.js) - Google Analytics
var script = document.createElement('script');
script.src = 'https://www.googletagmanager.com/gtag/js?id=' + GOOGLE_ANALITICS_ID;
script.async = true;
document.head.appendChild(script);
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', GOOGLE_ANALITICS_ID);
},
agreeCookies: function () {
if (MapGuesser.cookiesAgreed) {
return;
}
var expirationDate = new Date(new Date().getTime() + 20 * 365 * 24 * 60 * 60 * 1000).toUTCString();
document.cookie = 'COOKIES_CONSENT=1; expires=' + expirationDate + '; path=/';
MapGuesser.initGoogleAnalitics();
MapGuesser.httpRequest('GET', '/startSession.json', function () {
ANTI_CSRF_TOKEN = this.response.antiCsrfToken;
for (var hookId in MapGuesser.sessionAvailableHooks) {
if (!MapGuesser.sessionAvailableHooks.hasOwnProperty(hookId)) {
continue;
}
MapGuesser.sessionAvailableHooks[hookId]();
}
});
MapGuesser.cookiesAgreed = true;
},
httpRequest: function (method, url, callback, data) {
var xhr = new XMLHttpRequest();
xhr.responseType = 'json';
xhr.onload = callback;
xhr.open(method, url, true);
xhr.responseType = 'json';
if (method === 'POST') {
if (typeof data === 'undefined') {
data = new FormData();
@@ -67,52 +20,6 @@ var MapGuesser = {
}
},
setOnsubmitForForm: function (form) {
form.onsubmit = function (e) {
e.preventDefault();
document.getElementById('loading').style.visibility = 'visible';
var formData = new FormData(form);
var formError = form.getElementsByClassName('formError')[0];
var pageLeaveOnSuccess = form.dataset.redirectOnSuccess || form.dataset.reloadOnSuccess;
MapGuesser.httpRequest('POST', form.action, function () {
if (!pageLeaveOnSuccess) {
document.getElementById('loading').style.visibility = 'hidden';
}
if (this.response.error) {
if (pageLeaveOnSuccess) {
document.getElementById('loading').style.visibility = 'hidden';
}
formError.style.display = 'block';
formError.innerHTML = this.response.error.errorText;
return;
}
if (this.response.redirect) {
window.location.replace(this.response.redirect.target);
return;
}
if (!pageLeaveOnSuccess) {
formError.style.display = 'none';
form.reset();
} else {
if (form.dataset.redirectOnSuccess) {
window.location.replace(form.dataset.redirectOnSuccess);
} else if (form.dataset.reloadOnSuccess) {
window.location.reload();
}
}
}, formData);
}
},
showModal: function (id) {
document.getElementById(id).style.visibility = 'visible';
document.getElementById('cover').style.visibility = 'visible';
@@ -183,35 +90,11 @@ var MapGuesser = {
document.getElementById('cover').style.visibility = 'hidden';
},
observeInput: function (input, buttonToToggle) {
toggleDisableOnChange: function (input, button) {
if (input.defaultValue !== input.value) {
buttonToToggle.disabled = false;
button.disabled = false;
} else {
buttonToToggle.disabled = true;
}
},
observeInputsInForm: function (form, observedInputs) {
for (var i = 0; i < observedInputs.length; i++) {
var input = form.elements[observedInputs[i]];
switch (input.tagName) {
case 'INPUT':
case 'TEXTAREA':
input.oninput = function () {
MapGuesser.observeInput(this, form.elements.submit);
};
break;
case 'SELECT':
input.onchange = function () {
MapGuesser.observeInput(this, form.elements.submit);
};
break;
}
}
form.onreset = function () {
form.elements.submit.disabled = true;
button.disabled = true;
}
}
};
@@ -227,41 +110,7 @@ var MapGuesser = {
}
}
var forms = document.getElementsByTagName('form');
for (var i = 0; i < forms.length; i++) {
var form = forms[i];
if (form.dataset.noSubmit) {
continue;
}
MapGuesser.setOnsubmitForForm(form);
if (form.dataset.observeInputs) {
MapGuesser.observeInputsInForm(form, form.dataset.observeInputs.split(','));
}
}
document.getElementById('cover').onclick = function () {
MapGuesser.hideModal();
};
if (COOKIES_CONSENT) {
MapGuesser.initGoogleAnalitics();
} else {
// we don't want user to agree cookies when clicking on the notice itself
document.getElementById('cookiesNotice').onclick = function (e) {
e.stopPropagation();
};
document.getElementById('agreeCookiesButton').onclick = function () {
MapGuesser.agreeCookies();
document.getElementById('cookiesNotice').style.display = 'none';
};
window.onclick = function () {
MapGuesser.agreeCookies();
};
}
})();
-112
View File
@@ -1,112 +0,0 @@
(function () {
var Maps = {
descriptionDivs: null,
addStaticMaps: function () {
var imgContainers = document.getElementById('mapContainer').getElementsByClassName('imgContainer');
for (var i = 0; i < imgContainers.length; i++) {
var imgContainer = imgContainers[i];
var imgSrc = 'https://maps.googleapis.com/maps/api/staticmap?size=350x175&' +
'scale=' + (window.devicePixelRatio >= 2 ? 2 : 1) + '&' +
'visible=' + imgContainer.dataset.boundSouthLat + ',' + imgContainer.dataset.boundWestLng + '|' +
imgContainer.dataset.boundNorthLat + ',' + imgContainer.dataset.boundEastLng +
'&key=' + GOOGLE_MAPS_JS_API_KEY;
imgContainer.style.backgroundImage = 'url("' + imgSrc + '")';
}
},
initializeDescriptionDivs: function () {
Maps.descriptionDivs = document.getElementById('mapContainer').getElementsByClassName('description');
for (var i = 0; i < Maps.descriptionDivs.length; i++) {
var description = Maps.descriptionDivs[i];
var boundingClientRect = description.getBoundingClientRect();
description.defaultHeight = boundingClientRect.height;
}
},
calculateDescriptionDivHeights: function () {
var currentY;
var rows = [];
for (var i = 0; i < Maps.descriptionDivs.length; i++) {
var description = Maps.descriptionDivs[i];
var boundingClientRect = description.getBoundingClientRect();
if (currentY !== boundingClientRect.y) {
rows.push([]);
}
rows[rows.length - 1].push(description);
currentY = boundingClientRect.y;
}
for (var i = 0; i < rows.length; i++) {
var row = rows[i];
var maxHeight = 0;
for (var j = 0; j < row.length; j++) {
var description = row[j];
if (description.defaultHeight > maxHeight) {
maxHeight = description.defaultHeight;
}
}
for (var j = 0; j < row.length; j++) {
var description = row[j];
description.style.height = maxHeight + 'px';
}
}
}
};
Maps.addStaticMaps();
Maps.initializeDescriptionDivs();
Maps.calculateDescriptionDivHeights();
window.onresize = function () {
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('multiButton').onclick = function () {
MapGuesser.showModal('multi');
document.getElementById('createNewRoomButton').href = '/multiGame/new/' + this.dataset.mapId;
document.getElementById('multiForm').elements.roomId.select();
}
document.getElementById('closePlayModeButton').onclick = function () {
MapGuesser.hideModal();
};
document.getElementById('closeMultiButton').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;
};
}
})();
-34
View File
@@ -1,34 +0,0 @@
(function () {
Maps = {
deleteMap: function (mapId, mapName) {
MapGuesser.showModalWithContent('Delete map', 'Are you sure you want to delete map \'' + mapName + '\'?', [{
type: 'button',
classNames: ['red'],
text: 'Delete',
onclick: function () {
document.getElementById('loading').style.visibility = 'visible';
MapGuesser.httpRequest('POST', '/admin/deleteMap/' + mapId, function () {
if (this.response.error) {
document.getElementById('loading').style.visibility = 'hidden';
//TODO: handle this error
return;
}
window.location.reload();
});
}
}]);
}
};
var buttons = document.getElementById('mapContainer').getElementsByClassName('deleteButton');
for (var i = 0; i < buttons.length; i++) {
var button = buttons[i];
button.onclick = function () {
Maps.deleteMap(this.dataset.mapId, this.dataset.mapName);
};
}
})();
+51
View File
@@ -0,0 +1,51 @@
(function () {
var form = document.getElementById('profileForm');
form.elements.password_new.onkeyup = function () {
MapGuesser.toggleDisableOnChange(this, form.elements.save);
};
form.elements.password_new_confirm.onkeyup = function () {
MapGuesser.toggleDisableOnChange(this, form.elements.save);
};
form.onsubmit = function (e) {
document.getElementById('loading').style.visibility = 'visible';
e.preventDefault();
var formData = new FormData(form);
MapGuesser.httpRequest('POST', form.action, function () {
document.getElementById('loading').style.visibility = 'hidden';
if (this.response.error) {
var errorText;
switch (this.response.error) {
case 'password_not_match':
errorText = 'The given current password is wrong.'
break;
case 'passwords_too_short':
errorText = 'The given new password is too short. Please choose a password that is at least 6 characters long!'
break;
case 'passwords_not_match':
errorText = 'The given new passwords do not match.'
break;
}
var profileFormError = document.getElementById('profileFormError');
profileFormError.style.display = 'block';
profileFormError.innerHTML = errorText;
form.elements.password_new.select();
return;
}
document.getElementById('profileFormError').style.display = 'none';
form.reset();
form.elements.save.disabled = true;
form.elements.password_new.focus();
}, formData);
};
})();
+47
View File
@@ -0,0 +1,47 @@
(function () {
var form = document.getElementById('signupForm');
form.onsubmit = function (e) {
document.getElementById('loading').style.visibility = 'visible';
e.preventDefault();
var formData = new FormData(form);
MapGuesser.httpRequest('POST', form.action, function () {
document.getElementById('loading').style.visibility = 'hidden';
if (this.response.error) {
var errorText;
switch (this.response.error) {
case 'passwords_too_short':
errorText = 'The given password is too short. Please choose a password that is at least 6 characters long!'
break;
case 'passwords_not_match':
errorText = 'The given passwords do not match.'
break;
case 'user_found':
errorText = 'There is a user already registered with the given email address. Please <a href="/login" title="Login">login here</a>!';
break;
case 'not_active_user_found':
errorText = 'There is a user already registered with the given email address. Please check your email and click on the activation link!';
break;
}
var signupFormError = document.getElementById('signupFormError');
signupFormError.style.display = 'block';
signupFormError.innerHTML = errorText;
form.elements.email.select();
return;
}
document.getElementById('signupFormError').style.display = 'none';
form.reset();
form.elements.email.focus();
MapGuesser.showModalWithContent('Sign up successful', 'Sign up was successful. Please check your email and click on the activation link to activate your account!');
}, formData);
};
})();
+2 -17
View File
@@ -59,25 +59,10 @@ def getRevisionForRef(ref):
return subprocess.check_output(["git", "rev-list", "-1", ref], cwd=REPO).decode().strip()
def getLatestReleaseTag():
process = subprocess.Popen(["git", "for-each-ref", "refs/tags/Release*", "--sort=-creatordate", "--format=%(refname:short)"], stdout=subprocess.PIPE, cwd=REPO)
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
return subprocess.check_output(["git", "for-each-ref", "refs/tags/Release*", "--count=1", "--sort=-creatordate", "--format=%(refname:short)"], cwd=REPO).decode().strip()
def updateRepoFromRemote():
subprocess.call(["git", "fetch", "origin", "--prune", "--prune-tags"], cwd=REPO)
subprocess.call(["git", "fetch", "origin", "--prune"], cwd=REPO)
def checkoutWorktree(worktreePath, ref):
subprocess.call(["git", "checkout", "-f", ref], cwd=worktreePath)
+2 -11
View File
@@ -9,27 +9,18 @@ if [ -f ${ROOT_DIR}/installed ]; then
exit 1
fi
echo "Installing NPM packages..."
(cd ${ROOT_DIR}/multi && npm install)
echo "Installing Yarn packages..."
(cd ${ROOT_DIR}/public/static && yarn install)
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..."
(cd ${ROOT_DIR} && ./mapg db:migrate)
(cd ${ROOT_DIR} && ./mapg migrate)
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
echo "Minifying JS, CSS and SVG files..."
${ROOT_DIR}/scripts/minify.sh
echo "Linking view files..."
(cd ${ROOT_DIR} && ./mapg view:link)
else
echo "Creating the first user..."
(cd ${ROOT_DIR} && ./mapg user:add mapg@mapg.dev 123456 admin)
fi
touch ${ROOT_DIR}/installed
-20
View File
@@ -1,20 +0,0 @@
#!/bin/bash
if [ "$#" -lt 1 ]; then
echo "Usage: $0 <sql_file>"
exit 1
fi
SQL_FILE=$1
ROOT_DIR=$(dirname $(readlink -f "$0"))/..
. ${ROOT_DIR}/.env
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
echo "This script can only be used in DEV mode!"
exit 1
fi
echo "Running SQL on DB..."
mysql --host=${DB_HOST} --user=${DB_USER} --password=${DB_PASSWORD} ${DB_NAME} < ${SQL_FILE}
+2 -12
View File
@@ -5,25 +5,15 @@ ROOT_DIR=$(dirname $(readlink -f "$0"))/..
. ${ROOT_DIR}/.env
echo "Installing Composer packages..."
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
(cd ${ROOT_DIR} && composer install --no-dev)
else
(cd ${ROOT_DIR} && composer install --dev)
fi
echo "Installing NPM packages..."
(cd ${ROOT_DIR}/multi && npm install)
(cd ${ROOT_DIR} && composer install)
echo "Installing Yarn packages..."
(cd ${ROOT_DIR}/public/static && yarn install)
echo "Migrating DB..."
(cd ${ROOT_DIR} && ./mapg db:migrate)
(cd ${ROOT_DIR} && ./mapg migrate)
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
echo "Minifying JS, CSS and SVG files..."
${ROOT_DIR}/scripts/minify.sh
echo "Linking view files..."
(cd ${ROOT_DIR} && ./mapg view:link)
fi
+12 -12
View File
@@ -1,8 +1,7 @@
<?php namespace MapGuesser\Cli;
use DateTime;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\PersistentData\Model\User;
use MapGuesser\Database\Query\Modify;
use MapGuesser\Model\User;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
@@ -10,7 +9,7 @@ use Symfony\Component\Console\Output\OutputInterface;
class AddUserCommand extends Command
{
public function configure(): void
public function configure()
{
$this->setName('user:add')
->setDescription('Adding of user.')
@@ -21,19 +20,20 @@ class AddUserCommand extends Command
public function execute(InputInterface $input, OutputInterface $output): int
{
$user = new User();
$user->setEmail($input->getArgument('email'));
$user->setPlainPassword($input->getArgument('password'));
$user->setActive(true);
$user->setCreatedDate(new DateTime());
$user = new User([
'email' => $input->getArgument('email'),
]);
if ($input->hasArgument('type') && $input->getArgument('type') !== null) {
$user->setPlainPassword($input->getArgument('password'));
if ($input->hasArgument('type')) {
$user->setType($input->getArgument('type'));
}
try {
$pdm = new PersistentDataManager();
$pdm->saveToDb($user);
$modify = new Modify(\Container::$dbConnection, 'users');
$modify->fill($user->toArray());
$modify->save();
} catch (\Exception $e) {
$output->writeln('<error>Adding user failed!</error>');
$output->writeln('');
@@ -7,11 +7,11 @@ use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class MigrateDatabaseCommand extends Command
class DatabaseMigration extends Command
{
public function configure(): void
public function configure()
{
$this->setName('db:migrate')
$this->setName('migrate')
->setDescription('Migration of database changes.');
}
@@ -88,10 +88,6 @@ class MigrateDatabaseCommand extends Command
$path = ROOT . '/database/migrations/' . $type;
$dir = opendir($path);
if ($dir === false) {
throw new \Exception('Cannot open dir: ' . $path);
}
$files = [];
while ($file = readdir($dir)) {
$filePath = $path . '/' . $file;
-69
View File
@@ -1,69 +0,0 @@
<?php namespace MapGuesser\Cli;
use FilesystemIterator;
use MapGuesser\View\Linker;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class LinkViewCommand extends Command
{
public function configure(): void
{
$this->setName('view:link')
->setDescription('Linking of views.')
->addArgument('view', InputArgument::OPTIONAL, 'View file to be linked.');
}
public function execute(InputInterface $input, OutputInterface $output): int
{
$views = [];
$view = $input->getArgument('view');
if ($view !== null) {
$views[] = $view;
} else {
$folder = ROOT . '/views';
$folderLength = strlen($folder) + 1;
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($folder, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST);
foreach ($iterator as $file) {
if ($file->isDir() || $file->getExtension() !== 'php') {
continue;
}
$view = substr($file->getPath(), $folderLength) . '/' . $file->getBasename('.php');
if (strpos($view, 'templates') === 0 || strpos($view, 'tests') === 0) {
continue;
}
$views[] = $view;
}
}
try {
foreach ($views as $view) {
$generator = new Linker($view);
$generator->generate();
}
} catch (\Exception $e) {
$output->writeln('<error>Linking view(s) failed!</error>');
$output->writeln('');
$output->writeln((string) $e);
$output->writeln('');
return 1;
}
$output->writeln('<info>View(s) successfully linked!</info>');
return 0;
}
}
-127
View File
@@ -1,127 +0,0 @@
<?php namespace MapGuesser\Cli;
use DateTime;
use MapGuesser\Database\Query\Modify;
use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Repository\MultiRoomRepository;
use MapGuesser\Repository\UserConfirmationRepository;
use MapGuesser\Repository\UserPasswordResetterRepository;
use MapGuesser\Repository\UserPlayedPlaceRepository;
use MapGuesser\Repository\UserRepository;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class MaintainDatabaseCommand extends Command
{
private PersistentDataManager $pdm;
private UserRepository $userRepository;
private UserConfirmationRepository $userConfirmationRepository;
private UserPasswordResetterRepository $userPasswordResetterRepository;
private MultiRoomRepository $multiRoomRepository;
private UserPlayedPlaceRepository $userPlayedPlaceRepository;
public function __construct()
{
parent::__construct();
$this->pdm = new PersistentDataManager();
$this->userRepository = new UserRepository();
$this->userConfirmationRepository = new UserConfirmationRepository();
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
$this->multiRoomRepository = new MultiRoomRepository();
$this->userPlayedPlaceRepository = new UserPlayedPlaceRepository();
}
public function configure(): void
{
$this->setName('db:maintain')
->setDescription('Maintain database.');
}
public function execute(InputInterface $input, OutputInterface $output): int
{
try {
$this->deleteInactiveExpiredUsers();
$this->deleteExpiredPasswordResetters();
$this->deleteExpiredRooms();
$this->deleteExpiredSessions();
} catch (\Exception $e) {
$output->writeln('<error>Maintenance failed!</error>');
$output->writeln('');
$output->writeln((string) $e);
$output->writeln('');
return 1;
}
$output->writeln('<info>Maintenance was successful!</info>');
$output->writeln('');
return 0;
}
private function deleteInactiveExpiredUsers(): void
{
\Container::$dbConnection->startTransaction();
foreach ($this->userRepository->getAllInactiveExpired() as $user) {
//TODO: these can be in some wrapper class
$userConfirmation = $this->userConfirmationRepository->getByUser($user);
if ($userConfirmation !== null) {
$this->pdm->deleteFromDb($userConfirmation);
}
$userPasswordResetter = $this->userPasswordResetterRepository->getByUser($user);
if ($userPasswordResetter !== null) {
$this->pdm->deleteFromDb($userPasswordResetter);
}
foreach ($this->userPlayedPlaceRepository->getAllByUser($user) as $userPlayedPlace) {
$this->pdm->deleteFromDb($userPlayedPlace);
}
$this->pdm->deleteFromDb($user);
}
\Container::$dbConnection->commit();
}
private function deleteExpiredPasswordResetters(): void
{
foreach ($this->userPasswordResetterRepository->getAllExpired() as $passwordResetter) {
$this->pdm->deleteFromDb($passwordResetter);
}
}
private function deleteExpiredRooms(): void
{
foreach ($this->multiRoomRepository->getAllExpired() as $multiRoom) {
$this->pdm->deleteFromDb($multiRoom);
}
}
private function deleteExpiredSessions(): void
{
//TODO: model may be used for sessions too
$select = new Select(\Container::$dbConnection, 'sessions');
$select->columns(['id']);
$select->where('updated', '<', (new DateTime('-7 days'))->format('Y-m-d H:i:s'));
$result = $select->execute();
while ($session = $result->fetch(IResultSet::FETCH_ASSOC)) {
$modify = new Modify(\Container::$dbConnection, 'sessions');
$modify->setId($session['id']);
$modify->delete();
}
}
}
+13 -133
View File
@@ -1,174 +1,54 @@
<?php namespace MapGuesser\Controller;
use DateTime;
use Faker\Factory;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Util\Geo\Bounds;
use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent;
use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\Interfaces\Response\IRedirect;
use MapGuesser\Multi\MultiConnector;
use MapGuesser\PersistentData\Model\MultiRoom;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Repository\MapRepository;
use MapGuesser\Repository\MultiRoomRepository;
use MapGuesser\Response\Redirect;
class GameController
{
private IRequest $request;
private PersistentDataManager $pdm;
private MultiConnector $multiConnector;
private MultiRoomRepository $multiRoomRepository;
private MapRepository $mapRepository;
public function __construct(IRequest $request)
{
$this->request = $request;
$this->pdm = new PersistentDataManager();
$this->multiConnector = new MultiConnector();
$this->multiRoomRepository = new MultiRoomRepository();
$this->mapRepository = new MapRepository();
}
public function getGame(): IContent
{
$mapId = (int) $this->request->query('mapId');
return new HtmlContent('game', ['mapId' => $mapId]);
$data = $this->prepareGame($mapId);
return new HtmlContent('game', $data);
}
public function getNewMultiGame(): IRedirect
public function getGameJson(): IContent
{
$mapId = (int) $this->request->query('mapId');
$map = $this->mapRepository->getById($mapId);
$roomId = bin2hex(random_bytes(3));
$token = $this->getMultiToken($roomId);
$room = new MultiRoom();
$room->setRoomId($roomId);
$room->setStateArray([
'mapId' => $mapId,
'area' => $map->getArea(),
'rounds' => [],
'currentRound' => -1
]);
$room->setMembersArray(['owner' => $token, 'all' => []]);
$room->setUpdatedDate(new DateTime());
$this->pdm->saveToDb($room);
$this->multiConnector->sendMessage('create_room', ['roomId' => $roomId]);
return new Redirect(
\Container::$routeCollection
->getRoute('multiGame')
->generateLink(['roomId' => $roomId]),
IRedirect::TEMPORARY
);
$data = $this->prepareGame($mapId);
return new JsonContent($data);
}
public function getMultiGame(): IContent
private function prepareGame(int $mapId)
{
$roomId = $this->request->query('roomId');
return new HtmlContent('game', ['roomId' => $roomId]);
}
public function prepareGame(): IContent
{
$mapId = (int) $this->request->query('mapId');
$map = $this->mapRepository->getById($mapId);
$bounds = Bounds::createDirectly($map['bound_south_lat'], $map['bound_west_lng'], $map['bound_north_lat'], $map['bound_east_lng']);
$session = $this->request->session();
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
$session->set('state', [
'mapId' => $mapId,
'area' => $map->getArea(),
'rounds' => [],
'currentRound' => -1
'area' => $map['area'],
'rounds' => []
]);
} 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
{
$roomId = $this->request->query('roomId');
$userName = $this->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());
$this->pdm->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()
]);
}
private function getMultiToken(string $roomId): string
{
$session = $this->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;
return ['mapId' => $mapId, 'mapName' => $map['name'], 'bounds' => $bounds->toArray()];
}
}
+57 -211
View File
@@ -1,16 +1,10 @@
<?php namespace MapGuesser\Controller;
use DateTime;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Util\Geo\Position;
use MapGuesser\Response\JsonContent;
use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\Multi\MultiConnector;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\PersistentData\Model\UserPlayedPlace;
use MapGuesser\Repository\MultiRoomRepository;
use MapGuesser\Repository\PlaceRepository;
use MapGuesser\Repository\UserPlayedPlaceRepository;
class GameFlowController
{
@@ -19,255 +13,107 @@ class GameFlowController
private IRequest $request;
private PersistentDataManager $pdm;
private MultiConnector $multiConnector;
private MultiRoomRepository $multiRoomRepository;
private PlaceRepository $placeRepository;
private UserPlayedPlaceRepository $userPlayedPlaceRepository;
public function __construct(IRequest $request)
{
$this->request = $request;
$this->pdm = new PersistentDataManager();
$this->multiConnector = new MultiConnector();
$this->multiRoomRepository = new MultiRoomRepository();
$this->placeRepository = new PlaceRepository();
$this->userPlayedPlaceRepository = new UserPlayedPlaceRepository();
}
public function initialData(): IContent
public function getNewPlace(): IContent
{
$mapId = (int) $this->request->query('mapId');
$session = $this->request->session();
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
return new JsonContent(['error' => 'no_session_found']);
$data = ['error' => 'no_session_found'];
return new JsonContent($data);
}
if (!isset($state['currentRound']) || $state['currentRound'] == -1 || $state['currentRound'] >= static::NUMBER_OF_ROUNDS) {
$this->startNewGame($state, $mapId);
if (count($state['rounds']) === 0) {
$place = $this->placeRepository->getForMapWithValidPano($mapId);
$state['rounds'][] = $place;
$session->set('state', $state);
}
$response = [];
$data = ['panoId' => $place['panoId']];
} else {
$rounds = count($state['rounds']);
$last = $state['rounds'][$rounds - 1];
$last = $state['rounds'][$state['currentRound']];
$response['place'] = [
'panoId' => $last['panoId'],
'pov' => $last['pov']->toArray()
];
$response['history'] = [];
for ($i = 0; $i < $state['currentRound']; ++$i) {
$round = $state['rounds'][$i];
$response['history'][] = [
'position' => $round['position']->toArray(),
'result' => [
$history = [];
for ($i = 0; $i < $rounds - 1; ++$i) {
$round = $state['rounds'][$i];
$history[] = [
'position' => $round['position']->toArray(),
'guessPosition' => $round['guessPosition']->toArray(),
'distance' => $round['distance'],
'score' => $round['score']
]
];
}
$data = [
'history' => $history,
'panoId' => $last['panoId']
];
}
return new JsonContent($response);
return new JsonContent($data);
}
public function multiInitialData(): IContent
{
$roomId = $this->request->query('roomId');
$session = $this->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']);
}
if ($state['currentRound'] == -1 || $state['currentRound'] >= static::NUMBER_OF_ROUNDS - 1) {
$this->startNewGame($state, $state['mapId']);
$room->setStateArray($state);
$room->setUpdatedDate(new DateTime());
$this->pdm->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]);
}
public function guess(): IContent
public function evaluateGuess(): IContent
{
$mapId = (int) $this->request->query('mapId');
$session = $this->request->session();
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
return new JsonContent(['error' => 'no_session_found']);
$data = ['error' => 'no_session_found'];
return new JsonContent($data);
}
$last = $state['rounds'][$state['currentRound']];
$last = $state['rounds'][count($state['rounds']) - 1];
$position = $last['position'];
$guessPosition = new Position((float) $this->request->post('lat'), (float) $this->request->post('lng'));
$result = $this->evalueteGuess($last['position'], $guessPosition, $state['area']);
$distance = $this->calculateDistance($position, $guessPosition);
$score = $this->calculateScore($distance, $state['area']);
$last['guessPosition'] = $guessPosition;
$last['distance'] = $result['distance'];
$last['score'] = $result['score'];
$last['distance'] = $distance;
$last['score'] = $score;
$state['rounds'][count($state['rounds']) - 1] = $last;
$response = [
'position' => $last['position']->toArray(),
'result' => $result
];
if (count($state['rounds']) < static::NUMBER_OF_ROUNDS) {
$exclude = [];
$state['rounds'][$state['currentRound']] = $last;
$state['currentRound'] += 1;
if ($state['currentRound'] < static::NUMBER_OF_ROUNDS) {
$next = $state['rounds'][$state['currentRound']];
$response['place'] = [
'panoId' => $next['panoId'],
'pov' => $next['pov']->toArray()
];
}
$session->set('state', $state);
$this->saveVisit($last);
return new JsonContent($response);
}
// save the selected place for the round in UserPlayedPlace
private function saveVisit($last): void
{
$session = $this->request->session();
$userId = $session->get('userId');
if(isset($userId)) {
$placeId = $last['placeId'];
$userPlayedPlace = $this->userPlayedPlaceRepository->getByUserIdAndPlaceId($userId, $placeId);
if(!$userPlayedPlace) {
$userPlayedPlace = new UserPlayedPlace();
$userPlayedPlace->setUserId($userId);
$userPlayedPlace->setPlaceId($placeId);
} else {
$userPlayedPlace->incrementOccurrences();
foreach ($state['rounds'] as $round) {
$exclude = array_merge($exclude, $round['placesWithoutPano'], [$round['placeId']]);
}
$userPlayedPlace->setLastTimeDate(new DateTime());
$this->pdm->saveToDb($userPlayedPlace);
}
}
public function multiGuess(): IContent
{
$roomId = $this->request->query('roomId');
$session = $this->request->session();
$place = $this->placeRepository->getForMapWithValidPano($mapId, $exclude);
$state['rounds'][] = $place;
$session->set('state', $state);
if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) {
return new JsonContent(['error' => 'no_session_found']);
$panoId = $place['panoId'];
} else {
$state['rounds'] = [];
$session->set('state', $state);
$panoId = null;
}
$room = $this->multiRoomRepository->getByRoomId($roomId);
$state = $room->getStateArray();
$last = $state['rounds'][$state['currentRound']];
$guessPosition = new Position((float) $this->request->post('lat'), (float) $this->request->post('lng'));
$result = $this->evalueteGuess($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 = [
'position' => $last['position']->toArray(),
'result' => $result,
'allResults' => $responseFromMulti['allResults']
$data = [
'result' => [
'position' => $position->toArray(),
'distance' => $distance,
'score' => $score
],
'panoId' => $panoId
];
return new JsonContent($response);
}
public function multiNextRound(): IContent
{
$roomId = $this->request->query('roomId');
$session = $this->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());
$this->pdm->saveToDb($room);
return new JsonContent(['ok' => true]);
}
private function evalueteGuess(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 = $this->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'][] = [
'placeId' => $place->getId(),
'position' => $place->getPosition(),
'panoId' => $place->getPanoIdCached(),
'pov' => $place->getPov()
];
}
return new JsonContent($data);
}
private function calculateDistance(Position $realPosition, Position $guessPosition): float
+1 -18
View File
@@ -1,29 +1,12 @@
<?php namespace MapGuesser\Controller;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\Interfaces\Response\IRedirect;
use MapGuesser\Response\JsonContent;
use MapGuesser\Response\Redirect;
class HomeController
{
private IRequest $request;
public function __construct(IRequest $request)
{
$this->request = $request;
}
public function getIndex(): IRedirect
{
return new Redirect(\Container::$routeCollection->getRoute('maps')->generateLink(), IRedirect::TEMPORARY);
}
public function startSession(): IContent
{
// session starts with the request, this method just sends valid data to the client
return new JsonContent(['antiCsrfToken' => $this->request->session()->get('anti_csrf_token')]);
return new Redirect([\Container::$routeCollection->getRoute('maps'), []], IRedirect::TEMPORARY);
}
}
+32 -573
View File
@@ -1,619 +1,78 @@
<?php namespace MapGuesser\Controller;
use DateInterval;
use DateTime;
use MapGuesser\Http\Request;
use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\Interfaces\Response\IRedirect;
use MapGuesser\Mailing\Mail;
use MapGuesser\OAuth\GoogleOAuth;
use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\Model\UserConfirmation;
use MapGuesser\PersistentData\Model\UserPasswordResetter;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Repository\UserConfirmationRepository;
use MapGuesser\Repository\UserPasswordResetterRepository;
use MapGuesser\Repository\UserPlayedPlaceRepository;
use MapGuesser\Repository\UserRepository;
use MapGuesser\Model\User;
use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent;
use MapGuesser\Response\Redirect;
use MapGuesser\Util\JwtParser;
class LoginController
{
private IRequest $request;
private PersistentDataManager $pdm;
private UserRepository $userRepository;
private UserConfirmationRepository $userConfirmationRepository;
private UserPasswordResetterRepository $userPasswordResetterRepository;
private UserPlayedPlaceRepository $userPlayedPlaceRepository;
public function __construct(IRequest $request)
{
$this->request = $request;
$this->pdm = new PersistentDataManager();
$this->userRepository = new UserRepository();
$this->userConfirmationRepository = new UserConfirmationRepository();
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
$this->userPlayedPlaceRepository = new UserPlayedPlaceRepository();
}
public function getLoginForm()
{
if ($this->request->user() !== null) {
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
$session = $this->request->session();
if ($session->get('user')) {
return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY);
}
return new HtmlContent('login/login');
}
public function getGoogleLoginRedirect(): IRedirect
{
$state = bin2hex(random_bytes(16));
$nonce = bin2hex(random_bytes(16));
$this->request->session()->set('oauth_state', $state);
$this->request->session()->set('oauth_nonce', $nonce);
$oAuth = new GoogleOAuth(new Request());
$url = $oAuth->getDialogUrl(
$state,
$this->request->getBase() . '/' . \Container::$routeCollection->getRoute('login-google-action')->generateLink(),
$nonce
);
return new Redirect($url, IRedirect::TEMPORARY);
}
public function getSignupForm()
{
if ($this->request->user() !== null) {
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
}
if ($this->request->session()->has('tmp_user_data')) {
$tmpUserData = $this->request->session()->get('tmp_user_data');
$data = ['email' => $tmpUserData['email']];
} else {
$data = [];
}
return new HtmlContent('login/signup', $data);
}
public function getSignupSuccess(): IContent
{
return new HtmlContent('login/signup_success');
}
public function getSignupWithGoogleForm()
{
if ($this->request->user() !== null) {
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
}
if (!$this->request->session()->has('google_user_data')) {
return new Redirect(\Container::$routeCollection->getRoute('login-google')->generateLink(), IRedirect::TEMPORARY);
}
$userData = $this->request->session()->get('google_user_data');
$user = $this->userRepository->getByEmail($userData['email']);
return new HtmlContent('login/google_signup', ['found' => $user !== null, 'email' => $userData['email']]);
}
public function getRequestPasswordResetForm()
{
if ($this->request->user() !== null) {
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
}
return new HtmlContent('login/password_reset_request', ['email' => $this->request->query('email')]);
}
public function getRequestPasswordResetSuccess(): IContent
{
return new HtmlContent('login/password_reset_request_success');
}
public function getResetPasswordForm()
{
if ($this->request->user() !== null) {
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
}
$token = $this->request->query('token');
$resetter = $this->userPasswordResetterRepository->getByToken($token);
if ($resetter === null || $resetter->getExpiresDate() < new DateTime()) {
return new HtmlContent('login/reset_password', ['success' => false]);
}
$user = $this->userRepository->getById($resetter->getUserId());
return new HtmlContent('login/reset_password', ['success' => true, 'token' => $token, 'email' => $user->getEmail()]);
$data = [];
return new HtmlContent('login', $data);
}
public function login(): IContent
{
if ($this->request->user() !== null) {
return new JsonContent(['success' => true]);
$session = $this->request->session();
if ($session->get('user')) {
$data = ['success' => true];
return new JsonContent($data);
}
$user = $this->userRepository->getByEmail($this->request->post('email'));
$select = new Select(\Container::$dbConnection, 'users');
$select->columns(User::getFields());
$select->where('email', '=', $this->request->post('email'));
if ($user === null) {
if (strlen($this->request->post('password')) < 6) {
return new JsonContent([
'error' => [
'errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!'
]
]);
}
$userData = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
$tmpUser = new User();
$tmpUser->setPlainPassword($this->request->post('password'));
$this->request->session()->set('tmp_user_data', [
'email' => $this->request->post('email'),
'password_hashed' => $tmpUser->getPassword()
]);
return new JsonContent([
'redirect' => [
'target' => '/' . \Container::$routeCollection->getRoute('signup')->generateLink()
]
]);
if ($userData === null) {
$data = ['error' => 'user_not_found'];
return new JsonContent($data);
}
$user = new User($userData);
if (!$user->getActive()) {
$this->resendConfirmationEmail($user);
return new JsonContent([
'error' => [
'errorText' => 'User found with the given email address, but the account is not activated. ' .
'Please check your email and click on the activation link!'
]
]);
$data = ['error' => 'user_not_active'];
return new JsonContent($data);
}
if (!$user->checkPassword($this->request->post('password'))) {
return new JsonContent([
'error' => [
'errorText' => 'The given password is wrong. You can <a href="/password/requestReset?email=' .
urlencode($user->getEmail()) . '" title="Request password reset">request password reset</a>!'
]
]);
$data = ['error' => 'password_not_match'];
return new JsonContent($data);
}
$this->request->setUser($user);
$session->set('user', $user);
return new JsonContent(['success' => true]);
}
public function loginWithGoogle()
{
if ($this->request->user() !== null) {
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
}
if ($this->request->query('state') !== $this->request->session()->get('oauth_state')) {
return new HtmlContent('login/google_login');
}
$oAuth = new GoogleOAuth(new Request());
$tokenData = $oAuth->getToken(
$this->request->query('code'),
$this->request->getBase() . '/' . \Container::$routeCollection->getRoute('login-google-action')->generateLink()
);
if (!isset($tokenData['id_token'])) {
return new HtmlContent('login/google_login');
}
$jwtParser = new JwtParser($tokenData['id_token']);
$idToken = $jwtParser->getPayload();
if ($idToken['nonce'] !== $this->request->session()->get('oauth_nonce')) {
return new HtmlContent('login/google_login');
}
if (!$idToken['email_verified']) {
return new HtmlContent('login/google_login');
}
$user = $this->userRepository->getByGoogleSub($idToken['sub']);
if ($user === null) {
$this->request->session()->set('google_user_data', ['sub' => $idToken['sub'], 'email' => $idToken['email']]);
return new Redirect(\Container::$routeCollection->getRoute('signup-google')->generateLink(), IRedirect::TEMPORARY);
}
$this->request->setUser($user);
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
$data = ['success' => true];
return new JsonContent($data);
}
public function logout(): IRedirect
{
$this->request->setUser(null);
$this->request->session()->delete('user');
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
}
public function signup(): IContent
{
if ($this->request->user() !== null) {
return new JsonContent(['redirect' => ['target' => '/' . \Container::$routeCollection->getRoute('home')->generateLink()]]);
}
$user = $this->userRepository->getByEmail($this->request->post('email'));
if ($user !== null) {
if ($user->getActive()) {
if (!$user->checkPassword($this->request->post('password'))) {
return new JsonContent([
'error' => [
'errorText' => 'There is a user already registered with the given email address, ' .
'but the given password is wrong. You can <a href="/password/requestReset?email=' .
urlencode($user->getEmail()) . '" title="Request password reset">request password reset</a>!'
]
]);
}
$this->request->setUser($user);
$data = ['redirect' => ['target' => '/' . \Container::$routeCollection->getRoute('index')->generateLink()]];
} else {
$data = [
'error' => [
'errorText' => 'There is a user already registered with the given email address. ' .
'Please check your email and click on the activation link!'
]
];
}
return new JsonContent($data);
}
if (filter_var($this->request->post('email'), FILTER_VALIDATE_EMAIL) === false) {
return new JsonContent(['error' => ['errorText' => 'The given email address is not valid.']]);
}
if ($this->request->session()->has('tmp_user_data')) {
$tmpUserData = $this->request->session()->get('tmp_user_data');
$tmpUser = new User();
$tmpUser->setPassword($tmpUserData['password_hashed']);
if (!$tmpUser->checkPassword($this->request->post('password'))) {
return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]);
}
} else {
if (strlen($this->request->post('password')) < 6) {
return new JsonContent([
'error' => [
'errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!'
]
]);
}
if ($this->request->post('password') !== $this->request->post('password_confirm')) {
return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]);
}
}
$user = new User();
$user->setEmail($this->request->post('email'));
$user->setPlainPassword($this->request->post('password'));
$user->setCreatedDate(new DateTime());
\Container::$dbConnection->startTransaction();
$this->pdm->saveToDb($user);
$token = bin2hex(random_bytes(16));
$confirmation = new UserConfirmation();
$confirmation->setUser($user);
$confirmation->setToken($token);
$confirmation->setLastSentDate(new DateTime());
$this->pdm->saveToDb($confirmation);
\Container::$dbConnection->commit();
$this->sendConfirmationEmail($user->getEmail(), $token, $user->getCreatedDate());
$this->request->session()->delete('tmp_user_data');
return new JsonContent(['success' => true]);
}
public function signupWithGoogle(): IContent
{
if ($this->request->user() !== null) {
return new JsonContent(['success' => true]);
}
$userData = $this->request->session()->get('google_user_data');
$user = $this->userRepository->getByEmail($userData['email']);
if ($user === null) {
$sendWelcomeEmail = true;
$user = new User();
$user->setEmail($userData['email']);
$user->setCreatedDate(new DateTime());
} else {
$sendWelcomeEmail = false;
}
$user->setActive(true);
$user->setGoogleSub($userData['sub']);
$this->pdm->saveToDb($user);
if ($sendWelcomeEmail) {
$this->sendWelcomeEmail($user->getEmail());
}
$this->request->session()->delete('google_user_data');
$this->request->setUser($user);
return new JsonContent(['success' => true]);
}
public function resetSignup(): IContent
{
$this->request->session()->delete('tmp_user_data');
return new JsonContent(['success' => true]);
}
public function resetGoogleSignup(): IContent
{
$this->request->session()->delete('google_user_data');
return new JsonContent(['success' => true]);
}
public function activate()
{
if ($this->request->user() !== null) {
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
}
$confirmation = $this->userConfirmationRepository->getByToken(substr($this->request->query('token'), 0, 32));
if ($confirmation === null) {
return new HtmlContent('login/activate');
}
\Container::$dbConnection->startTransaction();
$this->pdm->deleteFromDb($confirmation);
$user = $this->userRepository->getById($confirmation->getUserId());
$user->setActive(true);
$this->pdm->saveToDb($user);
\Container::$dbConnection->commit();
$this->request->setUser($user);
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
}
public function cancel()
{
if ($this->request->user() !== null) {
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
}
$confirmation = $this->userConfirmationRepository->getByToken(substr($this->request->query('token'), 0, 32));
if ($confirmation === null) {
return new HtmlContent('login/cancel', ['success' => false]);
}
\Container::$dbConnection->startTransaction();
$this->pdm->deleteFromDb($confirmation);
$user = $this->userRepository->getById($confirmation->getUserId());
foreach ($this->userPlayedPlaceRepository->getAllByUser($user) as $userPlayedPlace) {
$this->pdm->deleteFromDb($userPlayedPlace);
}
$this->pdm->deleteFromDb($user);
\Container::$dbConnection->commit();
return new HtmlContent('login/cancel', ['success' => true]);
}
public function requestPasswordReset(): IContent
{
if ($this->request->user() !== null) {
return new JsonContent([
'redirect' => [
'target' => '/' . \Container::$routeCollection->getRoute('home')->generateLink()
]
]);
}
$user = $this->userRepository->getByEmail($this->request->post('email'));
if ($user === null) {
return new JsonContent([
'error' => [
'errorText' => 'No user found with the given email address. You can <a href="/signup" title="Sign up">sign up</a>!'
]
]);
}
if (!$user->getActive()) {
$this->resendConfirmationEmail($user);
return new JsonContent([
'error' => [
'errorText' => 'User found with the given email address, but the account is not activated. ' .
'Please check your email and click on the activation link!'
]
]);
}
$existingResetter = $this->userPasswordResetterRepository->getByUser($user);
if ($existingResetter !== null && $existingResetter->getExpiresDate() > new DateTime()) {
return new JsonContent([
'error' => [
'errorText' => 'Password reset was recently requested for this account. Please check your email, or try again later!'
]
]);
}
$token = bin2hex(random_bytes(16));
$expires = new DateTime('+1 hour');
$passwordResetter = new UserPasswordResetter();
$passwordResetter->setUser($user);
$passwordResetter->setToken($token);
$passwordResetter->setExpiresDate($expires);
\Container::$dbConnection->startTransaction();
if ($existingResetter !== null) {
$this->pdm->deleteFromDb($existingResetter);
}
$this->pdm->saveToDb($passwordResetter);
\Container::$dbConnection->commit();
$this->sendPasswordResetEmail($user->getEmail(), $token, $expires);
return new JsonContent(['success' => true]);
}
public function resetPassword(): IContent
{
if ($this->request->user() !== null) {
return new JsonContent([
'redirect' => [
'target' => '/' . \Container::$routeCollection->getRoute('home')->generateLink()
]
]);
}
$token = $this->request->query('token');
$resetter = $this->userPasswordResetterRepository->getByToken($token);
if ($resetter === null || $resetter->getExpiresDate() < new DateTime()) {
return new JsonContent([
'redirect' => [
'target' => '/' . \Container::$routeCollection->getRoute('password-reset')->generateLink(['token' => $token])
]
]);
}
if (strlen($this->request->post('password')) < 6) {
return new JsonContent([
'error' => [
'errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!'
]
]);
}
if ($this->request->post('password') !== $this->request->post('password_confirm')) {
return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]);
}
\Container::$dbConnection->startTransaction();
$this->pdm->deleteFromDb($resetter);
$user = $this->userRepository->getById($resetter->getUserId());
$user->setPlainPassword($this->request->post('password'));
$this->pdm->saveToDb($user);
\Container::$dbConnection->commit();
$this->request->setUser($user);
return new JsonContent(['success' => true]);
}
private function sendConfirmationEmail(string $email, string $token, DateTime $created): void
{
$mail = new Mail();
$mail->addRecipient($email);
$mail->setSubject('Welcome to ' . $_ENV['APP_NAME'] . ' - Activate your account');
$mail->setBodyFromTemplate('signup', [
'EMAIL' => $email,
'ACTIVATE_LINK' => $this->request->getBase() . '/' .
\Container::$routeCollection->getRoute('signup.activate')->generateLink(['token' => $token]),
'CANCEL_LINK' => $this->request->getBase() . '/' .
\Container::$routeCollection->getRoute('signup.cancel')->generateLink(['token' => $token]),
'ACTIVATABLE_UNTIL' => (clone $created)->add(new DateInterval('P1D'))->format('Y-m-d H:i T')
]);
$mail->send();
}
private function resendConfirmationEmail(User $user): bool
{
$confirmation = $this->userConfirmationRepository->getByUser($user);
if ($confirmation === null || (clone $confirmation->getLastSentDate())->add(new DateInterval('PT1H')) > new DateTime()) {
return false;
}
$confirmation->setLastSentDate(new DateTime());
$this->pdm->saveToDb($confirmation);
$this->sendConfirmationEmail($user->getEmail(), $confirmation->getToken(), $user->getCreatedDate());
return true;
}
private function sendWelcomeEmail(string $email): void
{
$mail = new Mail();
$mail->addRecipient($email);
$mail->setSubject('Welcome to ' . $_ENV['APP_NAME']);
$mail->setBodyFromTemplate('signup-noconfirm', [
'EMAIL' => $email,
]);
$mail->send();
}
private function sendPasswordResetEmail(string $email, string $token, DateTime $expires): void
{
$mail = new Mail();
$mail->addRecipient($email);
$mail->setSubject($_ENV['APP_NAME'] . ' - Password reset');
$mail->setBodyFromTemplate('password-reset', [
'EMAIL' => $email,
'RESET_LINK' => $this->request->getBase() . '/' .
\Container::$routeCollection->getRoute('password-reset')->generateLink(['token' => $token]),
'EXPIRES' => $expires->format('Y-m-d H:i T')
]);
$mail->send();
return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY);
}
}
+106 -98
View File
@@ -1,20 +1,19 @@
<?php namespace MapGuesser\Controller;
use DateTime;
use MapGuesser\Database\Query\Modify;
use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Authentication\IUser;
use MapGuesser\Interfaces\Authorization\ISecured;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\PersistentData\Model\Map;
use MapGuesser\PersistentData\Model\Place;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Repository\MapRepository;
use MapGuesser\Repository\PlaceRepository;
use MapGuesser\Repository\UserPlayedPlaceRepository;
use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent;
use MapGuesser\Util\Geo\Bounds;
use MapGuesser\Util\Panorama\Pov;
use MapGuesser\Util\Geo\Position;
class MapAdminController implements ISecured
{
@@ -22,21 +21,15 @@ class MapAdminController implements ISecured
private IRequest $request;
private PersistentDataManager $pdm;
private MapRepository $mapRepository;
private PlaceRepository $placeRepository;
private UserPlayedPlaceRepository $userPlayedPlaceRepository;
public function __construct(IRequest $request)
{
$this->request = $request;
$this->pdm = new PersistentDataManager();
$this->mapRepository = new MapRepository();
$this->placeRepository = new PlaceRepository();
$this->userPlayedPlaceRepository = new UserPlayedPlaceRepository();
}
public function authorize(): bool
@@ -52,29 +45,29 @@ class MapAdminController implements ISecured
if ($mapId) {
$map = $this->mapRepository->getById($mapId);
$places = $this->getPlaces($map);
$bounds = Bounds::createDirectly($map['bound_south_lat'], $map['bound_west_lng'], $map['bound_north_lat'], $map['bound_east_lng']);
$places = $this->getPlaces($mapId);
} else {
$map = new Map();
$map->setName(self::$unnamedMapName);
$map = [
'name' => self::$unnamedMapName,
'description' => ''
];
$bounds = Bounds::createDirectly(-90.0, -180.0, 90.0, 180.0);
$places = [];
}
return new HtmlContent('admin/map_editor', [
'mapId' => $mapId,
'mapName' => $map->getName(),
'mapDescription' => str_replace('<br>', "\n", $map->getDescription()),
'bounds' => $map->getBounds()->toArray(),
'places' => &$places
]);
$data = ['mapId' => $mapId, 'mapName' => $map['name'], 'mapDescription' => str_replace('<br>', "\n", $map['description']), 'bounds' => $bounds->toArray(), 'places' => &$places];
return new HtmlContent('admin/map_editor', $data);
}
public function getPlace(): IContent
{
$placeId = (int) $this->request->query('placeId');
$place = $this->placeRepository->getById($placeId);
$placeData = $this->placeRepository->getById($placeId);
return new JsonContent(['panoId' => $place->getFreshPanoId()]);
$data = ['panoId' => $placeData['panoId']];
return new JsonContent($data);
}
public function saveMap(): IContent
@@ -83,12 +76,8 @@ class MapAdminController implements ISecured
\Container::$dbConnection->startTransaction();
if ($mapId) {
$map = $this->mapRepository->getById($mapId);
} else {
$map = new Map();
$map->setName(self::$unnamedMapName);
$this->pdm->saveToDb($map);
if (!$mapId) {
$mapId = $this->addNewMap();
}
if (isset($_POST['added'])) {
@@ -96,23 +85,11 @@ class MapAdminController implements ISecured
foreach ($_POST['added'] as $placeRaw) {
$placeRaw = json_decode($placeRaw, true);
$place = new Place();
$place->setMap($map);
$place->setLat((float) $placeRaw['lat']);
$place->setLng((float) $placeRaw['lng']);
$place->setPov(new Pov(
(float) $placeRaw['pov']['heading'],
(float) $placeRaw['pov']['pitch'],
(float) $placeRaw['pov']['zoom']
));
if ($placeRaw['panoId'] === -1) {
$place->setPanoIdCachedTimestampDate(new DateTime('-1 day'));
}
$this->pdm->saveToDb($place);
$addedIds[] = ['tempId' => $placeRaw['id'], 'id' => $place->getId()];
$addedIds[] = ['tempId' => $placeRaw['id'], 'id' => $this->placeRepository->addToMap($mapId, [
'lat' => (float) $placeRaw['lat'],
'lng' => (float) $placeRaw['lng'],
'pano_id_cached_timestamp' => $placeRaw['panoId'] === -1 ? (new DateTime('-1 day'))->format('Y-m-d H:i:s') : null
])];
}
} else {
$addedIds = [];
@@ -122,17 +99,10 @@ class MapAdminController implements ISecured
foreach ($_POST['edited'] as $placeRaw) {
$placeRaw = json_decode($placeRaw, true);
$place = $this->placeRepository->getById((int) $placeRaw['id']);
$place->setLat((float) $placeRaw['lat']);
$place->setLng((float) $placeRaw['lng']);
$place->setPov(new Pov(
(float) $placeRaw['pov']['heading'],
(float) $placeRaw['pov']['pitch'],
(float) $placeRaw['pov']['zoom']
));
$place->setPanoIdCachedTimestampDate(new DateTime('-1 day'));
$this->pdm->saveToDb($place);
$this->placeRepository->modify((int) $placeRaw['id'], [
'lat' => (float) $placeRaw['lat'],
'lng' => (float) $placeRaw['lng']
]);
}
}
@@ -140,90 +110,128 @@ class MapAdminController implements ISecured
foreach ($_POST['deleted'] as $placeRaw) {
$placeRaw = json_decode($placeRaw, true);
$place = $this->placeRepository->getById((int) $placeRaw['id']);
$this->deletePlace($place);
$this->placeRepository->delete($placeRaw['id']);
}
}
$mapBounds = $this->calculateMapBounds($map);
$mapBounds = $this->calculateMapBounds($mapId);
$map->setBounds($mapBounds);
$map->setArea($mapBounds->calculateApproximateArea());
$map = [
'bound_south_lat' => $mapBounds->getSouthLat(),
'bound_west_lng' => $mapBounds->getWestLng(),
'bound_north_lat' => $mapBounds->getNorthLat(),
'bound_east_lng' => $mapBounds->getEastLng(),
'area' => $mapBounds->calculateApproximateArea(),
];
if (isset($_POST['name'])) {
$map->setName($_POST['name'] ? $_POST['name'] : self::$unnamedMapName);
$map['name'] = $_POST['name'] ? $_POST['name'] : self::$unnamedMapName;
}
if (isset($_POST['description'])) {
$map->setDescription(str_replace(["\n", "\r\n"], '<br>', $_POST['description']));
$map['description'] = str_replace(["\n", "\r\n"], '<br>', $_POST['description']);
}
$this->pdm->saveToDb($map);
$this->saveMapData($mapId, $map);
\Container::$dbConnection->commit();
return new JsonContent(['mapId' => $map->getId(), 'added' => $addedIds]);
$data = ['mapId' => $mapId, 'added' => $addedIds];
return new JsonContent($data);
}
public function deleteMap(): IContent
{
public function deleteMap() {
$mapId = (int) $this->request->query('mapId');
$map = $this->mapRepository->getById($mapId);
\Container::$dbConnection->startTransaction();
$this->deletePlaces($map);
$this->deletePlaces($mapId);
$this->pdm->deleteFromDb($map);
$modify = new Modify(\Container::$dbConnection, 'maps');
$modify->setId($mapId);
$modify->delete();
\Container::$dbConnection->commit();
return new JsonContent(['success' => true]);
$data = ['success' => true];
return new JsonContent($data);
}
private function deletePlace(Place $place): void
private function deletePlaces(int $mapId): void
{
foreach ($this->userPlayedPlaceRepository->getAllByPlace($place) as $userPlayedPlace) {
$this->pdm->deleteFromDb($userPlayedPlace);
}
$select = new Select(\Container::$dbConnection, 'places');
$select->columns(['id']);
$select->where('map_id', '=', $mapId);
$this->pdm->deleteFromDb($place);
}
$result = $select->execute();
private function deletePlaces(Map $map): void
{
foreach ($this->placeRepository->getAllForMap($map) as $place) {
$this->deletePlace($place);
while ($place = $result->fetch(IResultSet::FETCH_ASSOC)) {
$modify = new Modify(\Container::$dbConnection, 'places');
$modify->setId($place['id']);
$modify->delete();
}
}
private function calculateMapBounds(Map $map): Bounds
private function calculateMapBounds(int $mapId): Bounds
{
$select = new Select(\Container::$dbConnection, 'places');
$select->columns(['lat', 'lng']);
$select->where('map_id', '=', $mapId);
$result = $select->execute();
$bounds = new Bounds();
foreach ($this->placeRepository->getAllForMap($map) as $place) {
$bounds->extend($place->getPosition());
while ($place = $result->fetch(IResultSet::FETCH_ASSOC)) {
$bounds->extend(new Position($place['lat'], $place['lng']));
}
return $bounds;
}
private function &getPlaces(Map $map): array
private function addNewMap(): int
{
$modify = new Modify(\Container::$dbConnection, 'maps');
$modify->fill([
'name' => self::$unnamedMapName,
'description' => '',
'bound_south_lat' => 0.0,
'bound_west_lng' => 0.0,
'bound_north_lat' => 0.0,
'bound_east_lng' => 0.0
]);
$modify->save();
return $modify->getId();
}
private function saveMapData(int $mapId, array $map): void
{
$modify = new Modify(\Container::$dbConnection, 'maps');
$modify->setId($mapId);
$modify->fill($map);
$modify->save();
}
private function &getPlaces(int $mapId): array
{
$select = new Select(\Container::$dbConnection, 'places');
$select->columns(['id', 'lat', 'lng', 'pano_id_cached', 'pano_id_cached_timestamp']);
$select->where('map_id', '=', $mapId);
$result = $select->execute();
$places = [];
foreach ($this->placeRepository->getAllForMap($map) as $place) {
$noPano = $place->getPanoIdCachedTimestampDate() !== null && $place->getPanoIdCached() === null;
while ($place = $result->fetch(IResultSet::FETCH_ASSOC)) {
//$panoId = ???
//$pov = ???
$noPano = $place['pano_id_cached_timestamp'] && $place['pano_id_cached'] === null;
$placeId = $place->getId();
$places[$placeId] = [
'id' => $placeId,
'lat' => $place->getLat(),
'lng' => $place->getLng(),
$places[$place['id']] = [
'id' => $place['id'],
'lat' => $place['lat'],
'lng' => $place['lng'],
'panoId' => null,
'pov' => $place->getPov()->toArray(),
'pov' => ['heading' => 0.0, 'pitch' => 0.0, 'zoom' => 0.0],
'noPano' => $noPano
];
}
+2 -5
View File
@@ -19,7 +19,6 @@ class MapsController
public function getMaps(): IContent
{
//TODO: from repository - count should be added
$select = new Select(\Container::$dbConnection, 'maps');
$select->columns([
['maps', 'id'],
@@ -46,10 +45,8 @@ class MapsController
}
$user = $this->request->user();
return new HtmlContent('maps', [
'maps' => $maps,
'isAdmin' => $user !== null && $user->hasPermission(IUser::PERMISSION_ADMIN)
]);
$data = ['maps' => $maps, 'isAdmin' => $user !== null && $user->hasPermission(IUser::PERMISSION_ADMIN)];
return new HtmlContent('maps', $data);
}
private function formatMapAreaForHuman(float $area): array
+192
View File
@@ -0,0 +1,192 @@
<?php namespace MapGuesser\Controller;
use MapGuesser\Database\Query\Modify;
use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\Interfaces\Response\IRedirect;
use MapGuesser\Mailing\Mail;
use MapGuesser\Model\User;
use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent;
use MapGuesser\Response\Redirect;
class SignupController
{
private IRequest $request;
public function __construct(IRequest $request)
{
$this->request = $request;
}
public function getSignupForm()
{
$session = $this->request->session();
if ($session->get('user')) {
return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY);
}
$data = [];
return new HtmlContent('signup/signup', $data);
}
public function signup(): IContent
{
$session = $this->request->session();
if ($session->get('user')) {
//TODO: return with some error
$data = ['success' => true];
return new JsonContent($data);
}
$select = new Select(\Container::$dbConnection, 'users');
$select->columns(User::getFields());
$select->where('email', '=', $this->request->post('email'));
$userData = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
if ($userData !== null) {
$user = new User($userData);
if ($user->getActive()) {
$data = ['error' => 'user_found'];
} else {
$data = ['error' => 'not_active_user_found'];
}
return new JsonContent($data);
}
if (strlen($this->request->post('password')) < 6) {
$data = ['error' => 'passwords_too_short'];
return new JsonContent($data);
}
if ($this->request->post('password') !== $this->request->post('password_confirm')) {
$data = ['error' => 'passwords_not_match'];
return new JsonContent($data);
}
$user = new User([
'email' => $this->request->post('email'),
]);
$user->setPlainPassword($this->request->post('password'));
\Container::$dbConnection->startTransaction();
$modify = new Modify(\Container::$dbConnection, 'users');
$modify->fill($user->toArray());
$modify->save();
$userId = $modify->getId();
$token = hash('sha256', serialize($user) . random_bytes(10) . microtime());
$modify = new Modify(\Container::$dbConnection, 'user_confirmations');
$modify->set('user_id', $userId);
$modify->set('token', $token);
$modify->save();
\Container::$dbConnection->commit();
$this->sendConfirmationEmail($user->getEmail(), $token);
$data = ['success' => true];
return new JsonContent($data);
}
public function activate()
{
$session = $this->request->session();
if ($session->get('user')) {
return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY);
}
$select = new Select(\Container::$dbConnection, 'user_confirmations');
$select->columns(['id', 'user_id']);
$select->where('token', '=', $this->request->query('token'));
$confirmation = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
if ($confirmation === null) {
$data = [];
return new HtmlContent('signup/activate', $data);
}
\Container::$dbConnection->startTransaction();
$modify = new Modify(\Container::$dbConnection, 'user_confirmations');
$modify->setId($confirmation['id']);
$modify->delete();
$modify = new Modify(\Container::$dbConnection, 'users');
$modify->setId($confirmation['user_id']);
$modify->set('active', true);
$modify->save();
\Container::$dbConnection->commit();
$select = new Select(\Container::$dbConnection, 'users');
$select->columns(User::getFields());
$select->whereId($confirmation['user_id']);
$userData = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
$user = new User($userData);
$session->set('user', $user);
return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY);
}
public function cancel()
{
$session = $this->request->session();
if ($session->get('user')) {
return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY);
}
$select = new Select(\Container::$dbConnection, 'user_confirmations');
$select->columns(['id', 'user_id']);
$select->where('token', '=', $this->request->query('token'));
$confirmation = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
if ($confirmation === null) {
$data = ['success' => false];
return new HtmlContent('signup/cancel', $data);
}
\Container::$dbConnection->startTransaction();
$modify = new Modify(\Container::$dbConnection, 'user_confirmations');
$modify->setId($confirmation['id']);
$modify->delete();
$modify = new Modify(\Container::$dbConnection, 'users');
$modify->setId($confirmation['user_id']);
$modify->delete();
\Container::$dbConnection->commit();
$data = ['success' => true];
return new HtmlContent('signup/cancel', $data);
}
private function sendConfirmationEmail($email, $token): void
{
$mail = new Mail();
$mail->addRecipient($email);
$mail->setSubject('Welcome to MapGuesser - Activate your account');
$mail->setBodyFromTemplate('signup', [
'EMAIL' => $email,
'ACTIVATE_LINK' => $this->request->getBase() . '/signup/activate/' . $token,
'CANCEL_LINK' => $this->request->getBase() . '/signup/cancel/' . $token,
]);
$mail->send();
}
}
+17 -192
View File
@@ -1,41 +1,19 @@
<?php namespace MapGuesser\Controller;
use DateTime;
use MapGuesser\Http\Request;
use MapGuesser\Database\Query\Modify;
use MapGuesser\Interfaces\Authorization\ISecured;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\Interfaces\Response\IRedirect;
use MapGuesser\OAuth\GoogleOAuth;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\PersistentData\Model\User;
use MapGuesser\Repository\UserConfirmationRepository;
use MapGuesser\Repository\UserPasswordResetterRepository;
use MapGuesser\Repository\UserPlayedPlaceRepository;
use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent;
use MapGuesser\Response\Redirect;
use MapGuesser\Util\JwtParser;
class UserController implements ISecured
{
private IRequest $request;
private PersistentDataManager $pdm;
private UserConfirmationRepository $userConfirmationRepository;
private UserPasswordResetterRepository $userPasswordResetterRepository;
private UserPlayedPlaceRepository $userPlayedPlaceRepository;
public function __construct(IRequest $request)
{
$this->request = $request;
$this->pdm = new PersistentDataManager();
$this->userConfirmationRepository = new UserConfirmationRepository();
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
$this->userPlayedPlaceRepository = new UserPlayedPlaceRepository();
}
public function authorize(): bool
@@ -45,195 +23,42 @@ class UserController implements ISecured
return $user !== null;
}
public function getAccount(): IContent
public function getProfile(): IContent
{
/**
* @var User $user
*/
$user = $this->request->user();
return new HtmlContent('account/account', ['user' => $user->toArray()]);
$data = ['user' => $user->toArray()];
return new HtmlContent('profile', $data);
}
public function getGoogleAuthenticateRedirect(): IRedirect
public function saveProfile(): IContent
{
/**
* @var User $user
*/
$user = $this->request->user();
$state = bin2hex(random_bytes(16));
$nonce = bin2hex(random_bytes(16));
$this->request->session()->set('oauth_state', $state);
$this->request->session()->set('oauth_nonce', $nonce);
$oAuth = new GoogleOAuth(new Request());
$url = $oAuth->getDialogUrl(
$state,
$this->request->getBase() . '/' . \Container::$routeCollection->getRoute('account.googleAuthenticate-action')->generateLink(),
$nonce,
$user->getEmail()
);
return new Redirect($url, IRedirect::TEMPORARY);
}
public function authenticateWithGoogle(): IContent
{
/**
* @var User $user
*/
$user = $this->request->user();
if ($this->request->query('state') !== $this->request->session()->get('oauth_state')) {
return new HtmlContent('account/google_authenticate', ['success' => false]);
}
$oAuth = new GoogleOAuth(new Request());
$tokenData = $oAuth->getToken(
$this->request->query('code'),
$this->request->getBase() . '/' . \Container::$routeCollection->getRoute('account.googleAuthenticate-action')->generateLink()
);
if (!isset($tokenData['id_token'])) {
return new HtmlContent('account/google_authenticate', ['success' => false]);
}
$jwtParser = new JwtParser($tokenData['id_token']);
$idToken = $jwtParser->getPayload();
if ($idToken['nonce'] !== $this->request->session()->get('oauth_nonce')) {
return new HtmlContent('account/google_authenticate', ['success' => false]);
}
if ($idToken['sub'] !== $user->getGoogleSub()) {
return new HtmlContent('account/google_authenticate', [
'success' => false,
'errorText' => 'This Google account is not linked to your account.'
]);
}
$authenticatedWithGoogleUntil = new DateTime('+45 seconds');
$this->request->session()->set('authenticated_with_google_until', $authenticatedWithGoogleUntil);
return new HtmlContent('account/google_authenticate', [
'success' => true,
'authenticatedWithGoogleUntil' => $authenticatedWithGoogleUntil
]);
}
public function getDeleteAccount(): IContent
{
/**
* @var User $user
*/
$user = $this->request->user();
return new HtmlContent('account/delete', ['user' => $user->toArray()]);
}
public function saveAccount(): IContent
{
/**
* @var User $user
*/
$user = $this->request->user();
if (!$this->confirmUserIdentity(
$user,
$this->request->session()->get('authenticated_with_google_until'),
$this->request->post('password'),
$error
)) {
return new JsonContent(['error' => ['errorText' => $error]]);
if (!$user->checkPassword($this->request->post('password'))) {
$data = ['error' => 'password_not_match'];
return new JsonContent($data);
}
if (strlen($this->request->post('password_new')) > 0) {
if (strlen($this->request->post('password_new')) < 6) {
return new JsonContent([
'error' => [
'errorText' => 'The given new password is too short. Please choose a password that is at least 6 characters long!'
]
]);
$data = ['error' => 'passwords_too_short'];
return new JsonContent($data);
}
if ($this->request->post('password_new') !== $this->request->post('password_new_confirm')) {
return new JsonContent([
'error' => [
'errorText' => 'The given new passwords do not match.'
]
]);
$data = ['error' => 'passwords_not_match'];
return new JsonContent($data);
}
$user->setPlainPassword($this->request->post('password_new'));
}
$this->pdm->saveToDb($user);
$modify = new Modify(\Container::$dbConnection, 'users');
$modify->fill($user->toArray());
$modify->save();
$this->request->session()->delete('authenticated_with_google_until');
return new JsonContent(['success' => true]);
}
public function deleteAccount(): IContent
{
/**
* @var User $user
*/
$user = $this->request->user();
if (!$this->confirmUserIdentity(
$user,
$this->request->session()->get('authenticated_with_google_until'),
$this->request->post('password'),
$error
)) {
return new JsonContent(['error' => ['errorText' => $error]]);
}
\Container::$dbConnection->startTransaction();
$userConfirmation = $this->userConfirmationRepository->getByUser($user);
if ($userConfirmation !== null) {
$this->pdm->deleteFromDb($userConfirmation);
}
$userPasswordResetter = $this->userPasswordResetterRepository->getByUser($user);
if ($userPasswordResetter !== null) {
$this->pdm->deleteFromDb($userPasswordResetter);
}
foreach ($this->userPlayedPlaceRepository->getAllByUser($user) as $userPlayedPlace) {
$this->pdm->deleteFromDb($userPlayedPlace);
}
$this->pdm->deleteFromDb($user);
\Container::$dbConnection->commit();
$this->request->session()->delete('authenticated_with_google_until');
return new JsonContent(['success' => true]);
}
private function confirmUserIdentity(User $user, ?DateTime $authenticatedWithGoogleUntil, ?string $password, ?string &$error): bool
{
if ($authenticatedWithGoogleUntil !== null && $authenticatedWithGoogleUntil > new DateTime()) {
return true;
}
if ($password !== null) {
if ($user->checkPassword($password)) {
return true;
}
$error = 'The given current password is wrong.';
return false;
}
$error = 'Could not confirm your identity. Please try again!';
return false;
$data = ['success' => true];
return new JsonContent($data);
}
}
+2 -2
View File
@@ -12,11 +12,11 @@ class Connection implements IConnection
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');
$port = ini_get('mysqli.default_port');
}
if ($socket === null) {
$socket = (string) ini_get('mysqli.default_socket');
$socket = ini_get('mysqli.default_socket');
}
$this->connection = new mysqli($host, $user, $password, $db, $port, $socket);
+41 -5
View File
@@ -1,6 +1,7 @@
<?php namespace MapGuesser\Database\Query;
use MapGuesser\Interfaces\Database\IConnection;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Database\Utils;
class Modify
@@ -13,6 +14,8 @@ class Modify
private array $attributes = [];
private array $original = [];
private ?string $externalId = null;
private bool $autoIncrement = true;
@@ -113,15 +116,48 @@ class Modify
private function update(): void
{
$attributes = $this->attributes;
unset($attributes[$this->idName]);
/*$diff = $this->generateDiff();
$set = $this->generateColumnsWithBinding(array_keys($attributes));
if (count($diff) === 0) {
return;
}*/
$diff = $this->attributes;
unset($diff[$this->idName]);
$set = $this->generateColumnsWithBinding(array_keys($diff));
$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]]));
$stmt->execute(array_merge($diff, [$this->idName => $this->attributes[$this->idName]]));
}
private function readFromDB(array $columns): void
{
$select = (new Select($this->connection, $this->table))
->setIdName($this->idName)
->whereId($this->attributes[$this->idName])
->columns($columns);
$this->original = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
}
private function generateDiff(): array
{
$this->readFromDB(array_keys($this->attributes));
$diff = [];
foreach ($this->attributes as $name => $value) {
$original = $this->original[$name];
if ($original != $value) {
$diff[$name] = $value;
}
}
return $diff;
}
public static function generateColumnsWithBinding(array $columns): string
@@ -135,6 +171,6 @@ class Modify
private function generateKey(): string
{
return substr(hash('sha256', serialize($this->attributes) . random_bytes(5) . microtime()), 0, 7);
return substr(hash('sha256', serialize($this->attributes) . random_bytes(10) . microtime()), 0, 7);
}
}
+21 -64
View File
@@ -12,8 +12,6 @@ class Select
const CONDITION_HAVING = 1;
const DERIVED_TABLE_KEY = 'DERIVED';
private IConnection $connection;
private string $table;
@@ -32,15 +30,12 @@ class Select
private array $orders = [];
private ?array $limit;
private array $limit;
public function __construct(IConnection $connection, ?string $table = null)
public function __construct(IConnection $connection, string $table)
{
$this->connection = $connection;
if ($table !== null) {
$this->table = $table;
}
$this->table = $table;
}
public function setIdName(string $idName): Select
@@ -57,18 +52,6 @@ class Select
return $this;
}
public function setDerivedTableAlias(string $tableAlias): Select
{
return $this->setTableAliases([Select::DERIVED_TABLE_KEY => $tableAlias]);
}
public function from(string $table): Select
{
$this->table = $table;
return $this;
}
public function columns(array $columns): Select
{
$this->columns = array_merge($this->columns, $columns);
@@ -151,7 +134,7 @@ class Select
$this->limit = null;
}
public function paginate(int $page, int $itemsPerPage): Select
public function paginate(int $page, int $itemsPerPage)
{
$this->limit($itemsPerPage, ($page - 1) * $itemsPerPage);
@@ -201,11 +184,6 @@ class Select
}
}
private function isDerivedTable(): bool
{
return array_key_exists(Select::DERIVED_TABLE_KEY, $this->tableAliases);
}
private function addJoin(string $type, $table, $column1, string $relation, $column2): void
{
$this->joins[] = [$type, $table, $column1, $relation, $column2];
@@ -223,14 +201,10 @@ class Select
private function generateQuery(): array
{
list($tableQuery, $tableParams) = $this->generateTable($this->table, true);
$queryString = 'SELECT ' . $this->generateColumns() . ' FROM ' . $tableQuery;
$queryString = 'SELECT ' . $this->generateColumns() . ' FROM ' . $this->generateTable($this->table, true);
if (count($this->joins) > 0) {
list($joinQuery, $joinParams) = $this->generateJoins();
$queryString .= ' ' . $joinQuery;
} else {
$joinParams = [];
$queryString .= ' ' . $this->generateJoins();
}
if (count($this->conditions[self::CONDITION_WHERE]) > 0) {
@@ -261,32 +235,20 @@ class Select
$queryString .= ' LIMIT ' . $this->limit[1] . ', ' . $this->limit[0];
}
if($this->isDerivedTable()) {
$queryString = '(' . $queryString . ') AS ' . $this->tableAliases[Select::DERIVED_TABLE_KEY];
}
return [$queryString, array_merge($tableParams, $joinParams, $whereParams, $havingParams)];
return [$queryString, array_merge($whereParams, $havingParams)];
}
private function generateTable($table, bool $defineAlias = false): array
private function generateTable($table, bool $defineAlias = false): string
{
$params = [];
if ($table instanceof RawExpression) {
return [(string) $table, $params];
}
if($table instanceof Select)
{
return $table->generateQuery();
return (string) $table;
}
if (isset($this->tableAliases[$table])) {
$queryString = ($defineAlias ? Utils::backtick($this->tableAliases[$table]) . ' ' . Utils::backtick($table) : Utils::backtick($table));
return [$queryString, $params];
return ($defineAlias ? Utils::backtick($this->tableAliases[$table]) . ' ' . Utils::backtick($table) : Utils::backtick($table));
}
return [Utils::backtick($table), $params];
return Utils::backtick($table);
}
private function generateColumn($column): string
@@ -299,8 +261,7 @@ class Select
$out = '';
if ($column[0]) {
list($tableName, $params) = $this->generateTable($column[0]);
$out .= $tableName . '.';
$out .= $this->generateTable($column[0]) . '.';
}
$out .= Utils::backtick($column[1]);
@@ -326,22 +287,18 @@ class Select
return implode(',', $columns);
}
private function generateJoins(): array
private function generateJoins(): string
{
$joins = $this->joins;
$joinQueries = [];
$params = [];
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]);
});
foreach($this->joins as $join) {
list($joinQueryFragment, $paramsFragment) = $this->generateTable($join[1], true);
$joinQueries[] = $join[0] . ' JOIN ' . $joinQueryFragment . ' ON ' . $this->generateColumn($join[2]) . ' ' . $join[3] . ' ' . $this->generateColumn($join[4]);
$params = array_merge($params, $paramsFragment);
}
return [implode(' ', $joinQueries), $params];
return implode(' ', $joins);
}
private function generateConditions(int $type): array
private function generateConditions(string $type): array
{
$conditions = '';
$params = [];
@@ -409,9 +366,9 @@ class Select
return [$conditionsString, $params];
}
private function generateComplexConditionFragment(int $type, Closure $conditionCallback): array
private function generateComplexConditionFragment(string $type, Closure $conditionCallback): array
{
$instance = new self($this->connection, $this->table);
$instance = new static($this->connection, $this->table);
$instance->tableAliases = $this->tableAliases;
$conditionCallback($instance);
+1 -2
View File
@@ -1,8 +1,7 @@
<?php namespace MapGuesser\Database;
class Utils {
public static function backtick(string $name): string
{
public static function backtick(string $name) {
return '`' . $name . '`';
}
}
+13 -22
View File
@@ -1,10 +1,11 @@
<?php namespace MapGuesser\Http;
use MapGuesser\Interfaces\Http\IRequest;
use MapGuesser\Interfaces\Http\IResponse;
class Request implements IRequest
class Request
{
const HTTP_GET = 0;
const HTTP_POST = 1;
private string $url;
private int $method;
@@ -13,23 +14,13 @@ class Request implements IRequest
private array $headers = [];
public function __construct(string $url = '', int $method = self::HTTP_GET)
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
public function setQuery($query)
{
if (is_string($query)) {
$this->query = $query;
@@ -38,22 +29,22 @@ class Request implements IRequest
}
}
public function setHeaders(array $headers): void
public function setHeaders(array $headers)
{
$this->headers = array_merge($this->headers, $headers);
}
public function send(): IResponse
public function send(): Response
{
$ch = curl_init();
if ($this->method === self::HTTP_POST) {
$url = $this->url;
if ($this->method === self::HTTP_GET) {
$url = $this->url . '?' . $this->query;
} elseif ($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);
+3 -5
View File
@@ -1,8 +1,6 @@
<?php namespace MapGuesser\Http;
use MapGuesser\Interfaces\Http\IResponse;
class Response implements IResponse
class Response
{
private string $body;
@@ -14,12 +12,12 @@ class Response implements IResponse
$this->headers = $headers;
}
public function getBody(): string
public function getBody()
{
return $this->body;
}
public function getHeaders(): array
public function getHeaders()
{
return $this->headers;
}
-4
View File
@@ -8,9 +8,5 @@ interface IUser
public function hasPermission(int $permission): bool;
public function getUniqueId();
public function getDisplayName(): string;
public function checkPassword(string $password): bool;
}
-18
View File
@@ -1,18 +0,0 @@
<?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
@@ -1,8 +0,0 @@
<?php namespace MapGuesser\Interfaces\Http;
interface IResponse
{
public function getBody(): string;
public function getHeaders(): array;
}
+1 -3
View File
@@ -4,7 +4,7 @@ use MapGuesser\Interfaces\Authentication\IUser;
interface IRequest
{
public function setParsedRouteParams(array &$routeParams): void;
public function setParsedRouteParams(array &$routeParams);
public function getBase(): string;
@@ -14,7 +14,5 @@ interface IRequest
public function session(): ISession;
public function setUser(?IUser $user): void;
public function user(): ?IUser;
}
+2 -4
View File
@@ -2,11 +2,9 @@
interface IContent
{
public function setData(array $data): void;
public function &getData(): array;
public function getData(): array;
public function render(): void;
public function &render(): string;
public function getContentType(): string;
}
@@ -1,9 +0,0 @@
<?php namespace MapGuesser\Interfaces\Session;
use SessionHandlerInterface;
use SessionIdInterface;
use SessionUpdateTimestampHandlerInterface;
interface ISessionHandler extends SessionHandlerInterface, SessionIdInterface, SessionUpdateTimestampHandlerInterface
{
}
+3 -10
View File
@@ -29,13 +29,6 @@ class Mail
{
$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);
}
@@ -65,8 +58,8 @@ class Mail
$mailer->Mailer = 'mail';
}
$mailer->setFrom($_ENV['MAIL_FROM'], $_ENV['APP_NAME']);
$mailer->addReplyTo($_ENV['MAIL_FROM'], $_ENV['APP_NAME']);
$mailer->setFrom($_ENV['MAIL_FROM'], 'MapGuesser');
$mailer->addReplyTo($_ENV['MAIL_FROM'], 'MapGuesser');
$mailer->Sender = !empty($_ENV['MAIL_BOUNCE']) ? $_ENV['MAIL_BOUNCE'] : $_ENV['MAIL_FROM'];
$mailer->Subject = $this->subject;
@@ -77,7 +70,7 @@ class Mail
}
}
private function sendMail(PHPMailer $mailer, array $recipient): void
private function sendMail(PHPMailer $mailer, array $recipient)
{
$mailer->clearAddresses();
$mailer->addAddress($recipient[0], $recipient[1]);
+49
View File
@@ -0,0 +1,49 @@
<?php namespace MapGuesser\Model;
abstract class BaseModel
{
protected static array $fields;
protected $id = null;
public static function getFields(): array
{
return array_merge(['id'], static::$fields);
}
public function __construct(array $data)
{
foreach ($data as $key => $value) {
$method = 'set' . str_replace('_', '', ucwords($key, '_'));
if (method_exists($this, $method)) {
$this->$method($value);
}
}
}
public function setId($id): void
{
$this->id = $id;
}
public function getId()
{
return $this->id;
}
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;
}
}
@@ -1,34 +1,27 @@
<?php namespace MapGuesser\PersistentData\Model;
<?php namespace MapGuesser\Model;
use DateTime;
use MapGuesser\Interfaces\Authentication\IUser;
class User extends Model implements IUser
class User extends BaseModel implements IUser
{
protected static string $table = 'users';
protected static array $fields = ['email', 'password', 'type', 'active', 'google_sub', 'created'];
private static array $types = ['user', 'admin'];
private string $email = '';
protected static array $fields = ['email', 'password', 'type', 'active'];
private ?string $password = null;
private string $email;
private string $password;
private string $type = 'user';
private bool $active = false;
private ?string $googleSub = null;
private DateTime $created;
public function setEmail(string $email): void
{
$this->email = $email;
}
public function setPassword(?string $hashedPassword): void
public function setPassword(string $hashedPassword): void
{
$this->password = $hashedPassword;
}
@@ -45,24 +38,9 @@ class User extends Model implements IUser
}
}
public function setActive(bool $active): void
public function setActive($active): void
{
$this->active = $active;
}
public function setGoogleSub(?string $googleSub): void
{
$this->googleSub = $googleSub;
}
public function setCreatedDate(DateTime $created): void
{
$this->created = $created;
}
public function setCreated(string $created): void
{
$this->created = new DateTime($created);
$this->active = (bool) $active;
}
public function getEmail(): string
@@ -70,7 +48,7 @@ class User extends Model implements IUser
return $this->email;
}
public function getPassword(): ?string
public function getPassword(): string
{
return $this->password;
}
@@ -85,38 +63,18 @@ class User extends Model implements IUser
return $this->active;
}
public function getGoogleSub(): ?string
{
return $this->googleSub;
}
public function getCreatedDate(): DateTime
{
return $this->created;
}
public function getCreated(): string
{
return $this->created->format('Y-m-d H:i:s');
}
public function hasPermission(int $permission): bool
{
switch ($permission) {
case IUser::PERMISSION_NORMAL:
return true;
break;
case IUser::PERMISSION_ADMIN:
return $this->type === 'admin';
default:
throw new \Exception('Permission does not exist: ' . $permission);
break;
}
}
public function getUniqueId()
{
return $this->id;
}
public function getDisplayName(): string
{
return $this->email;
@@ -124,10 +82,6 @@ class User extends Model implements IUser
public function checkPassword(string $password): bool
{
if ($this->password === null) {
return false;
}
return password_verify($password, $this->password);
}
}
-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
@@ -1,56 +0,0 @@
<?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);
}
}
-103
View File
@@ -1,103 +0,0 @@
<?php namespace MapGuesser\PersistentData\Model;
use MapGuesser\Util\Geo\Bounds;
class Map extends Model
{
protected static string $table = 'maps';
protected static array $fields = ['name', 'description', 'bound_south_lat', 'bound_west_lng', 'bound_north_lat', 'bound_east_lng', 'area'];
private string $name = '';
private string $description = '';
private Bounds $bounds;
private float $area = 0.0;
public function __construct()
{
$this->bounds = Bounds::createDirectly(-90.0, -180.0, 90.0, 180.0);
}
public function setName(string $name): void
{
$this->name = $name;
}
public function setDescription(string $description): void
{
$this->description = $description;
}
public function setBounds(Bounds $bounds): void
{
$this->bounds = $bounds;
}
public function setBoundSouthLat(float $bound_south_lat): void
{
$this->bounds->setSouthLat($bound_south_lat);
}
public function setBoundWestLng(float $bound_west_lng): void
{
$this->bounds->setWestLng($bound_west_lng);
}
public function setBoundNorthLat(float $bound_north_lat): void
{
$this->bounds->setNorthLat($bound_north_lat);
}
public function setBoundEastLng(float $bound_east_lng): void
{
$this->bounds->setEastLng($bound_east_lng);
}
public function setArea(float $area): void
{
$this->area = $area;
}
public function getName(): string
{
return $this->name;
}
public function getDescription(): string
{
return $this->description;
}
public function getBounds(): Bounds
{
return $this->bounds;
}
public function getBoundSouthLat(): float
{
return $this->bounds->getSouthLat();
}
public function getBoundWestLng(): float
{
return $this->bounds->getWestLng();
}
public function getBoundNorthLat(): float
{
return $this->bounds->getNorthLat();
}
public function getBoundEastLng(): float
{
return $this->bounds->getEastLng();
}
public function getArea(): float
{
return $this->area;
}
}
-69
View File
@@ -1,69 +0,0 @@
<?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;
}
}
-88
View File
@@ -1,88 +0,0 @@
<?php namespace MapGuesser\PersistentData\Model;
use DateTime;
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');
}
}
-188
View File
@@ -1,188 +0,0 @@
<?php namespace MapGuesser\PersistentData\Model;
use DateInterval;
use DateTime;
use MapGuesser\Http\Request;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Util\Geo\Position;
use MapGuesser\Util\Panorama\Pov;
class Place extends Model
{
protected static string $table = 'places';
protected static array $fields = ['map_id', 'lat', 'lng', 'pano_id_cached', 'pano_id_cached_timestamp', 'pov_heading', 'pov_pitch', 'pov_zoom'];
protected static array $relations = ['map' => Map::class];
private ?Map $map = null;
private ?int $mapId = null;
private Position $position;
private ?string $panoIdCached = null;
private ?DateTime $panoIdCachedTimestamp = null;
private Pov $pov;
public function __construct()
{
$this->position = new Position(0.0, 0.0);
$this->pov = new Pov(0.0, 0.0, 0.0);
}
public function setMap(Map $map): void
{
$this->map = $map;
}
public function setMapId(int $mapId): void
{
$this->mapId = $mapId;
}
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 setPov(Pov $pov): void
{
$this->pov = $pov;
}
public function setPovHeading(float $heading): void
{
$this->pov->setHeading($heading);
}
public function setPovPitch(float $pitch): void
{
$this->pov->setPitch($pitch);
}
public function setPovZoom(float $zoom): void
{
$this->pov->setZoom($zoom);
}
public function setPanoIdCached(?string $panoIdCached): void
{
$this->panoIdCached = $panoIdCached;
}
public function setPanoIdCachedTimestampDate(?DateTime $panoIdCachedTimestamp): void
{
$this->panoIdCachedTimestamp = $panoIdCachedTimestamp;
}
public function setPanoIdCachedTimestamp(?string $panoIdCachedTimestamp): void
{
if ($panoIdCachedTimestamp !== null) {
$this->panoIdCachedTimestamp = new DateTime($panoIdCachedTimestamp);
}
}
public function getMap(): ?Map
{
return $this->map;
}
public function getMapId(): ?int
{
return $this->mapId;
}
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 getPov(): Pov
{
return $this->pov;
}
public function getPovHeading(): float
{
return $this->pov->getHeading();
}
public function getPovPitch(): float
{
return $this->pov->getPitch();
}
public function getPovZoom(): float
{
return $this->pov->getZoom();
}
public function getFreshPanoId(): ?string
{
if (
$this->panoIdCachedTimestamp !== null &&
(clone $this->panoIdCachedTimestamp)->add(new DateInterval('P1D')) > new DateTime()
) {
return $this->panoIdCached;
}
$request = new Request('https://maps.googleapis.com/maps/api/streetview/metadata', Request::HTTP_GET);
$request->setQuery([
'key' => $_ENV['GOOGLE_MAPS_SERVER_API_KEY'],
'location' => $this->position->getLat() . ',' . $this->position->getLng()
]);
$response = $request->send();
$panoData = json_decode($response->getBody(), true);
$panoId = $panoData['status'] === 'OK' ? $panoData['pano_id'] : null;
$this->panoIdCached = $panoId;
$this->panoIdCachedTimestamp = new DateTime();
(new PersistentDataManager())->saveToDb($this);
return $panoId;
}
public function getPanoIdCached(): ?string
{
return $this->panoIdCached;
}
public function getPanoIdCachedTimestampDate(): ?DateTime
{
return $this->panoIdCachedTimestamp;
}
public function getPanoIdCachedTimestamp(): ?string
{
if ($this->panoIdCachedTimestamp === null) {
return null;
}
return $this->panoIdCachedTimestamp->format('Y-m-d H:i:s');
}
}
@@ -1,70 +0,0 @@
<?php namespace MapGuesser\PersistentData\Model;
use DateTime;
class UserConfirmation extends Model
{
protected static string $table = 'user_confirmations';
protected static array $fields = ['user_id', 'token', 'last_sent'];
protected static array $relations = ['user' => User::class];
private ?User $user = null;
private ?int $userId = null;
private string $token = '';
private DateTime $lastSent;
public function setUser(User $user): void
{
$this->user = $user;
}
public function setUserId(int $userId): void
{
$this->userId = $userId;
}
public function setToken(string $token): void
{
$this->token = $token;
}
public function setLastSentDate(DateTime $lastSent): void
{
$this->lastSent = $lastSent;
}
public function setLastSent(string $lastSent): void
{
$this->lastSent = new DateTime($lastSent);
}
public function getUser(): ?User
{
return $this->user;
}
public function getUserId(): ?int
{
return $this->userId;
}
public function getToken(): string
{
return $this->token;
}
public function getLastSentDate(): DateTime
{
return $this->lastSent;
}
public function getLastSent(): string
{
return $this->lastSent->format('Y-m-d H:i:s');
}
}
@@ -1,70 +0,0 @@
<?php namespace MapGuesser\PersistentData\Model;
use DateTime;
class UserPasswordResetter extends Model
{
protected static string $table = 'user_password_resetters';
protected static array $fields = ['user_id', 'token', 'expires'];
protected static array $relations = ['user' => User::class];
private ?User $user = null;
private ?int $userId = null;
private string $token = '';
private DateTime $expires;
public function setUser(User $user): void
{
$this->user = $user;
}
public function setUserId(int $userId): void
{
$this->userId = $userId;
}
public function setToken(string $token): void
{
$this->token = $token;
}
public function setExpiresDate(DateTime $expires): void
{
$this->expires = $expires;
}
public function setExpires(string $expires): void
{
$this->expires = new DateTime($expires);
}
public function getUser(): ?User
{
return $this->user;
}
public function getUserId(): ?int
{
return $this->userId;
}
public function getToken(): string
{
return $this->token;
}
public function getExpiresDate(): DateTime
{
return $this->expires;
}
public function getExpires(): string
{
return $this->expires->format('Y-m-d H:i:s');
}
}
@@ -1,99 +0,0 @@
<?php namespace MapGuesser\PersistentData\Model;
use DateTime;
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;
}
}
@@ -1,226 +0,0 @@
<?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());
}
}
}
}
+7 -10
View File
@@ -1,19 +1,16 @@
<?php namespace MapGuesser\Repository;
use MapGuesser\PersistentData\Model\Map;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Database\IResultSet;
class MapRepository
{
private PersistentDataManager $pdm;
public function __construct()
public function getById(int $mapId)
{
$this->pdm = new PersistentDataManager();
}
$select = new Select(\Container::$dbConnection, 'maps');
$select->columns(['id', 'name', 'description', 'bound_south_lat', 'bound_west_lng', 'bound_north_lat', 'bound_east_lng', 'area']);
$select->whereId($mapId);
public function getById(int $mapId): ?Map
{
return $this->pdm->selectFromDbById($mapId, Map::class);
return $select->execute()->fetch(IResultSet::FETCH_ASSOC);
}
}
-38
View File
@@ -1,38 +0,0 @@
<?php namespace MapGuesser\Repository;
use DateTime;
use Generator;
use MapGuesser\Database\Query\Select;
use MapGuesser\PersistentData\Model\MultiRoom;
use MapGuesser\PersistentData\PersistentDataManager;
class MultiRoomRepository
{
private PersistentDataManager $pdm;
public function __construct()
{
$this->pdm = new PersistentDataManager();
}
public function getById(int $id): ?MultiRoom
{
return $this->pdm->selectFromDbById($id, MultiRoom::class);
}
public function getByRoomId(string $roomId): ?MultiRoom
{
$select = new Select(\Container::$dbConnection);
$select->where('room_id', '=', $roomId);
return $this->pdm->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 $this->pdm->selectMultipleFromDb($select, MultiRoom::class);
}
}
Loaded 100 of 163 files, more files were not shown because too many files have changed in this diff. Show more