This commit is contained in:
Andrew 2022-05-16 01:28:21 +07:00
parent 1d087064dd
commit ee0715c3b2
19 changed files with 1525 additions and 0 deletions

2
w12/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*.jpg
/vendor

5
w12/.htaccess Normal file
View file

@ -0,0 +1,5 @@
RewriteEngine on
RewriteCond %{SCRIPT_FILENAME} !-f
RewriteCond %{SCRIPT_FILENAME} !-d
RewriteCond %{SCRIPT_FILENAME} !-l
RewriteRule ^(.*)$ index.php/$1

21
w12/composer.json Normal file
View file

@ -0,0 +1,21 @@
{
"name": "nuark/w12",
"description": "Work 12 app",
"type": "project",
"license": "MIT",
"autoload": {
"psr-4": {
"Nuark\\W12\\": "src/"
}
},
"authors": [
{
"name": "Andrew",
"email": "me@nuark.xyz"
}
],
"require": {
"pecee/simple-router": "4.3.7.2",
"twig/twig": "^3.3"
}
}

323
w12/composer.lock generated Normal file
View file

@ -0,0 +1,323 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "28d991162beec76e49d4e126da6f453e",
"packages": [
{
"name": "pecee/simple-router",
"version": "4.3.7.2",
"source": {
"type": "git",
"url": "https://github.com/skipperbent/simple-php-router.git",
"reference": "032a2ae7e0e2d876599758f85b61bc965a63ea7c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/skipperbent/simple-php-router/zipball/032a2ae7e0e2d876599758f85b61bc965a63ea7c",
"reference": "032a2ae7e0e2d876599758f85b61bc965a63ea7c",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": ">=7.1"
},
"require-dev": {
"mockery/mockery": "^1",
"phpstan/phpstan": "^0",
"phpstan/phpstan-deprecation-rules": "^0",
"phpstan/phpstan-phpunit": "^0",
"phpstan/phpstan-strict-rules": "^0",
"phpunit/phpunit": "^7"
},
"type": "library",
"autoload": {
"psr-4": {
"Pecee\\": "src/Pecee/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Simon Sessingø",
"email": "simon.sessingoe@gmail.com"
}
],
"description": "Simple, fast PHP router that is easy to get integrated and in almost any project. Heavily inspired by the Laravel router.",
"keywords": [
"framework",
"input-handler",
"laravel",
"pecee",
"php",
"request-handler",
"route",
"router",
"routing",
"routing-engine",
"simple-php-router",
"url-handling"
],
"support": {
"issues": "https://github.com/skipperbent/simple-php-router/issues",
"source": "https://github.com/skipperbent/simple-php-router/issues"
},
"time": "2021-07-17T23:45:36+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.25.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "30885182c981ab175d4d034db0f6f469898070ab"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab",
"reference": "30885182c981ab175d4d034db0f6f469898070ab",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"provide": {
"ext-ctype": "*"
},
"suggest": {
"ext-ctype": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Ctype\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Gert de Pagter",
"email": "BackEndTea@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for ctype functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"ctype",
"polyfill",
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.25.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-10-20T20:35:02+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.25.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/0abb51d2f102e00a4eefcf46ba7fec406d245825",
"reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"provide": {
"ext-mbstring": "*"
},
"suggest": {
"ext-mbstring": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Mbstring extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"mbstring",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.25.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-11-30T18:21:41+00:00"
},
{
"name": "twig/twig",
"version": "v3.3.10",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
"reference": "8442df056c51b706793adf80a9fd363406dd3674"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/8442df056c51b706793adf80a9fd363406dd3674",
"reference": "8442df056c51b706793adf80a9fd363406dd3674",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/polyfill-ctype": "^1.8",
"symfony/polyfill-mbstring": "^1.3"
},
"require-dev": {
"psr/container": "^1.0",
"symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.3-dev"
}
},
"autoload": {
"psr-4": {
"Twig\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com",
"homepage": "http://fabien.potencier.org",
"role": "Lead Developer"
},
{
"name": "Twig Team",
"role": "Contributors"
},
{
"name": "Armin Ronacher",
"email": "armin.ronacher@active-4.com",
"role": "Project Founder"
}
],
"description": "Twig, the flexible, fast, and secure template language for PHP",
"homepage": "https://twig.symfony.com",
"keywords": [
"templating"
],
"support": {
"issues": "https://github.com/twigphp/Twig/issues",
"source": "https://github.com/twigphp/Twig/tree/v3.3.10"
},
"funding": [
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/twig/twig",
"type": "tidelift"
}
],
"time": "2022-04-06T06:47:41+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"plugin-api-version": "2.3.0"
}

