From 74e1b25fcfdb769e439eb80c3d3aa2a89545a261 Mon Sep 17 00:00:00 2001 From: Michel Fedde <35878897+Neintonine@users.noreply.github.com> Date: Fri, 5 Jul 2024 22:29:35 +0200 Subject: [PATCH] implermented key import --- composer.json | 4 +- composer.lock | 346 +++++++++++++++++- deploy/Vagrantfile | 40 +- src/file-index.json | 2 +- src/js/pages/keys.ts | 155 ++++++++ src/php/DoctrineManager.php | 2 + src/php/Entities/Games/Game.php | 26 ++ src/php/Entities/Games/Key.php | 39 ++ src/php/Entities/Games/KeyAttribute.php | 22 ++ src/php/Entities/Games/Store.php | 15 + src/php/Importer/GameImporter.php | 165 +++++++++ .../Importer/ImportColumnInterpretation.php | 26 ++ .../Api/Web/ImportKeysPrepareRoute.php | 50 +++ src/php/Routing/Api/Web/ImportKeysRoute.php | 53 +++ src/php/Routing/Api/Web/WebAPIRoutes.php | 3 + src/php/Routing/KeysRoute.php | 41 +++ src/php/Routing/Router.php | 2 + src/templates/layout/main.php | 4 + src/templates/pages/key-manager.php | 51 +++ webpack.config.js | 1 + 20 files changed, 1035 insertions(+), 12 deletions(-) create mode 100644 src/js/pages/keys.ts create mode 100644 src/php/Entities/Games/Game.php create mode 100644 src/php/Entities/Games/Key.php create mode 100644 src/php/Entities/Games/KeyAttribute.php create mode 100644 src/php/Entities/Games/Store.php create mode 100644 src/php/Importer/GameImporter.php create mode 100644 src/php/Importer/ImportColumnInterpretation.php create mode 100644 src/php/Routing/Api/Web/ImportKeysPrepareRoute.php create mode 100644 src/php/Routing/Api/Web/ImportKeysRoute.php create mode 100644 src/php/Routing/KeysRoute.php create mode 100644 src/templates/pages/key-manager.php diff --git a/composer.json b/composer.json index b59f2d5..a2bed0f 100644 --- a/composer.json +++ b/composer.json @@ -18,8 +18,6 @@ "symfony/uid": "^7.1", "php-curl-class/php-curl-class": "^9.19", "symfony/cache": "^7.1", - }, - "require-dev": { - "ext-xdebug": "*" + "phpoffice/phpspreadsheet": "^2.1" } } diff --git a/composer.lock b/composer.lock index b7b1891..5ac48fa 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3dbd3cfcf406f3c8c63570303d3d7942", + "content-hash": "89e0df3a5a9ac052a0102b007850e150", "packages": [ { "name": "doctrine/collections", @@ -1334,6 +1334,194 @@ ], "time": "2021-07-30T08:33:09+00:00" }, + { + "name": "maennchen/zipstream-php", + "version": "3.1.0", + "source": { + "type": "git", + "url": "https://github.com/maennchen/ZipStream-PHP.git", + "reference": "b8174494eda667f7d13876b4a7bfef0f62a7c0d1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/b8174494eda667f7d13876b4a7bfef0f62a7c0d1", + "reference": "b8174494eda667f7d13876b4a7bfef0f62a7c0d1", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "ext-zlib": "*", + "php-64bit": "^8.1" + }, + "require-dev": { + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.16", + "guzzlehttp/guzzle": "^7.5", + "mikey179/vfsstream": "^1.6", + "php-coveralls/php-coveralls": "^2.5", + "phpunit/phpunit": "^10.0", + "vimeo/psalm": "^5.0" + }, + "suggest": { + "guzzlehttp/psr7": "^2.4", + "psr/http-message": "^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "ZipStream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paul Duncan", + "email": "pabs@pablotron.org" + }, + { + "name": "Jonatan Männchen", + "email": "jonatan@maennchen.ch" + }, + { + "name": "Jesse Donat", + "email": "donatj@gmail.com" + }, + { + "name": "András Kolesár", + "email": "kolesar@kolesar.hu" + } + ], + "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.", + "keywords": [ + "stream", + "zip" + ], + "support": { + "issues": "https://github.com/maennchen/ZipStream-PHP/issues", + "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.1.0" + }, + "funding": [ + { + "url": "https://github.com/maennchen", + "type": "github" + }, + { + "url": "https://opencollective.com/zipstream", + "type": "open_collective" + } + ], + "time": "2023-06-21T14:59:35+00:00" + }, + { + "name": "markbaker/complex", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPComplex.git", + "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9", + "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "phpcompatibility/php-compatibility": "^9.3", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Complex\\": "classes/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@lange.demon.co.uk" + } + ], + "description": "PHP Class for working with complex numbers", + "homepage": "https://github.com/MarkBaker/PHPComplex", + "keywords": [ + "complex", + "mathematics" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPComplex/issues", + "source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2" + }, + "time": "2022-12-06T16:21:08+00:00" + }, + { + "name": "markbaker/matrix", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPMatrix.git", + "reference": "728434227fe21be27ff6d86621a1b13107a2562c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c", + "reference": "728434227fe21be27ff6d86621a1b13107a2562c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "phpcompatibility/php-compatibility": "^9.3", + "phpdocumentor/phpdocumentor": "2.*", + "phploc/phploc": "^4.0", + "phpmd/phpmd": "2.*", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "sebastian/phpcpd": "^4.0", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Matrix\\": "classes/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@demon-angel.eu" + } + ], + "description": "PHP Class for working with matrices", + "homepage": "https://github.com/MarkBaker/PHPMatrix", + "keywords": [ + "mathematics", + "matrix", + "vector" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPMatrix/issues", + "source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1" + }, + "time": "2022-12-02T22:17:43+00:00" + }, { "name": "nikic/fast-route", "version": "v1.3.0", @@ -1529,6 +1717,110 @@ }, "time": "2024-04-09T18:03:13+00:00" }, + { + "name": "phpoffice/phpspreadsheet", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", + "reference": "dbed77bd3a0f68f96c0dd68ad4499d5674fecc3e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/dbed77bd3a0f68f96c0dd68ad4499d5674fecc3e", + "reference": "dbed77bd3a0f68f96c0dd68ad4499d5674fecc3e", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-dom": "*", + "ext-fileinfo": "*", + "ext-gd": "*", + "ext-iconv": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "ext-xml": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "ext-zip": "*", + "ext-zlib": "*", + "maennchen/zipstream-php": "^2.1 || ^3.0", + "markbaker/complex": "^3.0", + "markbaker/matrix": "^3.0", + "php": "^8.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-main", + "dompdf/dompdf": "^2.0", + "friendsofphp/php-cs-fixer": "^3.2", + "mitoteam/jpgraph": "^10.3", + "mpdf/mpdf": "^8.1.1", + "phpcompatibility/php-compatibility": "^9.3", + "phpstan/phpstan": "^1.1", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^9.6", + "squizlabs/php_codesniffer": "^3.7", + "tecnickcom/tcpdf": "^6.5" + }, + "suggest": { + "dompdf/dompdf": "Option for rendering PDF with PDF Writer", + "ext-intl": "PHP Internationalization Functions", + "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers", + "mpdf/mpdf": "Option for rendering PDF with PDF Writer", + "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Maarten Balliauw", + "homepage": "https://blog.maartenballiauw.be" + }, + { + "name": "Mark Baker", + "homepage": "https://markbakeruk.net" + }, + { + "name": "Franck Lefevre", + "homepage": "https://rootslabs.net" + }, + { + "name": "Erik Tilt" + }, + { + "name": "Adrien Crivelli" + } + ], + "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine", + "homepage": "https://github.com/PHPOffice/PhpSpreadsheet", + "keywords": [ + "OpenXML", + "excel", + "gnumeric", + "ods", + "php", + "spreadsheet", + "xls", + "xlsx" + ], + "support": { + "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", + "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/2.1.0" + }, + "time": "2024-05-11T04:17:56+00:00" + }, { "name": "phpoption/phpoption", "version": "1.9.2", @@ -1706,6 +1998,58 @@ }, "time": "2021-11-05T16:47:00+00:00" }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, { "name": "psr/http-factory", "version": "1.1.0", diff --git a/deploy/Vagrantfile b/deploy/Vagrantfile index d9a5e60..1ed5e06 100644 --- a/deploy/Vagrantfile +++ b/deploy/Vagrantfile @@ -1,13 +1,39 @@ Vagrant.configure("2") do |config| + config.vm.boot_timeout = 600 - config.vm.box = "vagrant-php83-mysql" + config.vm.box = "ubuntu/focal64" - config.vm.network "forwarded_port", guest: 80, host: 8080 + config.vm.network :forwarded_port, guest: 80, host: 1111 - config.vm.synced_folder "../../games-shop", "/var/www/html" + config.vm.synced_folder "../../games-shop", "/var/www/html" - config.vm.provision "shell", inline: <<-SHELL - sudo rm /etc/nginx/sites-enabled/default - sudo ln -s /var/www/html/deploy/nginx-file /etc/nginx/sites-enabled/default - SHELL + config.vm.provision "shell", inline: <<-SHELL + sudo apt update + + # Install dependencies + sudo apt install ca-certificates apt-transport-https software-properties-common lsb-release debian-keyring debian-archive-keyring curl -y + sudo add-apt-repository ppa:ondrej/php -y + + sudo apt update + + sudo apt install nginx php8.3 php8.3-{cli,fpm,curl,gd,zip,fileinfo,common,sqlite3,dom,mbstring,simplexml,xml,xmlreader,xmlwriter} -y + + sudo ufw allow "OpenSSH" + sudo ufw allow "Nginx HTTP" + sudo ufw --force enable + + sudo rm /etc/nginx/sites-enabled/default + sudo ln -s /var/www/html/deploy/nginx-file /etc/nginx/sites-enabled/default + + sudo systemctl enable --now nginx + sudo systemctl restart nginx + + php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" + php -r "if (hash_file('sha384', 'composer-setup.php') === 'dac665fdc30fdd8ec78b38b9800061b4150413ff2e3b6f88543c636f7cd84f6db9189d43a81e5503cda447da73c7e5b6') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" + php composer-setup.php + php -r "unlink('composer-setup.php');" + + sudo mv composer.phar /usr/local/bin/composer + + SHELL end diff --git a/src/file-index.json b/src/file-index.json index 4c8ab9b..6a93645 100644 --- a/src/file-index.json +++ b/src/file-index.json @@ -1 +1 @@ -{"index":{"js":["js/runtime.js","js/vendors.js","js/index.js"],"css":["css/vendors.css","css/common.css"]},"admin/accounts":{"js":["js/runtime.js","js/vendors.js","js/admin/accounts.js"],"css":["css/vendors.css","css/common.css"]}} \ No newline at end of file +{"index":{"js":["js/runtime.js","js/vendors.js","js/index.js"],"css":["css/vendors.css","css/common.css"]},"admin/accounts":{"js":["js/runtime.js","js/vendors.js","js/admin/accounts.js"],"css":["css/vendors.css","css/common.css"]},"keys":{"js":["js/runtime.js","js/vendors.js","js/keys.js"],"css":["css/vendors.css","css/common.css"]}} \ No newline at end of file diff --git a/src/js/pages/keys.ts b/src/js/pages/keys.ts new file mode 100644 index 0000000..94ae972 --- /dev/null +++ b/src/js/pages/keys.ts @@ -0,0 +1,155 @@ +import '../../css/common/index.scss'; +import '../common/index'; + +import 'datatables.net-bs5/css/dataTables.bootstrap5.css'; + +import { Tab } from 'bootstrap'; + +async function checkImportFile() { + const fileInput = document.querySelector('#import'); + + if (!fileInput) { + return; + } + + if (!fileInput.files || !fileInput.files[0]) { + return; + } + + const formData = new FormData(); + formData.set('file', fileInput.files[0]); + + const response = await fetch( + `/api/web/keys/import/prepare`, + { + method: 'POST', + body: formData + } + ); + + if (!response.ok) { + throw new Error(response.statusText); + } + + const jsonData = await response.json(); + + const container = document.getElementById('import-info-container'); + if (!container) { + return + } + + container.classList.remove('d-none'); + createTableContents(jsonData); +} + +function createTableContents(data: {index: string, displayName: string, guessedAttribute: string}[]) { + const table = document.querySelector('#import-attribute-table'); + if (!table) { + return; + } + + const tbody = table.querySelector('tbody'); + if (!tbody) { + return; + } + + for (let child of tbody.children) { + child.remove(); + } + + const keyAttributeMeta = document.querySelector('meta[name="key-attributes"]')?.content; + const attributes = JSON.parse(keyAttributeMeta ?? ''); + + const select = document.createElement('select'); + select.classList.add('form-select', 'w-100'); + for (let attributeName in attributes) { + const attributeValue = attributes[attributeName]; + const option = document.createElement('option'); + option.value = attributeValue; + option.textContent = attributeName; + + select.add(option); + } + + data.forEach((datum) => { + const row = tbody.insertRow(); + const indexCell = row.insertCell(); + indexCell.classList.add('index-cell'); + indexCell.innerText = datum.index; + + const displayNameCEll = row.insertCell(); + displayNameCEll.classList.add('name-cell'); + displayNameCEll.innerText = datum.displayName; + + const attributeCell = row.insertCell(); + attributeCell.classList.add('attribute-cell'); + const rowSelect = select.cloneNode(true); + rowSelect.value = datum.guessedAttribute; + attributeCell.appendChild(rowSelect); + }) +} + +async function doImport() { + const fileInput = document.querySelector('#import'); + + if (!fileInput) { + return; + } + + if (!fileInput.files || !fileInput.files[0]) { + return; + } + + const formData = new FormData(); + formData.set('file', fileInput.files[0]); + + const tbody = document.querySelector('#import-attribute-table tbody'); + if (!tbody) { + return; + } + + for (let row of tbody.querySelectorAll('tr')) { + const columnIndex = row.querySelector('.index-cell')?.textContent; + const attribute = row.querySelector('.attribute-cell select')?.value ?? 'none'; + + formData.set(`columns[${columnIndex}]`, attribute); + } + + const response = await fetch( + `/api/web/keys/import/perform`, + { + method: 'POST', + body: formData + } + ); + + if (!response.ok) { + throw new Error(response.statusText); + } + + const container = document.getElementById('import-info-container'); + if (!container) { + return + } + + container.classList.add('d-none'); + fileInput.value = ''; +} + +document.addEventListener('DOMContentLoaded', () => { + const triggerTabList = document.querySelectorAll('#key-tab button') + triggerTabList.forEach(triggerEl => { + const tabTrigger = new Tab(triggerEl) + + triggerEl.addEventListener('click', event => { + event.preventDefault() + tabTrigger.show() + }) + }) + + const importButton = document.querySelector('.js--send-import'); + importButton?.addEventListener('click', checkImportFile); + + const doImportButton = document.querySelector('.js--do-import'); + doImportButton?.addEventListener('click', doImport); +}) \ No newline at end of file diff --git a/src/php/DoctrineManager.php b/src/php/DoctrineManager.php index ca0f674..9545e45 100644 --- a/src/php/DoctrineManager.php +++ b/src/php/DoctrineManager.php @@ -6,6 +6,7 @@ namespace GamesShop; use Doctrine\DBAL\Connection; use Doctrine\DBAL\DriverManager; use Doctrine\ORM\EntityManager; +use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\ORMSetup; use GamesShop\Environment\EnvironmentHandler; @@ -27,6 +28,7 @@ final class DoctrineManager $entityManager = new EntityManager($connection, $config); $container->addShared(EntityManager::class, $entityManager); + $container->addShared(EntityManagerInterface::class, $entityManager); $container->addShared(Connection::class, $connection); } diff --git a/src/php/Entities/Games/Game.php b/src/php/Entities/Games/Game.php new file mode 100644 index 0000000..31c2239 --- /dev/null +++ b/src/php/Entities/Games/Game.php @@ -0,0 +1,26 @@ + true])] + #[ORM\GeneratedValue] + private int|null $id; + #[ORM\Column] + private string $name; + + /** + * @param string $name + */ + public function __construct(string $name) + { + $this->name = $name; + } +} \ No newline at end of file diff --git a/src/php/Entities/Games/Key.php b/src/php/Entities/Games/Key.php new file mode 100644 index 0000000..9f76bce --- /dev/null +++ b/src/php/Entities/Games/Key.php @@ -0,0 +1,39 @@ + true])] + #[ORM\GeneratedValue] + private int|null $id; + #[ORM\ManyToOne] + private Game $game; + #[ORM\ManyToOne] + private User $contributedUser; + #[ORM\Column] + private string $key; + #[ORM\Column(type: 'string', enumType: Store::class)] + private Store $store; + #[ORM\Column(nullable: true)] + private string|null $storeLink; + #[ORM\Column] + private string|null $fromWhere; + + public function __construct(Game $game, User $contributedUser, string $key, Store $store, ?string $storeLink, ?string $fromWhere) + { + $this->game = $game; + $this->contributedUser = $contributedUser; + $this->key = $key; + $this->store = $store; + $this->storeLink = $storeLink; + $this->fromWhere = $fromWhere; + } +} \ No newline at end of file diff --git a/src/php/Entities/Games/KeyAttribute.php b/src/php/Entities/Games/KeyAttribute.php new file mode 100644 index 0000000..345458f --- /dev/null +++ b/src/php/Entities/Games/KeyAttribute.php @@ -0,0 +1,22 @@ +name] = $case->value; + } + + return $result; + } +} \ No newline at end of file diff --git a/src/php/Entities/Games/Store.php b/src/php/Entities/Games/Store.php new file mode 100644 index 0000000..0f423b4 --- /dev/null +++ b/src/php/Entities/Games/Store.php @@ -0,0 +1,15 @@ + Store::EPICGAMES, + 'ea' => Store::ORIGIN, + 'eaplay' => Store::ORIGIN, + 'ubisoft' => Store::UPLAY, + 'activision' => Store::BATTLENET + ]; + + public function __construct( + private readonly EntityManagerInterface $entityManager, + ) { } + + /** + * @param string $path + * @return ImportColumnInterpretation[] + */ + public function interpret(string $path): array { + $spreadsheet = IOFactory::load($path); + + $worksheet = $spreadsheet->getSheet(0); + $result = []; + foreach ($worksheet->getColumnIterator() as $column) { + $columnIndex = $column->getColumnIndex(); + + $value = $worksheet->getCell(sprintf('%s%d', $columnIndex, self::HEADER_ROW_INDEX))->getValueString(); + + if (empty(trim($value))) { + continue; + } + + $guessedAttribute = $this->guessAttribute($value); + $result[] = new ImportColumnInterpretation( + $columnIndex, + $value, + $guessedAttribute + ); + } + return $result; + } + + private function guessAttribute(string $value): KeyAttribute|null { + $value = trim($value); + $value = strtolower($value); + $value = str_replace(' ', '_', $value); + + $attribute = match($value) { + 'key' => KeyAttribute::KEY, + 'name', 'game_name', 'game' => KeyAttribute::GAME_NAME, + 'from' => KeyAttribute::FROM, + 'store', 'for' => KeyAttribute::STORE, + default => null + }; + + return $attribute; + } + + /** + * @param string[] $columnDefinitions + */ + public function import(string $path, array $columnDefinitions, User $contributedUser): array { + $spreadsheet = IOFactory::load($path); + + $worksheet = $spreadsheet->getSheet(0); + + $totalRows = 0; + $addedAmount = 0; + + foreach ($worksheet->getRowIterator(self::HEADER_ROW_INDEX + 1) as $row) { + $totalRows++; + $values = [ + 'name' => null, + 'key' => null, + 'from' => null, + 'store' => null, + 'store_link' => null + ]; + + foreach ($columnDefinitions as $columnIndex => $attribute) { + $value = $worksheet->getCell(sprintf('%s%d', $columnIndex, $row->getRowIndex()))->getValueString(); + + switch(KeyAttribute::from($attribute)) { + case KeyAttribute::NONE: + break; + case KeyAttribute::GAME_NAME: + $values['name'] = $value; + break; + case KeyAttribute::KEY: + $values['key'] = $value; + break; + case KeyAttribute::FROM: + $values['from'] = $value; + break; + case KeyAttribute::STORE: + $store = $this->interpretStore($value); + $values['store'] = $store; + if ($store === Store::EXTERNAL) { + $values['store_link'] = $value; + } + break; + } + } + + if ($values['key'] === null || $values['name'] === null || $values['store'] === null) { + continue; + } + + $game = $this->entityManager->getRepository(Game::class)->findOneBy([ 'name' => $values['name'] ]); + if ($game === null) { + $game = new Game($values['name']); + } + + $key = new Key( + $game, + $contributedUser, + $values['key'], + $values['store'], + $values['store_link'], + $values['from'], + ); + + $this->entityManager->persist($game); + $this->entityManager->persist($key); + $addedAmount++; + } + + $this->entityManager->flush(); + + return [$totalRows, $addedAmount]; + } + + private function interpretStore(string $storeString): Store { + $storeString = trim($storeString); + $storeString = strtolower($storeString); + $storeString = str_replace(' ', '', $storeString); + + $triedConversion = Store::tryFrom($storeString); + if ($triedConversion !== null) { + return $triedConversion; + } + + if (array_key_exists($storeString, self::STORE_ADDITIONAL_CASES)) { + return self::STORE_ADDITIONAL_CASES[$storeString]; + } + + return Store::EXTERNAL; + } +} \ No newline at end of file diff --git a/src/php/Importer/ImportColumnInterpretation.php b/src/php/Importer/ImportColumnInterpretation.php new file mode 100644 index 0000000..1e5b97a --- /dev/null +++ b/src/php/Importer/ImportColumnInterpretation.php @@ -0,0 +1,26 @@ + $this->index, + 'displayName' => $this->displayName, + 'guessedAttribute' => $this->guessedAttribute ?? null + ]; + } +} \ No newline at end of file diff --git a/src/php/Routing/Api/Web/ImportKeysPrepareRoute.php b/src/php/Routing/Api/Web/ImportKeysPrepareRoute.php new file mode 100644 index 0000000..ba9cea4 --- /dev/null +++ b/src/php/Routing/Api/Web/ImportKeysPrepareRoute.php @@ -0,0 +1,50 @@ +loginHandler->isLoggedIn()) { + throw new UnauthorizedException(); + } + + $user = $this->loginHandler->getCurrentUser(); + if (!$user->getPermission()->hasLevel(UserPermission::PROVIDER)) { + throw new ForbiddenException(); + } + + /** + * @var UploadedFile $file + */ + $file = $request->getUploadedFiles()['file'] ?? null; + if (!$file === null) { + throw new BadRequestException(); + } + + $fileName = tempnam(sys_get_temp_dir(), 'ImportKeys'); + $file->moveTo($fileName); + $results = $this->importer->interpret($fileName); + unlink($fileName); + + return new JsonResponse($results); + } +} \ No newline at end of file diff --git a/src/php/Routing/Api/Web/ImportKeysRoute.php b/src/php/Routing/Api/Web/ImportKeysRoute.php new file mode 100644 index 0000000..17c00ac --- /dev/null +++ b/src/php/Routing/Api/Web/ImportKeysRoute.php @@ -0,0 +1,53 @@ +loginHandler->isLoggedIn()) { + throw new UnauthorizedException(); + } + + $user = $this->loginHandler->getCurrentUser(); + if (!$user->getPermission()->hasLevel(UserPermission::PROVIDER)) { + throw new ForbiddenException(); + } + + /** + * @var UploadedFile $file + */ + $file = $request->getUploadedFiles()['file'] ?? null; + if (!$file === null) { + throw new BadRequestException(); + } + + $fileName = tempnam(sys_get_temp_dir(), 'ImportKeys'); + $file->moveTo($fileName); + + $columnDefs = $request->getParsedBody()['columns']; + + [$total, $imported] = $this->importer->import($fileName, $columnDefs, $user); + unlink($fileName); + + return new JsonResponse([ 'success' => true, 'total' => $total, 'imported' => $imported ]); + } +} \ No newline at end of file diff --git a/src/php/Routing/Api/Web/WebAPIRoutes.php b/src/php/Routing/Api/Web/WebAPIRoutes.php index fefc155..4007296 100644 --- a/src/php/Routing/Api/Web/WebAPIRoutes.php +++ b/src/php/Routing/Api/Web/WebAPIRoutes.php @@ -9,5 +9,8 @@ final class WebAPIRoutes { public static function applyRoutes(RouteGroup $group): void { $group->post('/users/{id:number}', UserModifyRoute::class); + + $group->post('/keys/import/prepare', ImportKeysPrepareRoute::class); + $group->post('/keys/import/perform', ImportKeysRoute::class); } } \ No newline at end of file diff --git a/src/php/Routing/KeysRoute.php b/src/php/Routing/KeysRoute.php new file mode 100644 index 0000000..977c095 --- /dev/null +++ b/src/php/Routing/KeysRoute.php @@ -0,0 +1,41 @@ +loginHandler->isLoggedIn()) { + throw new UnauthorizedException(); + } + + $user = $this->loginHandler->getCurrentUser(); + if (!$user->getPermission()->hasLevel(UserPermission::PROVIDER)) { + throw new ForbiddenException(); + } + + + return new TemplateResponse('key-manager'); + } + + public static function applyRoutes(\League\Route\Router $router): void + { + $router->get('/keys', KeysRoute::class); + } +} \ No newline at end of file diff --git a/src/php/Routing/Router.php b/src/php/Routing/Router.php index ce32605..0df7188 100644 --- a/src/php/Routing/Router.php +++ b/src/php/Routing/Router.php @@ -42,6 +42,8 @@ final class Router IndexRoute::applyRoutes($router); LoginRoutes::addRoutes($router); SetupRoute::applyRoutes($router); + + KeysRoute::applyRoutes($router); AdminAccountConfigRoute::applyRoutes($router); APIRoutes::applyRoutes($router); diff --git a/src/templates/layout/main.php b/src/templates/layout/main.php index 472b100..78d6de1 100644 --- a/src/templates/layout/main.php +++ b/src/templates/layout/main.php @@ -41,5 +41,9 @@ $resource = $resources->getResource($resourceEntry); section('modal') ?> +
+ PROTOTYPE / POC +
+ diff --git a/src/templates/pages/key-manager.php b/src/templates/pages/key-manager.php new file mode 100644 index 0000000..6a44231 --- /dev/null +++ b/src/templates/pages/key-manager.php @@ -0,0 +1,51 @@ +layout('layout/main', [ 'resourceEntry' => 'keys' ]); +?> + + + +
+
+ Key Table +
+
+ + +
+ + +
+ +
+ + + + + + + + + +
ColumnHeaderAttribute
+ + +
+
+
\ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index fa8251c..82d3389 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -93,6 +93,7 @@ module.exports = { entry: { index: JS_FOLDER + "/pages/index", 'admin/accounts': JS_FOLDER + "/pages/admin/accounts", + keys: JS_FOLDER + "/pages/keys", }, output: { path: PUBLIC_FOLDER,