436
w12/index.php Normal file
View file

@ -0,0 +1,436 @@
<?php
require_once 'vendor/autoload.php';
require_once 'vendor/pecee/simple-router/helpers.php';
require_once 'src/database.php';
require_once 'src/helpers.php';
use Pecee\SimpleRouter\SimpleRouter;
$loader = new \Twig\Loader\FilesystemLoader('views');
$twig = new \Twig\Environment($loader, [
'cache' => false,
]);
// start session
session_start();
// create favicon route
SimpleRouter::get('/favicon.ico', function() {
return;
});
// create main page route
SimpleRouter::get('/', function() use($twig) {
if (getSessionVariable('user') !== null) {
$user = getSessionVariable('user');
echo $twig->render('index.twig', [
'user' => $user,
'menu' => json_decode(Database::getUserMenu($user)["data"]),
'gallery' => Database::getPublishedImages()
]);
} else {
echo $twig->render('propose-auth.twig');
}
});
// create register route
SimpleRouter::get('/register', function() use($twig) {
if (getSessionVariable('user') !== null) {
return response()->redirect('/');
}
// get error from get parameters
$error = input()->get('error', '');
echo $twig->render('register.twig', [
'error' => htmlspecialchars($error),
]);
});
// create register POST route
SimpleRouter::post('/register', function() {
$login = trim(input()->post('login', ''));
$password = trim(input()->post('password', ''));
// if login or password is empty, redirect to register page with error
if (empty($login) || empty($password)) {
return response()->redirect('/register?error=Empty login or password given!');
}
// try create user if ok - redirect to login page with message
try {
Database::createUser($login, $password);
$user = Database::getUser($login);
Database::createEmptyMenuForUser($user);
response()->redirect('/login?message=User created');
} catch (PDOException $e) {
response()->redirect('/register?error=User already exists!');
}
});
// create login route
SimpleRouter::get('/login', function() use($twig) {
if (getSessionVariable('user') !== null) {
return response()->redirect('/');
}
// get message from get parameters
$message = input()->get('message', '');
// get error from get parameters
$error = input()->get('error', '');
echo $twig->render('login.twig', [
'message' => htmlspecialchars($message),
'error' => htmlspecialchars($error),
]);
});
// create login POST route
SimpleRouter::post('/login', function() {
$login = trim(input()->post('login', ''));
$password = trim(input()->post('password', ''));
// if login or password is empty, redirect to register page with error
if (empty($login) || empty($password)) {
return response()->redirect('/login?error=Empty login or password given!');
}
if (!Database::userExists($login)) {
return response()->redirect('/login?error=User does not exist!');
}
if (!Database::verifyUser($login, $password)) {
return response()->redirect('/login?error=Wrong password!');
}
$user = Database::getUser($login);
// set session variables
setSessionVariable('user', $user);
// redirect to index page
response()->redirect('/');
});
// create logout route
SimpleRouter::get('/logout', function() {
// unset session variables
unsetSessionVariable('user');
// redirect to index page
response()->redirect('/');
});
// create menu editing route
SimpleRouter::get('/editMenu', function() use($twig) {
if (getSessionVariable('user') === null) {
return response()->redirect('/');
}
$user = getSessionVariable('user');
echo $twig->render('edit-menu.twig', [
'user' => $user,
'menu' => json_decode(Database::getUserMenu($user)["data"]),
]);
});
// create menu save POST route
SimpleRouter::post('/editMenu', function() {
if (getSessionVariable('user') === null) {
return response()->json([
'error' => "Not authorized",
'code' => 403,
]);
}
try {
$user = getSessionVariable('user');
$menuData = [];
$menu = input()->post('menu', []);
for ($i = 0; $i < count($menu->value); $i++) {
array_push($menuData, [
'url' => $menu->value[$i]->value['url']->value,
'name' => $menu->value[$i]->value['name']->value,
]);
}
Database::setUserMenu($user, json_encode($menuData));
response()->json([
'data' => json_encode($menuData),
'code' => 200,
]);
} catch (PDOException $e) {
response()->json([
'error' => json_encode($e->getMessage()),
'code' => 500,
]);
}
});
// create tables lookup route
SimpleRouter::get('/lookupTables', function() use($twig) {
if (getSessionVariable('user') === null) {
return response()->redirect('/');
}
$tables = [];
$tableData = [];
$user = getSessionVariable('user');
$table = input()->get('table', '');
$sort = input()->get('sort', 'DESC');
$col = input()->get('col', '');
if ($table !== '') {
$cols = Database::getTableColumns($table);
if ($col === '') {
$col = $cols[0]['column_name'];
} else {
$col = $col->value;
}
$tableData = [
"header" => $cols,
"data" => Database::getAnyTableSorted($table, $col, $sort),
];
} else {
$tables = array_map(function($tbl) {
return $tbl["table_name"];
}, Database::getAvailableTables());
}
echo $twig->render('lookup-tables.twig', [
'user' => $user,
'table' => $table,
'tables' => $tables,
'tableData' => $tableData
]);
});
// create POST router for image uploading
SimpleRouter::post('/uploadImage', function() {
if (getSessionVariable('user') === null) {
return response()->json([
'error' => "Not authorized",
'code' => 403,
]);
}
$user = getSessionVariable('user');
$file = input()->file('file');
$fileType = $file->type;
if ($fileType === 'image/jpeg') {
$filename = $user['login'] . '_' . time() . '.jpg';
$fullpath = 'data/full/' . $filename;
$file->move($fullpath);
$size = getimagesize($fullpath);
$ratio = $size[0]/$size[1]; // width/height
if( $ratio > 1) {
$width = 200;
$height = 200/$ratio;
}
else {
$width = 200*$ratio;
$height = 200;
}
$thumb = imagecreatetruecolor($width, $height);
$source = imagecreatefromjpeg($fullpath);
imagecopyresized($thumb, $source, 0, 0, 0, 0, $width, $height, $size[0], $size[1]);
$color = imagecolorallocate($thumb, 255, 0, 0);
imagestring($thumb, 2, 2, 2, "Watermark text :)", $color);
imagejpeg($thumb, 'data/thumb/' . $filename);
try {
Database::addImageToGallery($filename, $user);
} catch (PDOException $e) {
return response()->json([
'error' => json_encode($e->getMessage()),
'code' => 500,
]);
}
return response()->json([
'code' => 200,
]);
} else {
return response()->json([
'error' => "Wrong file type",
'code' => 400,
]);
}
});
// create gallery route
SimpleRouter::get('/gallery', function() use($twig) {
if (getSessionVariable('user') === null) {
return response()->redirect('/');
}
$user = getSessionVariable('user');
$images = Database::getUsersImages($user);
echo $twig->render('gallery.twig', [
'user' => $user,
'images' => $images,
]);
});
// create image route
SimpleRouter::get('/image/{id}', function($id) use($twig) {
if (getSessionVariable('user') === null) {
return response()->redirect('/');
}
$user = getSessionVariable('user');
$image = Database::getImage($id);
if ($image['user_id'] !== $user['id']) {
return response()->httpCode(403);
}
echo $twig->render('image.twig', [
'user' => $user,
'image' => $image,
]);
});
// create image info save POST route
SimpleRouter::post('/image/{id}', function($id) {
if (getSessionVariable('user') === null) {
return response()->redirect('/');
}
$user = getSessionVariable('user');
$image = Database::getImage($id);
if ($image['user_id'] !== $user['id']) {
return response()->httpCode(403);
}
$description = input()->post('description', '')->value;
$published = boolval(input()->post('published', 0)->value)?"true":"false";
var_dump($published);
try {
Database::updateImage($id, $description, $published);
} catch (PDOException $e) {
return response()->json([
'error' => json_encode($e->getMessage()),
'code' => 500,
]);
}
response()->redirect('/');
});
// create image DELETE route
SimpleRouter::get('/image/{id}/delete', function($id) {
if (getSessionVariable('user') === null) {
return response()->redirect('/');
}
$user = getSessionVariable('user');
$image = Database::getImage($id);
if ($image['user_id'] !== $user['id']) {
return response()->httpCode(403);
}
unlink('data/full/' . $image['filename']);
unlink('data/thumb/' . $image['filename']);
Database::deleteImage($id);
response()->redirect('/gallery');
});
// create image serving route
SimpleRouter::all('/images', function() {
$type = input()->get('type', 'thumb');
$filename = input()->get('filename', '');
if (file_exists("data/$type/$filename")) {
header("Content-type: image/jpeg");
return file_get_contents("data/$type/$filename");
} else {
response()->httpCode(404);
}
});
// create export route
SimpleRouter::get('/export', function() {
if (getSessionVariable('user') === null) {
return response()->redirect('/');
}
header("Content-type: application/xml");
echo Database::exportUsersXML();
});
// create import route
// create gallery route
SimpleRouter::get('/import', function() use($twig) {
if (getSessionVariable('user') === null) {
return response()->redirect('/');
}
$user = getSessionVariable('user');
$message = input()->get('message', '');
$error = input()->get('error', '');
echo $twig->render('import.twig', [
'user' => $user,
'message' => htmlspecialchars($message),
'error' => htmlspecialchars($error),
]);
});
// create import POST route
SimpleRouter::post('/import', function() {
if (getSessionVariable('user') === null) {
return response()->redirect('/');
}
$xml = input()->post('xml')->value;
if ($xml) {
try {
list($ok, $fail) = Database::importUsersXML($xml);
response()->redirect("/import?message=OK: $ok, FAIL: $fail");
} catch (Exception $e) {
response()->redirect("/import?error=" . $e->getMessage());
}
} else {
response()->redirect('/import?error=Error happened ');
}
});
// create image search route
SimpleRouter::get('/search', function() use($twig) {
if (getSessionVariable('user') === null) {
return response()->redirect('/');
}
$user = getSessionVariable('user');
$search = input()->get('search', '')->value;
$author = input()->get('author', '')->value;
$authors = Database::getUsersList();
$images = [];
if ($search and $author) {
$images = Database::searchImages("%".$search."%", $author);
}
echo $twig->render('search.twig', [
'user' => $user,
'images' => $images,
'search' => $search,
'author' => $author,
'authors' => $authors,
]);
});
// create recover password route
SimpleRouter::get('/recover-password', function() use($twig) {
$message = input()->get('message', '');
$error = input()->get('error', '');
$mail = input()->get('mail', '');
echo $twig->render('recover-password.twig', [
'message' => htmlspecialchars($message),
'error' => htmlspecialchars($error),
'mail' => $mail,
]);
});
// create recover password POST route
SimpleRouter::post('/recover-password', function() use ($twig) {
$login = input()->post('login', '')->value;
if ($login) {
try {
$user = Database::getUser($login);
if ($user) {
$newPassword = Database::generateUserNewPassword($login);
$sentMail = $twig->render('recover-password-mail.twig', [
'login' => $login,
'password' => $newPassword,
]);
response()->redirect('/recover-password?message=Success! We sent you an email with your new password&mail=' . urlencode($sentMail));
} else {
response()->redirect('/recover-password?error=User not found');
}
} catch (PDOException $e) {
response()->redirect('/recover-password?error=' . $e->getMessage());
}
} else {
response()->redirect('/recover-password?error=Error happened');
}
});
SimpleRouter::start();

222
w12/src/database.php Normal file
View file

@ -0,0 +1,222 @@
<?php
// simple database PDO utilizing the Postgres driver
class Database {
private static $db;
private static $dsn = 'pgsql:host=localhost;port=5432;dbname=w12';
private static $username = 'postgres';
private static $password = 'asarch6122';
private static $options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
public static function getDB() {
if (!isset(self::$db)) {
try {
self::$db = new PDO(self::$dsn, self::$username, self::$password, self::$options);
} catch (PDOException $e) {
throw new PDOException($e->getMessage(), (int)$e->getCode());
}
}
return self::$db;
}
public static function userExists($login) {
$db = self::getDB();
$query = $db->prepare('SELECT * FROM users WHERE login = :login');
$query->bindParam(':login', $login);
$query->execute();
return $query->rowCount() > 0;
}
public static function createUser($login, $password) {
$db = self::getDB();
$query = $db->prepare('INSERT INTO users (login, password) VALUES (:login, :password)');
$query->bindParam(':login', $login);
$query->bindParam(':password', $password);
$query->execute();
}
public static function verifyUser($login, $password) {
$db = self::getDB();
$query = $db->prepare('SELECT * FROM users WHERE login = :login AND password = :password');
$query->bindParam(':login', $login);
$query->bindParam(':password', $password);
$query->execute();
return $query->rowCount() > 0;
}
public static function getUser($login) {
$db = self::getDB();
$query = $db->prepare('SELECT * FROM users WHERE login = :login');
$query->bindParam(':login', $login);
$query->execute();
return $query->fetch();
}
public static function generateUserNewPassword($login) {
$db = self::getDB();
$query = $db->prepare('UPDATE users SET password = :password WHERE login = :login');
$query->bindParam(':login', $login);
$query->bindParam(':password', $password);
$password = bin2hex(random_bytes(8));
$query->execute();
return $password;
}
public static function getUsersList() {
$db = self::getDB();
$query = $db->prepare('SELECT id, login FROM users');
$query->execute();
return $query->fetchAll();
}
public static function createEmptyMenuForUser($user) {
$db = self::getDB();
$query = $db->prepare('INSERT INTO menu (user_id) VALUES (:user_id)');
$query->bindParam(':user_id', $user["id"]);
$query->execute();
}
public static function getUserMenu($user) {
$db = self::getDB();
$query = $db->prepare('SELECT data FROM menu WHERE user_id = :user_id');
$query->bindParam(':user_id', $user["id"]);
$query->execute();
return $query->fetch();
}
public static function setUserMenu($user, $data) {
$db = self::getDB();
$query = $db->prepare('UPDATE menu SET data = :data WHERE user_id = :user_id');
$query->bindParam(':data', $data);
$query->bindParam(':user_id', $user["id"]);
$query->execute();
}
public static function addImageToGallery($filename, $user) {
$db = self::getDB();
$query = $db->prepare('INSERT INTO images (user_id, filename) VALUES (:user_id, :filename)');
$query->bindParam(':user_id', $user["id"]);
$query->bindParam(':filename', $filename);
$query->execute();
}
public static function getUsersImages($user) {
$db = self::getDB();
$query = $db->prepare('SELECT id, filename FROM images WHERE user_id = :user_id');
$query->bindParam(':user_id', $user["id"]);
$query->execute();
return $query->fetchAll();
}
public static function getPublishedImages() {
$db = self::getDB();
$query = $db->prepare('SELECT id, filename FROM images WHERE published = TRUE');
$query->execute();
return $query->fetchAll();
}
public static function getImage($id) {
$db = self::getDB();
$query = $db->prepare('SELECT * FROM images WHERE id = :id');
$query->bindParam(':id', $id);
$query->execute();
return $query->fetch();
}
public static function searchImages($search, $author) {
$db = self::getDB();
if (intval($author) !== -1) {
$query = $db->prepare('SELECT * FROM images WHERE description LIKE :search AND user_id = :author');
$query->bindParam(':search', $search);
$query->bindParam(':author', $author);
} else {
$query = $db->prepare('SELECT * FROM images WHERE description LIKE :search');
$query->bindParam(':search', $search);
}
$query->execute();
return $query->fetchAll();
}
public static function updateImage($id, $description, $published) {
$db = self::getDB();
$query = $db->prepare('UPDATE images SET published = :published, description = :description WHERE id = :id');
$query->bindParam(':id', $id);
$query->bindParam(':published', $published);
$query->bindParam(':description', $description);
$query->execute();
}
public static function deleteImage($id) {
$db = self::getDB();
$query = $db->prepare('DELETE FROM images WHERE id = :id');
$query->bindParam(':id', $id);
$query->execute();
}
public static function getAnyTableSorted($table, $sort_by, $sort_order) {
$db = self::getDB();
$query = $db->prepare('SELECT * FROM ' . $table . ' ORDER BY ' . $sort_by . ' ' . $sort_order);
$query->execute();
return $query->fetchAll();
}
public static function getTableColumns($table) {
$db = self::getDB();
$query = $db->prepare('SELECT column_name FROM information_schema.columns WHERE table_schema = \'public\' AND table_name = :table_name');
$query->bindParam(':table_name', $table);
$query->execute();
return $query->fetchAll();
}
public static function getAvailableTables() {
$db = self::getDB();
$query = $db->prepare('SELECT table_name FROM information_schema.tables WHERE table_schema = \'public\' ORDER BY table_name');
$query->execute();
return $query->fetchAll();
}
public static function exportUsersXML() {
$db = self::getDB();
$query = $db->prepare('SELECT * FROM users');
$query->execute();
$users = $query->fetchAll();
$xml = new DOMDocument('1.0', 'UTF-8');
$xml->formatOutput = true;
$root = $xml->createElement('users');
$xml->appendChild($root);
foreach ($users as $user) {
$user_node = $xml->createElement('user');
$root->appendChild($user_node);
$user_node->setAttribute('exported_id', $user['id']);
$user_node->setAttribute('login', $user['login']);
$user_node->setAttribute('password', $user['password']);
}
return $xml->saveXML();
}
public static function importUsersXML($xmlData) {
$xml = new DOMDocument();
$xml->loadXML($xmlData);
$users = $xml->getElementsByTagName('user');
$ok = 0;
$fail = 0;
foreach ($users as $user) {
try {
$login = $user->getAttribute('login');
$password = $user->getAttribute('password');
self::createUser($login, $password);
$user = self::getUser($login);
self::createEmptyMenuForUser($user);
$ok += 1;
} catch (PDOException $e) {
$fail += 1;
}
}
return [$ok, $fail];
}
}
?>

13
w12/src/helpers.php Normal file
View file

@ -0,0 +1,13 @@
<?php
function setSessionVariable($name, $value) {
$_SESSION[$name] = $value;
}
function getSessionVariable($name, $default = null) {
return $_SESSION[$name] ?? $default;
}
function unsetSessionVariable($name) {
unset($_SESSION[$name]);
}
?>

99
w12/views/edit-menu.twig Normal file
View file

@ -0,0 +1,99 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<b>{{user.login}}</b>, вы редактируете меню. <a href="/">На главную</a>
<hr>
<b>Меню:</b>
<div id="menuHolder">
{% if menu %}
{% for item in menu %}
<div>
<input type="text" name="url" value="{{item.url}}">
<input type="text" name="name" value="{{item.name}}">
<button class="removeItemBtn">X</button>
</div>
{% endfor %}
{% endif %}
</div>
<hr>
<div>
<input class="inputData" type="text" name="url" placeholder="Ссылка">
<input class="inputData" type="text" name="name" placeholder="Название">
<button id="addItemBtn">+</button>
</div>
<button id="saveBtn">Сохранить</button>
</body>
<script>
let menu = [];
function updateDeleteBtns() {
document.querySelectorAll('.removeItemBtn').forEach(e => e.onclick = function() {
let item = this.parentNode;
let index = menu.indexOf(item);
menu.splice(index, 1);
item.remove();
});
}
function init() {
let addItemBtn = document.getElementById('addItemBtn');
let saveBtn = document.getElementById('saveBtn');
let menuHolder = document.getElementById('menuHolder');
let children = menuHolder.children;
for (let i = 0; i < children.length; i++) {
let child = children[i];
let url = child.children[0].value;
let name = child.children[1].value;
menu.push({url, name});
}
addItemBtn.addEventListener('click', function() {
let url = document.querySelector('input.inputData[name="url"]').value;
let name = document.querySelector('input.inputData[name="name"]').value;
let item = {
url: url,
name: name
};
menu.push(item);
let newItem = document.createElement('div');
newItem.innerHTML = '<input type="text" name="url" value="' + url + '"><input type="text" name="name" value="' + name + '"><button class="removeItemBtn">X</button>';
menuHolder.appendChild(newItem);
document.querySelector('input.inputData[name="url"]').value = '';
document.querySelector('input.inputData[name="name"]').value = '';
updateDeleteBtns()
});
saveBtn.addEventListener('click', async function() {
let data = {
menu: menu
};
let response = await fetch('/editMenu', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
let result = await response.json();
if (result.code == 200) {
alert('Меню сохранено');
location.reload();
} else {
alert(`Ошибка ${result.code}: ${result.error}`);
}
console.log(result);
});
}
document.addEventListener('DOMContentLoaded', init);
</script>
</html>

20
w12/views/gallery.twig Normal file
View file

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<b>{{user.login}}</b>, вы смотрите свою галерею. <a href="/">На главную</a>
<hr>
{% for image in images %}
<a href="/image/{{image.id}}">
<img src="/images?filename={{image.filename}}" alt="{{image.id}}">
</a>
{% else %}
<p>Нет изображений</p>
{% endfor %}
</body>
</html>

26
w12/views/image.twig Normal file
View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<b>{{user.login}}</b> осматривает картинку... <a href="/">На главную</a>
<hr>
<img src="/images?type=full&filename={{image.filename}}" alt="{{image.filename}}" style="max-width: 100%">
<hr>
<a href="/images?type=thumb&filename={{image.filename}}">Миниатюра</a>
<hr>
<form action="/image/{{image.id}}" method="post">
<input type="text" name="description" value="{{image.description}}">
<br>
Опубликовано?
<input type="checkbox" name="published" value="1" {{image.published ? 'checked' : ''}}>
<br>
<input type="submit" value="Сохранить">
</form>
<a href="/image/{{image.id}}/delete">Удалить</a>
</body>
</html>

51
w12/views/import.twig Normal file
View file

@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
{% if message %}
<div id="message" style="padding: 1rem; border: 2px solid black; background-color: green; color: white">{{message}}</div>
{% endif %}
{% if error %}
<div id="error" style="padding: 1rem; border: 2px solid black; background-color: red; color: white">{{error}}</div>
{% endif %}
<h4>Import page</h4>
<p>Drop XML file on page or enter content down here</p>
<form action="/import" method="post">
<textarea name="xml" cols="30" rows="10"></textarea>
<br>
<input type="submit" name="submit" value="submit">
</form>
</body>
<script>
document.addEventListener("DOMContentLoaded", function() {
let dropZone = document.body;
dropZone.addEventListener("dragover", function(e) {
e.stopPropagation();
e.preventDefault();
e.dataTransfer.dropEffect = "copy";
}, false);
dropZone.addEventListener("dragleave", function(e) {
e.stopPropagation();
e.preventDefault();
e.dataTransfer.dropEffect = "copy";
}, false);
dropZone.addEventListener("drop", async function(e) {
e.stopPropagation();
e.preventDefault();
e.dataTransfer.dropEffect = "copy";
// get the file
let file = e.dataTransfer.files[0];
let fr=new FileReader();
fr.onload=function(){
document.querySelector('[name=xml]').textContent = fr.result;
}
fr.readAsText(file);
}, false);
});
</script>
</html>

108
w12/views/index.twig Normal file
View file

@ -0,0 +1,108 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#imageDrop {
display: flex;
align-content: center;
justify-content: center;
align-items: center;
height: 5rem;
border: 0.2rem dashed gray;
border-radius: 1rem;
}
</style>
</head>
<body>
Привет, {{user.login}}! <a href="/logout">Выйти</a>
<hr>
<b>Меню:</b>
{% if menu %}
<ul>
{% for item in menu %}
<li><a href="{{item.url}}">{{item.name}}</a></li>
{% endfor %}
</ul>
{% else %}
<p>Пусто</p>
{% endif %}
<hr>
<div id="imageDrop">
Загрузить изображение
</div>
<hr>
<b>Действия:</b>
<ul>
<li><a href="/editMenu">Изменить меню</a></li>
<li><a href="/lookupTables">Просмотреть данные таблиц</a></li>
<li><a href="/gallery">Галерея</a></li>
<li><a href="/search">Поиск</a></li>
<hr>
<li><a href="/import">Импортировать данные</a></li>
<li><a href="/export" download>Экспортировать данные</a></li>
</ul>
<hr>
<b>Общая галерея:</b>
<div class="gallery">
{% for image in gallery %}
<a href="image/{{image.id}}">
<img src="/images?filename={{image.filename}}" alt="{{image.filename}}">
</a>
{% else %}
<p>Пусто</p>
{% endfor %}
</div>
</body>
<script>
// dom loaded listener
document.addEventListener("DOMContentLoaded", function() {
// create a drop zone
let dropZone = document.getElementById("imageDrop");
// add event listeners
dropZone.addEventListener("dragover", function(e) {
e.stopPropagation();
e.preventDefault();
e.dataTransfer.dropEffect = "copy";
}, false);
dropZone.addEventListener("dragleave", function(e) {
e.stopPropagation();
e.preventDefault();
e.dataTransfer.dropEffect = "copy";
}, false);
dropZone.addEventListener("drop", async function(e) {
e.stopPropagation();
e.preventDefault();
e.dataTransfer.dropEffect = "copy";
// get the file
let file = e.dataTransfer.files[0];
// create a form data
let formData = new FormData();
// append the file
formData.append("file", file);
try {
// upload image with fetch
const response = await fetch("/uploadImage", {
method: "POST",
body: formData
});
// get the response
const data = await response.json();
if (data.code == 200) {
location.href = "/gallery";
} else {
alert(data.error);
}
} catch (e) {
alert(e);
}
}, false);
});
</script>
</html>

27
w12/views/login.twig Normal file
View file

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
{% if message %}
<div id="message" style="padding: 1rem; border: 2px solid black; background-color: green; color: white">{{message}}</div>
{% endif %}
{% if error %}
<div id="error" style="padding: 1rem; border: 2px solid black; background-color: red; color: white">{{error}}</div>
{% endif %}
<h4>Login page</h4>
<form action="/login" method="post">
<input type="text" name="login" placeholder="login">
<input type="password" name="password" placeholder="password">
<input type="submit" name="submit" value="submit">
</form>
<hr>
<a href="/register">Registration</a>
<br>
<a href="/recover-password">Recover password</a>
</body>
</html>

View file

@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
{% if table %}
<b>{{user.login}}</b>, вы смотрите таблицу <i>{{table}}</i>. <a href="/lookupTables">Отмена</a>
{% else %}
<b>{{user.login}}</b>, вы смотрите таблицы. <a href="/">На главную</a>
{% endif %}
<hr>
{% if table %}
<table border width="100%">
<thead>
<tr>
{% for item in tableData.header %}
<th>
<a href="/lookupTables?table={{table}}&sort=ASC&col={{item.column_name}}">[ASC]</a>
{{item.column_name}}
<a href="/lookupTables?table={{table}}&sort=DESC&col={{item.column_name}}">[DESC]</a>
</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in tableData.data %}
<tr>
{% for col in row %}
<td>{{col}}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<form action="/lookupTables" method="get">
<select name="table">
{% for table in tables %}
<option value="{{table}}">{{table}}</option>
{% endfor %}
</select>
<input type="submit" value="Перейти">
</form>
{% endif %}
</body>
</html>

View file

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
Вам нужно <a href="/login">войти</a> или <a href="/register">зарегистрироваться</a> для работы с системой.
</body>
</html>

View file

@ -0,0 +1,4 @@
Hello, {{login}}!
You have requested password change, so we generated you a new one (without backtics): `<b>{{password}}</b>`.
<br>
Please be sure to delete this message after you have logged in. For now you cannot change your password by yourself.

View file

@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
{% if mail %}
<div id="mail" style="padding: 1rem; border: 2px solid black; background-color: lightsteelblue; color: stellblue">{{mail | raw}}</div>
<br>
{% endif %}
{% if message %}
<div id="message" style="padding: 1rem; border: 2px solid black; background-color: green; color: white">{{message}}</div>
<br>
{% endif %}
{% if error %}
<div id="error" style="padding: 1rem; border: 2px solid black; background-color: red; color: white">{{error}}</div>
<br>
{% endif %}
<h4>Password recovery page</h4>
<form action="/recover-password" method="post">
<input type="text" name="login" placeholder="login">
<input type="submit" name="submit" value="submit">
</form>
<hr>
<a href="/register">Registration</a>
<br>
<a href="/login">Login</a>
</body>
</html>

24
w12/views/register.twig Normal file
View file

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
{% if error %}
<div id="error" style="padding: 1rem; border: 2px solid black; background-color: red; color: white">{{error}}</div>
{% endif %}
<h4>Registration page</h4>
<form action="/register" method="post">
<input type="text" name="login" placeholder="login">
<input type="password" name="password" placeholder="password">
<input type="submit" name="submit" value="submit">
</form>
<hr>
<a href="/login">Login</a>
<br>
<a href="/recover-password">Recover password</a>
</body>
</html>

50
w12/views/search.twig Normal file
View file

@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#imageDrop {
display: flex;
align-content: center;
justify-content: center;
align-items: center;
height: 5rem;
border: 0.2rem dashed gray;
border-radius: 1rem;
}
</style>
</head>
<body>
<b>{{user.login}}</b> что-то ищет 🤔 <a href="/">На главную</a>
<hr>
<form action="/search" method="get">
<input type="text" name="search" placeholder="Поиск..." value="{{search}}">
<select name="author">
<option value="-1">Все авторы</option>
{% for item in authors %}
<option value="{{item.id}}" {% if item.id == author %}selected{% endif %}>{{item.login}}</option>
{% endfor %}
</select>
<input type="submit" value="Найти">
</form>
{% if images %}
<b>Результаты поиска:</b>
<div class="images">
{% for image in images %}
<a href="image/{{image.id}}">
<img src="/images?filename={{image.filename}}" alt="{{image.filename}}">
</a>
{% else %}
<p>Пусто</p>
{% endfor %}
{% else %}
<p>Пусто</p>
{% endif %}
</div>
</body>
</html>