2.0 Flight -> Slim 3
This commit is contained in:
parent
932a3763ca
commit
4a4c51996c
20 changed files with 989 additions and 655 deletions
|
@ -3,52 +3,34 @@
|
|||
namespace App\Controllers;
|
||||
|
||||
|
||||
use App\Exceptions\AuthenticationException;
|
||||
use App\Exceptions\UnauthorizedException;
|
||||
use App\Web\Session;
|
||||
use Flight;
|
||||
use App\Database\DB;
|
||||
use League\Flysystem\Adapter\Local;
|
||||
use League\Flysystem\Filesystem;
|
||||
use Slim\Container;
|
||||
use Slim\Http\Request;
|
||||
use Slim\Http\Response;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
|
||||
/**
|
||||
* Check if the current user is logged in
|
||||
* @throws AuthenticationException
|
||||
*/
|
||||
protected function checkLogin(): void
|
||||
/** @var Container */
|
||||
protected $container;
|
||||
|
||||
public function __construct(Container $container)
|
||||
{
|
||||
if (!Session::get('logged', false)) {
|
||||
Session::set('redirectTo', (isset($_SERVER['HTTPS']) ? "https" : "http") . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]");
|
||||
throw new AuthenticationException();
|
||||
}
|
||||
|
||||
if (!DB::query('SELECT `id`, `active` FROM `users` WHERE `id` = ? LIMIT 1', [Session::get('user_id')])->fetch()->active) {
|
||||
Session::alert('Your account is not active anymore.', 'danger');
|
||||
Session::set('logged', false);
|
||||
Session::set('redirectTo', (isset($_SERVER['HTTPS']) ? "https" : "http") . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]");
|
||||
throw new AuthenticationException();
|
||||
}
|
||||
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current user is an admin
|
||||
* @throws AuthenticationException
|
||||
* @throws UnauthorizedException
|
||||
* @param $name
|
||||
* @return mixed|null
|
||||
* @throws \Interop\Container\Exception\ContainerException
|
||||
*/
|
||||
protected function checkAdmin(): void
|
||||
public function __get($name)
|
||||
{
|
||||
$this->checkLogin();
|
||||
|
||||
if (!DB::query('SELECT `id`, `is_admin` FROM `users` WHERE `id` = ? LIMIT 1', [Session::get('user_id')])->fetch()->is_admin) {
|
||||
Session::alert('Your account is not admin anymore.', 'danger');
|
||||
Session::set('admin', false);
|
||||
Session::set('redirectTo', (isset($_SERVER['HTTPS']) ? "https" : "http") . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]");
|
||||
throw new UnauthorizedException();
|
||||
if ($this->container->has($name)) {
|
||||
return $this->container->get($name);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
@ -71,20 +53,6 @@ abstract class Controller
|
|||
*/
|
||||
protected function getStorage(): Filesystem
|
||||
{
|
||||
return new Filesystem(new Local(Flight::get('config')['storage_dir']));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set http2 header for a resource if is supported
|
||||
* @param string $url
|
||||
* @param string $as
|
||||
*/
|
||||
protected function http2push(string $url, string $as = 'image'): void
|
||||
{
|
||||
if (Flight::request()->scheme === 'HTTP/2.0') {
|
||||
$headers = isset(Flight::response()->headers()['Link']) ? Flight::response()->headers()['Link'] : [];
|
||||
$headers[] = "<${url}>; rel=preload; as=${as}";
|
||||
Flight::response()->header('Link', $headers);
|
||||
}
|
||||
return new Filesystem(new Local($this->settings['storage_dir']));
|
||||
}
|
||||
}
|
|
@ -3,37 +3,44 @@
|
|||
namespace App\Controllers;
|
||||
|
||||
|
||||
use App\Database\DB;
|
||||
use App\Traits\SingletonController;
|
||||
use App\Web\Session;
|
||||
use Flight;
|
||||
use League\Flysystem\FileNotFoundException;
|
||||
use Slim\Http\Request;
|
||||
use Slim\Http\Response;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
use SingletonController;
|
||||
|
||||
const PER_PAGE = 21;
|
||||
const PER_PAGE_ADMIN = 50;
|
||||
const PER_PAGE_ADMIN = 25;
|
||||
|
||||
public function redirects(): void
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @return Response
|
||||
*/
|
||||
public function redirects(Request $request, Response $response): Response
|
||||
{
|
||||
$this->checkLogin();
|
||||
Flight::redirect('/home');
|
||||
return $response->withRedirect('/home');
|
||||
}
|
||||
|
||||
public function home($page = 1): void
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param $args
|
||||
* @return Response
|
||||
*/
|
||||
public function home(Request $request, Response $response, $args): Response
|
||||
{
|
||||
$this->checkLogin();
|
||||
|
||||
$page = isset($args['page']) ? (int)$args['page'] : 0;
|
||||
$page = max(0, --$page);
|
||||
|
||||
if (Session::get('admin', false)) {
|
||||
$medias = DB::query('SELECT `uploads`.*, `users`.`user_code`, `users`.`username` FROM `uploads` LEFT JOIN `users` ON `uploads`.`user_id` = `users`.`id` ORDER BY `timestamp` DESC LIMIT ? OFFSET ?', [self::PER_PAGE_ADMIN, $page * self::PER_PAGE_ADMIN])->fetchAll();
|
||||
$pages = DB::query('SELECT COUNT(*) AS `count` FROM `uploads`')->fetch()->count / self::PER_PAGE_ADMIN;
|
||||
$medias = $this->database->query('SELECT `uploads`.*, `users`.`user_code`, `users`.`username` FROM `uploads` LEFT JOIN `users` ON `uploads`.`user_id` = `users`.`id` ORDER BY `timestamp` DESC LIMIT ? OFFSET ?', [self::PER_PAGE_ADMIN, $page * self::PER_PAGE_ADMIN])->fetchAll();
|
||||
$pages = $this->database->query('SELECT COUNT(*) AS `count` FROM `uploads`')->fetch()->count / self::PER_PAGE_ADMIN;
|
||||
} else {
|
||||
$medias = DB::query('SELECT `uploads`.*,`users`.`user_code`, `users`.`username` FROM `uploads` INNER JOIN `users` ON `uploads`.`user_id` = `users`.`id` WHERE `user_id` = ? ORDER BY `timestamp` DESC LIMIT ? OFFSET ?', [Session::get('user_id'), self::PER_PAGE, $page * self::PER_PAGE])->fetchAll();
|
||||
$pages = DB::query('SELECT COUNT(*) AS `count` FROM `uploads` WHERE `user_id` = ?', Session::get('user_id'))->fetch()->count / self::PER_PAGE;
|
||||
$medias = $this->database->query('SELECT `uploads`.*,`users`.`user_code`, `users`.`username` FROM `uploads` INNER JOIN `users` ON `uploads`.`user_id` = `users`.`id` WHERE `user_id` = ? ORDER BY `timestamp` DESC LIMIT ? OFFSET ?', [Session::get('user_id'), self::PER_PAGE, $page * self::PER_PAGE])->fetchAll();
|
||||
$pages = $this->database->query('SELECT COUNT(*) AS `count` FROM `uploads` WHERE `user_id` = ?', Session::get('user_id'))->fetch()->count / self::PER_PAGE;
|
||||
}
|
||||
|
||||
$filesystem = $this->getStorage();
|
||||
|
@ -51,7 +58,8 @@ class DashboardController extends Controller
|
|||
$media->size = $this->humanFilesize($size);
|
||||
}
|
||||
|
||||
Flight::render(
|
||||
return $this->view->render(
|
||||
$response,
|
||||
Session::get('admin', false) ? 'dashboard/admin.twig' : 'dashboard/home.twig',
|
||||
[
|
||||
'medias' => $medias,
|
||||
|
@ -62,15 +70,19 @@ class DashboardController extends Controller
|
|||
);
|
||||
}
|
||||
|
||||
public function system()
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @return Response
|
||||
* @throws FileNotFoundException
|
||||
*/
|
||||
public function system(Request $request, Response $response): Response
|
||||
{
|
||||
$this->checkAdmin();
|
||||
$usersCount = $this->database->query('SELECT COUNT(*) AS `count` FROM `users`')->fetch()->count;
|
||||
$mediasCount = $this->database->query('SELECT COUNT(*) AS `count` FROM `uploads`')->fetch()->count;
|
||||
$orphanFilesCount = $this->database->query('SELECT COUNT(*) AS `count` FROM `uploads` WHERE `user_id` IS NULL')->fetch()->count;
|
||||
|
||||
$usersCount = DB::query('SELECT COUNT(*) AS `count` FROM `users`')->fetch()->count;
|
||||
$mediasCount = DB::query('SELECT COUNT(*) AS `count` FROM `uploads`')->fetch()->count;
|
||||
$orphanFilesCount = DB::query('SELECT COUNT(*) AS `count` FROM `uploads` WHERE `user_id` IS NULL')->fetch()->count;
|
||||
|
||||
$medias = DB::query('SELECT `users`.`user_code`, `uploads`.`code`, `uploads`.`storage_path` FROM `uploads` LEFT JOIN `users` ON `uploads`.`user_id` = `users`.`id`')->fetchAll();
|
||||
$medias = $this->database->query('SELECT `users`.`user_code`, `uploads`.`code`, `uploads`.`storage_path` FROM `uploads` LEFT JOIN `users` ON `uploads`.`user_id` = `users`.`id`')->fetchAll();
|
||||
|
||||
$totalSize = 0;
|
||||
|
||||
|
@ -79,7 +91,9 @@ class DashboardController extends Controller
|
|||
$totalSize += $filesystem->getSize($media->storage_path);
|
||||
}
|
||||
|
||||
Flight::render('dashboard/system.twig', [
|
||||
return $this->view->render(
|
||||
$response,
|
||||
'dashboard/system.twig', [
|
||||
'usersCount' => $usersCount,
|
||||
'mediasCount' => $mediasCount,
|
||||
'orphanFilesCount' => $orphanFilesCount,
|
||||
|
|
|
@ -4,40 +4,44 @@ namespace App\Controllers;
|
|||
|
||||
|
||||
use App\Database\DB;
|
||||
use App\Traits\SingletonController;
|
||||
use App\Web\Log;
|
||||
use App\Web\Session;
|
||||
use Flight;
|
||||
use Slim\Http\Request;
|
||||
use Slim\Http\Response;
|
||||
|
||||
class LoginController extends Controller
|
||||
{
|
||||
use SingletonController;
|
||||
|
||||
public function show(): void
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @return Response
|
||||
*/
|
||||
public function show(Request $request, Response $response): Response
|
||||
{
|
||||
if (Session::get('logged', false)) {
|
||||
Flight::redirect('/home');
|
||||
return;
|
||||
return $response->withRedirect('/home');
|
||||
}
|
||||
Flight::render('auth/login.twig');
|
||||
return $this->view->render($response, 'auth/login.twig');
|
||||
}
|
||||
|
||||
public function login(): void
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @return Response
|
||||
*/
|
||||
public function login(Request $request, Response $response): Response
|
||||
{
|
||||
$form = Flight::request()->data;
|
||||
|
||||
$result = DB::query('SELECT `id`,`username`, `password`,`is_admin`, `active` FROM `users` WHERE `username` = ? LIMIT 1', $form->username)->fetch();
|
||||
$result = DB::query('SELECT `id`,`username`, `password`,`is_admin`, `active` FROM `users` WHERE `username` = ? LIMIT 1', $request->getParam('username'))->fetch();
|
||||
|
||||
if (!$result || !password_verify($form->password, $result->password)) {
|
||||
Flight::redirect('login');
|
||||
if (!$result || !password_verify($request->getParam('password'), $result->password)) {
|
||||
Session::alert('Wrong credentials', 'danger');
|
||||
return;
|
||||
return $response->withRedirect('/login');
|
||||
}
|
||||
|
||||
if (!$result->active) {
|
||||
Flight::redirect('login');
|
||||
Session::alert('Your account is disabled.', 'danger');
|
||||
return;
|
||||
return $response->withRedirect('/login');
|
||||
}
|
||||
|
||||
Session::set('logged', true);
|
||||
|
@ -46,23 +50,26 @@ class LoginController extends Controller
|
|||
Session::set('admin', $result->is_admin);
|
||||
|
||||
Session::alert("Welcome, $result->username!", 'info');
|
||||
Log::info("User $result->username logged in.");
|
||||
$this->logger->info("User $result->username logged in.");
|
||||
|
||||
if (Session::has('redirectTo')) {
|
||||
Flight::redirect(Session::get('redirectTo'));
|
||||
return;
|
||||
return $response->withRedirect(Session::get('redirectTo'));
|
||||
}
|
||||
|
||||
Flight::redirect('/home');
|
||||
return $response->withRedirect('/home');
|
||||
}
|
||||
|
||||
public function logout(): void
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @return Response
|
||||
*/
|
||||
public function logout(Request $request, Response $response): Response
|
||||
{
|
||||
$this->checkLogin();
|
||||
Session::clear();
|
||||
Session::set('logged', false);
|
||||
Session::alert('Goodbye!', 'warning');
|
||||
Flight::redirect('/login');
|
||||
return $response->withRedirect('/login');
|
||||
}
|
||||
|
||||
}
|
|
@ -3,99 +3,98 @@
|
|||
namespace App\Controllers;
|
||||
|
||||
|
||||
use App\Database\DB;
|
||||
use App\Exceptions\NotFoundException;
|
||||
use App\Traits\SingletonController;
|
||||
use App\Web\Log;
|
||||
use App\Exceptions\UnauthorizedException;
|
||||
use App\Web\Session;
|
||||
use Flight;
|
||||
use League\Flysystem\FileExistsException;
|
||||
use League\Flysystem\FileNotFoundException;
|
||||
use League\Flysystem\Filesystem;
|
||||
use Slim\Exception\NotFoundException;
|
||||
use Slim\Http\Request;
|
||||
use Slim\Http\Response;
|
||||
use Slim\Http\Stream;
|
||||
|
||||
class UploadController extends Controller
|
||||
{
|
||||
use SingletonController;
|
||||
|
||||
public function upload(): void
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @return Response
|
||||
* @throws FileExistsException
|
||||
*/
|
||||
public function upload(Request $request, Response $response): Response
|
||||
{
|
||||
$requestData = Flight::request()->data;
|
||||
|
||||
$response = [
|
||||
'message' => null,
|
||||
];
|
||||
$json = ['message' => null];
|
||||
|
||||
if (!isset($requestData->token)) {
|
||||
$response['message'] = 'Token not specified.';
|
||||
Flight::json($response, 400);
|
||||
return;
|
||||
if ($request->getParam('token') === null) {
|
||||
$json['message'] = 'Token not specified.';
|
||||
return $response->withJson($json, 400);
|
||||
}
|
||||
|
||||
$user = DB::query('SELECT * FROM `users` WHERE `token` = ? LIMIT 1', $requestData->token)->fetch();
|
||||
$user = $this->database->query('SELECT * FROM `users` WHERE `token` = ? LIMIT 1', $request->getParam('token'))->fetch();
|
||||
|
||||
if (!$user) {
|
||||
$response['message'] = 'Token specified not found.';
|
||||
Flight::json($response, 404);
|
||||
return;
|
||||
$json['message'] = 'Token specified not found.';
|
||||
return $response->withJson($json, 404);
|
||||
}
|
||||
|
||||
if (!$user->active) {
|
||||
$response['message'] = 'Account disabled.';
|
||||
Flight::json($response, 401);
|
||||
return;
|
||||
$json['message'] = 'Account disabled.';
|
||||
return $response->withJson($json, 401);
|
||||
}
|
||||
|
||||
do {
|
||||
$code = uniqid();
|
||||
} while (DB::query('SELECT COUNT(*) AS `count` FROM `uploads` WHERE `code` = ?', $code)->fetch()->count > 0);
|
||||
} while ($this->database->query('SELECT COUNT(*) AS `count` FROM `uploads` WHERE `code` = ?', $code)->fetch()->count > 0);
|
||||
|
||||
$file = Flight::request()->files->current();
|
||||
$fileInfo = pathinfo($file['name']);
|
||||
/** @var \Psr\Http\Message\UploadedFileInterface $file */
|
||||
$file = $request->getUploadedFiles()['upload'];
|
||||
|
||||
$fileInfo = pathinfo($file->getClientFilename());
|
||||
$storagePath = "$user->user_code/$code.$fileInfo[extension]";
|
||||
|
||||
$stream = fopen($file['tmp_name'], 'r+');
|
||||
$this->getStorage()->writeStream($storagePath, $file->getStream()->detach());
|
||||
|
||||
$filesystem = $this->getStorage();
|
||||
try {
|
||||
$filesystem->writeStream($storagePath, $stream);
|
||||
} catch (FileExistsException $e) {
|
||||
Flight::halt(500);
|
||||
return;
|
||||
} finally {
|
||||
fclose($stream);
|
||||
}
|
||||
|
||||
DB::query('INSERT INTO `uploads`(`user_id`, `code`, `filename`, `storage_path`) VALUES (?, ?, ?, ?)', [
|
||||
$this->database->query('INSERT INTO `uploads`(`user_id`, `code`, `filename`, `storage_path`) VALUES (?, ?, ?, ?)', [
|
||||
$user->id,
|
||||
$code,
|
||||
$file['name'],
|
||||
$file->getClientFilename(),
|
||||
$storagePath
|
||||
]);
|
||||
|
||||
$base_url = Flight::get('config')['base_url'];
|
||||
$base_url = $this->settings['base_url'];
|
||||
|
||||
$response['message'] = 'OK.';
|
||||
$response['url'] = "$base_url/$user->user_code/$code.$fileInfo[extension]";
|
||||
Flight::json($response, 201);
|
||||
$json['message'] = 'OK.';
|
||||
$json['url'] = "$base_url/$user->user_code/$code.$fileInfo[extension]";
|
||||
|
||||
Log::info("User $user->username uploaded new media.", [DB::raw()->lastInsertId()]);
|
||||
$this->logger->info("User $user->username uploaded new media.", [$this->database->raw()->lastInsertId()]);
|
||||
|
||||
return $response->withJson($json, 201);
|
||||
}
|
||||
|
||||
public function show($userCode, $mediaCode): void
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param $args
|
||||
* @return Response
|
||||
* @throws FileNotFoundException
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function show(Request $request, Response $response, $args): Response
|
||||
{
|
||||
$media = $this->getMedia($userCode, $mediaCode);
|
||||
$media = $this->getMedia($args['userCode'], $args['mediaCode']);
|
||||
|
||||
if (!$media || !$media->published && Session::get('user_id') !== $media->user_id && !Session::get('admin', false)) {
|
||||
Flight::error(new NotFoundException());
|
||||
return;
|
||||
throw new NotFoundException($request, $response);
|
||||
}
|
||||
|
||||
$filesystem = $this->getStorage();
|
||||
|
||||
if (stristr(Flight::request()->user_agent, 'TelegramBot') ||
|
||||
stristr(Flight::request()->user_agent, 'facebookexternalhit/') ||
|
||||
stristr(Flight::request()->user_agent, 'Facebot')) {
|
||||
$this->streamMedia($filesystem, $media);
|
||||
if (stristr($request->getHeaderLine('User-Agent'), 'TelegramBot') ||
|
||||
stristr($request->getHeaderLine('User-Agent'), 'facebookexternalhit/') ||
|
||||
stristr($request->getHeaderLine('User-Agent'), 'Facebot')) {
|
||||
return $this->streamMedia($request, $response, $filesystem, $media);
|
||||
} else {
|
||||
|
||||
try {
|
||||
|
@ -104,16 +103,15 @@ class UploadController extends Controller
|
|||
$type = explode('/', $mime)[0];
|
||||
if ($type === 'text') {
|
||||
$media->text = $filesystem->read($media->storage_path);
|
||||
} elseif (in_array($type, ['image', 'video'])) {
|
||||
$this->http2push(Flight::get('config')['base_url'] . "/$userCode/$mediaCode/raw");
|
||||
} elseif (in_array($type, ['image', 'video']) && $request->getHeaderLine('Scheme') === 'HTTP/2.0') {
|
||||
$response = $response->withHeader('Link', "<{$this->settings['base_url']}/$args[userCode]/$args[mediaCode]/raw>; rel=preload; as={$type}");
|
||||
}
|
||||
|
||||
} catch (FileNotFoundException $e) {
|
||||
Flight::error($e);
|
||||
return;
|
||||
throw $e;
|
||||
}
|
||||
|
||||
Flight::render('upload/public.twig', [
|
||||
return $this->view->render($response, 'upload/public.twig', [
|
||||
'media' => $media,
|
||||
'type' => $mime,
|
||||
'extension' => pathinfo($media->filename, PATHINFO_EXTENSION)
|
||||
|
@ -121,68 +119,98 @@ class UploadController extends Controller
|
|||
}
|
||||
}
|
||||
|
||||
public function getRawById($id): void
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param $args
|
||||
* @return Response
|
||||
* @throws NotFoundException
|
||||
* @throws FileNotFoundException
|
||||
*/
|
||||
public function getRawById(Request $request, Response $response, $args): Response
|
||||
{
|
||||
$this->checkAdmin();
|
||||
|
||||
$media = DB::query('SELECT * FROM `uploads` WHERE `id` = ? LIMIT 1', $id)->fetch();
|
||||
$media = $this->database->query('SELECT * FROM `uploads` WHERE `id` = ? LIMIT 1', $args['id'])->fetch();
|
||||
|
||||
if (!$media) {
|
||||
Flight::error(new NotFoundException());
|
||||
return;
|
||||
throw new NotFoundException($request, $response);
|
||||
}
|
||||
|
||||
$this->streamMedia($this->getStorage(), $media);
|
||||
return $this->streamMedia($request, $response, $this->getStorage(), $media);
|
||||
}
|
||||
|
||||
public function showRaw($userCode, $mediaCode): void
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param $args
|
||||
* @return Response
|
||||
* @throws NotFoundException
|
||||
* @throws FileNotFoundException
|
||||
*/
|
||||
public function showRaw(Request $request, Response $response, $args): Response
|
||||
{
|
||||
$media = $this->getMedia($userCode, $mediaCode);
|
||||
$media = $this->getMedia($args['userCode'], $args['mediaCode']);
|
||||
|
||||
if (!$media || !$media->published && Session::get('user_id') !== $media->user_id && !Session::get('admin', false)) {
|
||||
Flight::error(new NotFoundException());
|
||||
return;
|
||||
throw new NotFoundException($request, $response);
|
||||
}
|
||||
|
||||
$this->streamMedia($this->getStorage(), $media);
|
||||
return $this->streamMedia($request, $response, $this->getStorage(), $media);
|
||||
}
|
||||
|
||||
|
||||
public function download($userCode, $mediaCode): void
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param $args
|
||||
* @return Response
|
||||
* @throws NotFoundException
|
||||
* @throws FileNotFoundException
|
||||
*/
|
||||
public function download(Request $request, Response $response, $args): Response
|
||||
{
|
||||
$media = $this->getMedia($userCode, $mediaCode);
|
||||
$media = $this->getMedia($args['userCode'], $args['mediaCode']);
|
||||
|
||||
if (!$media || !$media->published && Session::get('user_id') !== $media->user_id && !Session::get('admin', false)) {
|
||||
Flight::error(new NotFoundException());
|
||||
return;
|
||||
throw new NotFoundException($request, $response);
|
||||
}
|
||||
|
||||
$this->streamMedia($this->getStorage(), $media, 'attachment');
|
||||
return $this->streamMedia($request, $response, $this->getStorage(), $media, 'attachment');
|
||||
}
|
||||
|
||||
public function togglePublish($id): void
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param $args
|
||||
* @return Response
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function togglePublish(Request $request, Response $response, $args): Response
|
||||
{
|
||||
$this->checkLogin();
|
||||
|
||||
if (Session::get('admin')) {
|
||||
$media = DB::query('SELECT * FROM `uploads` WHERE `id` = ? LIMIT 1', $id)->fetch();
|
||||
$media = $this->database->query('SELECT * FROM `uploads` WHERE `id` = ? LIMIT 1', $args['id'])->fetch();
|
||||
} else {
|
||||
$media = DB::query('SELECT * FROM `uploads` WHERE `id` = ? AND `user_id` = ? LIMIT 1', [$id, Session::get('user_id')])->fetch();
|
||||
$media = $this->database->query('SELECT * FROM `uploads` WHERE `id` = ? AND `user_id` = ? LIMIT 1', [$args['id'], Session::get('user_id')])->fetch();
|
||||
}
|
||||
|
||||
if (!$media) {
|
||||
Flight::halt(404);
|
||||
return;
|
||||
throw new NotFoundException($request, $response);
|
||||
}
|
||||
|
||||
DB::query('UPDATE `uploads` SET `published`=? WHERE `id`=?', [!$media->published, $media->id]);
|
||||
$this->database->query('UPDATE `uploads` SET `published`=? WHERE `id`=?', [!$media->published, $media->id]);
|
||||
|
||||
return $response->withStatus(200);
|
||||
}
|
||||
|
||||
public function delete($id): void
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param $args
|
||||
* @return Response
|
||||
* @throws NotFoundException
|
||||
* @throws UnauthorizedException
|
||||
*/
|
||||
public function delete(Request $request, Response $response, $args): Response
|
||||
{
|
||||
$this->checkLogin();
|
||||
|
||||
$media = DB::query('SELECT * FROM `uploads` WHERE `id` = ? LIMIT 1', $id)->fetch();
|
||||
$media = $this->database->query('SELECT * FROM `uploads` WHERE `id` = ? LIMIT 1', $args['id'])->fetch();
|
||||
|
||||
if (Session::get('admin', false) || $media->user_id === Session::get('user_id')) {
|
||||
|
||||
|
@ -190,22 +218,28 @@ class UploadController extends Controller
|
|||
try {
|
||||
$filesystem->delete($media->storage_path);
|
||||
} catch (FileNotFoundException $e) {
|
||||
Flight::halt(404);
|
||||
return;
|
||||
throw new NotFoundException($request, $response);
|
||||
} finally {
|
||||
DB::query('DELETE FROM `uploads` WHERE `id` = ?', $id);
|
||||
Log::info('User ' . Session::get('username') . " deleted media $id");
|
||||
$this->database->query('DELETE FROM `uploads` WHERE `id` = ?', $args['id']);
|
||||
$this->logger->info('User ' . Session::get('username') . ' deleted a media.', [$args['id']]);
|
||||
}
|
||||
} else {
|
||||
Flight::halt(403);
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
return $response->withStatus(200);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $userCode
|
||||
* @param $mediaCode
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getMedia($userCode, $mediaCode)
|
||||
{
|
||||
$mediaCode = pathinfo($mediaCode)['filename'];
|
||||
|
||||
$media = DB::query('SELECT * FROM `uploads` INNER JOIN `users` ON `uploads`.`user_id` = `users`.`id` WHERE `user_code` = ? AND `uploads`.`code` = ? LIMIT 1', [
|
||||
$media = $this->database->query('SELECT * FROM `uploads` INNER JOIN `users` ON `uploads`.`user_id` = `users`.`id` WHERE `user_code` = ? AND `uploads`.`code` = ? LIMIT 1', [
|
||||
$userCode,
|
||||
$mediaCode
|
||||
])->fetch();
|
||||
|
@ -213,36 +247,43 @@ class UploadController extends Controller
|
|||
return $media;
|
||||
}
|
||||
|
||||
protected function streamMedia(Filesystem $storage, $media, string $disposition = 'inline'): void
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param Filesystem $storage
|
||||
* @param $media
|
||||
* @param string $disposition
|
||||
* @return Response
|
||||
* @throws FileNotFoundException
|
||||
*/
|
||||
protected function streamMedia(Request $request, Response $response, Filesystem $storage, $media, string $disposition = 'inline'): Response
|
||||
{
|
||||
try {
|
||||
$mime = $storage->getMimetype($media->storage_path);
|
||||
$query = Flight::request()->query;
|
||||
$mime = $storage->getMimetype($media->storage_path);
|
||||
|
||||
if ($query['width'] !== null && explode('/', $mime)[0] === 'image') {
|
||||
Flight::response()->header('Content-Type', 'image/png');
|
||||
Flight::response()->header('Content-Disposition', $disposition . ';filename="scaled-' . pathinfo($media->filename)['filename'] . '.png"');
|
||||
Flight::response()->sendHeaders();
|
||||
ob_clean();
|
||||
if ($request->getParam('width') !== null && explode('/', $mime)[0] === 'image') {
|
||||
|
||||
$image = imagecreatefromstring($storage->read($media->storage_path));
|
||||
$scaled = imagescale($image, $query['width'], $query['height'] !== null ? $query['height'] : -1);
|
||||
$image = imagecreatefromstring($storage->read($media->storage_path));
|
||||
$scaled = imagescale($image, $request->getParam('width'), $request->getParam('height') !== null ? $request->getParam('height') : -1);
|
||||
imagedestroy($image);
|
||||
|
||||
imagedestroy($image);
|
||||
ob_start();
|
||||
imagepng($scaled, null, 9);
|
||||
|
||||
imagepng($scaled, null, 9);
|
||||
imagedestroy($scaled);
|
||||
} else {
|
||||
Flight::response()->header('Content-Type', $mime);
|
||||
Flight::response()->header('Content-Disposition', $disposition . ';filename="' . $media->filename . '"');
|
||||
Flight::response()->header('Content-Length', $storage->getSize($media->storage_path));
|
||||
Flight::response()->sendHeaders();
|
||||
ob_end_clean();
|
||||
$imagedata = ob_get_contents();
|
||||
ob_end_clean();
|
||||
imagedestroy($scaled);
|
||||
|
||||
fpassthru($storage->readStream($media->storage_path));
|
||||
}
|
||||
} catch (FileNotFoundException $e) {
|
||||
Flight::error($e);
|
||||
return $response
|
||||
->withHeader('Content-Type', 'image/png')
|
||||
->withHeader('Content-Disposition', $disposition . ';filename="scaled-' . pathinfo($media->filename)['filename'] . '.png"')
|
||||
->write($imagedata);
|
||||
} else {
|
||||
ob_end_clean();
|
||||
return $response
|
||||
->withHeader('Content-Type', $mime)
|
||||
->withHeader('Content-Disposition', $disposition . ';filename="' . $media->filename . '"')
|
||||
->withHeader('Content-Length', $storage->getSize($media->storage_path))
|
||||
->withBody(new Stream($storage->readStream($media->storage_path)));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,309 +3,333 @@
|
|||
namespace App\Controllers;
|
||||
|
||||
|
||||
use App\Database\DB;
|
||||
use App\Exceptions\NotFoundException;
|
||||
use App\Exceptions\UnauthorizedException;
|
||||
use App\Traits\SingletonController;
|
||||
use App\Web\Log;
|
||||
use App\Web\Session;
|
||||
use Flight;
|
||||
use Slim\Exception\NotFoundException;
|
||||
use Slim\Http\Request;
|
||||
use Slim\Http\Response;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
use SingletonController;
|
||||
|
||||
const PER_PAGE = 15;
|
||||
|
||||
public function index($page = 1): void
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param $args
|
||||
* @return Response
|
||||
*/
|
||||
public function index(Request $request, Response $response, $args): Response
|
||||
{
|
||||
$this->checkAdmin();
|
||||
|
||||
$page = isset($args['page']) ? (int)$args['page'] : 0;
|
||||
$page = max(0, --$page);
|
||||
|
||||
$users = DB::query('SELECT * FROM `users` LIMIT ? OFFSET ?', [self::PER_PAGE, $page * self::PER_PAGE])->fetchAll();
|
||||
$users = $this->database->query('SELECT * FROM `users` LIMIT ? OFFSET ?', [self::PER_PAGE, $page * self::PER_PAGE])->fetchAll();
|
||||
|
||||
$pages = DB::query('SELECT COUNT(*) AS `count` FROM `users`')->fetch()->count / self::PER_PAGE;
|
||||
$pages = $this->database->query('SELECT COUNT(*) AS `count` FROM `users`')->fetch()->count / self::PER_PAGE;
|
||||
|
||||
Flight::render('user/index.twig', [
|
||||
'users' => $users,
|
||||
'next' => $page < floor($pages),
|
||||
'previous' => $page >= 1,
|
||||
'current_page' => ++$page,
|
||||
]);
|
||||
return $this->view->render($response,
|
||||
'user/index.twig',
|
||||
[
|
||||
'users' => $users,
|
||||
'next' => $page < floor($pages),
|
||||
'previous' => $page >= 1,
|
||||
'current_page' => ++$page,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function create(): void
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @return Response
|
||||
*/
|
||||
public function create(Request $request, Response $response): Response
|
||||
{
|
||||
$this->checkAdmin();
|
||||
Flight::render('user/create.twig');
|
||||
return $this->view->render($response, 'user/create.twig');
|
||||
}
|
||||
|
||||
public function store(): void
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @return Response
|
||||
*/
|
||||
public function store(Request $request, Response $response): Response
|
||||
{
|
||||
$this->checkAdmin();
|
||||
|
||||
$form = Flight::request()->data;
|
||||
|
||||
if (!isset($form->email) || empty($form->email)) {
|
||||
if ($request->getParam('email') === null) {
|
||||
Session::alert('The email is required.', 'danger');
|
||||
Flight::redirectBack();
|
||||
return;
|
||||
return $response->withRedirect('/user/create');
|
||||
}
|
||||
|
||||
if (!isset($form->username) || empty($form->username)) {
|
||||
if ($request->getParam('username') === null) {
|
||||
Session::alert('The username is required.', 'danger');
|
||||
Flight::redirectBack();
|
||||
return;
|
||||
return $response->withRedirect('/user/create');
|
||||
}
|
||||
|
||||
if (DB::query('SELECT COUNT(*) AS `count` FROM `users` WHERE `username` = ?', $form->username)->fetch()->count > 0) {
|
||||
Session::alert('The username already taken.', 'danger');
|
||||
Flight::redirectBack();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isset($form->password) || empty($form->password)) {
|
||||
if ($request->getParam('password') === null) {
|
||||
Session::alert('The password is required.', 'danger');
|
||||
Flight::redirectBack();
|
||||
return;
|
||||
return $response->withRedirect('/user/create');
|
||||
}
|
||||
|
||||
if ($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `username` = ?', $request->getParam('username'))->fetch()->count > 0) {
|
||||
Session::alert('The username already taken.', 'danger');
|
||||
return $response->withRedirect('/user/create');
|
||||
}
|
||||
|
||||
do {
|
||||
$userCode = substr(md5(microtime()), rand(0, 26), 5);
|
||||
} while (DB::query('SELECT COUNT(*) AS `count` FROM `users` WHERE `user_code` = ?', $userCode)->fetch()->count > 0);
|
||||
} while ($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `user_code` = ?', $userCode)->fetch()->count > 0);
|
||||
|
||||
$token = $this->generateNewToken();
|
||||
|
||||
DB::query('INSERT INTO `users`(`email`, `username`, `password`, `is_admin`, `active`, `user_code`, `token`) VALUES (?, ?, ?, ?, ?, ?, ?)', [
|
||||
$form->email,
|
||||
$form->username,
|
||||
password_hash($form->password, PASSWORD_DEFAULT),
|
||||
isset($form->is_admin),
|
||||
isset($form->is_active),
|
||||
$this->database->query('INSERT INTO `users`(`email`, `username`, `password`, `is_admin`, `active`, `user_code`, `token`) VALUES (?, ?, ?, ?, ?, ?, ?)', [
|
||||
$request->getParam('email'),
|
||||
$request->getParam('username'),
|
||||
password_hash($request->getParam('password'), PASSWORD_DEFAULT),
|
||||
$request->getParam('is_admin') !== null,
|
||||
$request->getParam('is_active') !== null,
|
||||
$userCode,
|
||||
$token
|
||||
]);
|
||||
|
||||
Session::alert("User '$form->username' created!", 'success');
|
||||
Log::info('User ' . Session::get('username') . ' created a new user.', [array_diff($form->getData(), ['password'])]);
|
||||
Session::alert("User '{$request->getParam('username')}' created!", 'success');
|
||||
$this->logger->info('User ' . Session::get('username') . ' created a new user.', [array_diff($request->getParams(), ['password'])]);
|
||||
|
||||
Flight::redirect('/users');
|
||||
return $response->withRedirect('/users');
|
||||
}
|
||||
|
||||
public function edit($id): void
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param $args
|
||||
* @return Response
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function edit(Request $request, Response $response, $args): Response
|
||||
{
|
||||
$this->checkAdmin();
|
||||
|
||||
$user = DB::query('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', $id)->fetch();
|
||||
$user = $this->database->query('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', $args['id'])->fetch();
|
||||
|
||||
if (!$user) {
|
||||
Flight::error(new NotFoundException());
|
||||
return;
|
||||
throw new NotFoundException($request, $response);
|
||||
}
|
||||
|
||||
Flight::render('user/edit.twig', [
|
||||
return $this->view->render($response, 'user/edit.twig', [
|
||||
'user' => $user
|
||||
]);
|
||||
}
|
||||
|
||||
public function update($id): void
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param $args
|
||||
* @return Response
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function update(Request $request, Response $response, $args): Response
|
||||
{
|
||||
$this->checkAdmin();
|
||||
|
||||
$form = Flight::request()->data;
|
||||
|
||||
$user = DB::query('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', $id)->fetch();
|
||||
$user = $this->database->query('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', $args['id'])->fetch();
|
||||
|
||||
if (!$user) {
|
||||
Flight::error(new NotFoundException());
|
||||
return;
|
||||
throw new NotFoundException($request, $response);
|
||||
}
|
||||
|
||||
if (!isset($form->email) || empty($form->email)) {
|
||||
if ($request->getParam('email') === null) {
|
||||
Session::alert('The email is required.', 'danger');
|
||||
Flight::redirectBack();
|
||||
return;
|
||||
return $response->withRedirect('/user/' . $args['id'] . '/edit');
|
||||
}
|
||||
|
||||
if (!isset($form->username) || empty($form->username)) {
|
||||
if ($request->getParam('username') === null) {
|
||||
Session::alert('The username is required.', 'danger');
|
||||
Flight::redirectBack();
|
||||
return;
|
||||
return $response->withRedirect('/user/' . $args['id'] . '/edit');
|
||||
}
|
||||
|
||||
if (DB::query('SELECT COUNT(*) AS `count` FROM `users` WHERE `username` = ? AND `username` <> ?', [$form->username, $user->username])->fetch()->count > 0) {
|
||||
if ($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `username` = ? AND `username` <> ?', [$request->getParam('username'), $user->username])->fetch()->count > 0) {
|
||||
Session::alert('The username already taken.', 'danger');
|
||||
Flight::redirectBack();
|
||||
return;
|
||||
return $response->withRedirect('/user/' . $args['id'] . '/edit');
|
||||
}
|
||||
|
||||
if ($user->id === Session::get('user_id') && !isset($form->is_admin)) {
|
||||
if ($user->id === Session::get('user_id') && $request->getParam('is_admin') === null) {
|
||||
Session::alert('You cannot demote yourself.', 'danger');
|
||||
Flight::redirectBack();
|
||||
return;
|
||||
return $response->withRedirect('/user/' . $args['id'] . '/edit');
|
||||
}
|
||||
|
||||
if (isset($form->password) && !empty($form->password)) {
|
||||
DB::query('UPDATE `users` SET `email`=?, `username`=?, `password`=?, `is_admin`=?, `active`=? WHERE `id` = ?', [
|
||||
$form->email,
|
||||
$form->username,
|
||||
password_hash($form->password, PASSWORD_DEFAULT),
|
||||
isset($form->is_admin),
|
||||
isset($form->is_active),
|
||||
if ($request->getParam('password') !== null && !empty($request->getParam('password'))) {
|
||||
$this->database->query('UPDATE `users` SET `email`=?, `username`=?, `password`=?, `is_admin`=?, `active`=? WHERE `id` = ?', [
|
||||
$request->getParam('email'),
|
||||
$request->getParam('username'),
|
||||
password_hash($request->getParam('password'), PASSWORD_DEFAULT),
|
||||
$request->getParam('is_admin') !== null,
|
||||
$request->getParam('is_active') !== null,
|
||||
$user->id
|
||||
]);
|
||||
} else {
|
||||
DB::query('UPDATE `users` SET `email`=?, `username`=?, `is_admin`=?, `active`=? WHERE `id` = ?', [
|
||||
$form->email,
|
||||
$form->username,
|
||||
isset($form->is_admin),
|
||||
isset($form->is_active),
|
||||
$this->database->query('UPDATE `users` SET `email`=?, `username`=?, `is_admin`=?, `active`=? WHERE `id` = ?', [
|
||||
$request->getParam('email'),
|
||||
$request->getParam('username'),
|
||||
$request->getParam('is_admin') !== null,
|
||||
$request->getParam('is_active') !== null,
|
||||
$user->id
|
||||
]);
|
||||
}
|
||||
|
||||
Session::alert("User '$form->username' updated!", 'success');
|
||||
Log::info('User ' . Session::get('username') . " updated $user->id.", [$user, array_diff($form->getData(), ['password'])]);
|
||||
Session::alert("User '{$request->getParam('username')}' updated!", 'success');
|
||||
$this->logger->info('User ' . Session::get('username') . " updated $user->id.", [$user, array_diff($request->getParams(), ['password'])]);
|
||||
|
||||
Flight::redirect('/users');
|
||||
return $response->withRedirect('/users');
|
||||
|
||||
}
|
||||
|
||||
public function delete($id): void
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param $args
|
||||
* @return Response
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function delete(Request $request, Response $response, $args): Response
|
||||
{
|
||||
$this->checkAdmin();
|
||||
|
||||
$user = DB::query('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', $id)->fetch();
|
||||
$user = $this->database->query('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', $args['id'])->fetch();
|
||||
|
||||
if (!$user) {
|
||||
Flight::error(new NotFoundException());
|
||||
return;
|
||||
throw new NotFoundException($request, $response);
|
||||
}
|
||||
|
||||
if ($user->id === Session::get('user_id')) {
|
||||
Session::alert('You cannot delete yourself.', 'danger');
|
||||
Flight::redirectBack();
|
||||
return;
|
||||
return $response->withRedirect('/users');
|
||||
}
|
||||
|
||||
DB::query('DELETE FROM `users` WHERE `id` = ?', $user->id);
|
||||
$this->database->query('DELETE FROM `users` WHERE `id` = ?', $user->id);
|
||||
|
||||
Session::alert('User deleted.', 'success');
|
||||
Log::info('User ' . Session::get('username') . " deleted $user->id.");
|
||||
$this->logger->info('User ' . Session::get('username') . " deleted $user->id.");
|
||||
|
||||
Flight::redirect('/users');
|
||||
return $response->withRedirect('/users');
|
||||
}
|
||||
|
||||
public function profile(): void
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @return Response
|
||||
* @throws NotFoundException
|
||||
* @throws UnauthorizedException
|
||||
*/
|
||||
public function profile(Request $request, Response $response): Response
|
||||
{
|
||||
$this->checkLogin();
|
||||
|
||||
$user = DB::query('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', Session::get('user_id'))->fetch();
|
||||
$user = $this->database->query('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', Session::get('user_id'))->fetch();
|
||||
|
||||
if (!$user) {
|
||||
Flight::error(new NotFoundException());
|
||||
return;
|
||||
throw new NotFoundException($request, $response);
|
||||
}
|
||||
|
||||
if ($user->id !== Session::get('user_id') && !Session::get('admin', false)) {
|
||||
Flight::error(new UnauthorizedException());
|
||||
return;
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
Flight::render('user/profile.twig', [
|
||||
return $this->view->render($response, 'user/profile.twig', [
|
||||
'user' => $user
|
||||
]);
|
||||
}
|
||||
|
||||
public function profileEdit($id): void
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param $args
|
||||
* @return Response
|
||||
* @throws NotFoundException
|
||||
* @throws UnauthorizedException
|
||||
*/
|
||||
public function profileEdit(Request $request, Response $response, $args): Response
|
||||
{
|
||||
$this->checkLogin();
|
||||
|
||||
$form = Flight::request()->data;
|
||||
|
||||
$user = DB::query('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', $id)->fetch();
|
||||
$user = $this->database->query('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', $args['id'])->fetch();
|
||||
|
||||
if (!$user) {
|
||||
Flight::error(new NotFoundException());
|
||||
return;
|
||||
throw new NotFoundException($request, $response);
|
||||
}
|
||||
|
||||
if ($user->id !== Session::get('user_id') && !Session::get('admin', false)) {
|
||||
Flight::error(new UnauthorizedException());
|
||||
return;
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
if (!isset($form->email) || empty($form->email)) {
|
||||
if ($request->getParam('email') === null) {
|
||||
Session::alert('The email is required.', 'danger');
|
||||
Flight::redirectBack();
|
||||
return;
|
||||
return $response->withRedirect('/profile');
|
||||
}
|
||||
|
||||
if (isset($form->password) && !empty($form->password)) {
|
||||
DB::query('UPDATE `users` SET `email`=?, `password`=? WHERE `id` = ?', [
|
||||
$form->email,
|
||||
password_hash($form->password, PASSWORD_DEFAULT),
|
||||
if ($request->getParam('password') !== null && !empty($request->getParam('password'))) {
|
||||
$this->database->query('UPDATE `users` SET `email`=?, `password`=? WHERE `id` = ?', [
|
||||
$request->getParam('email'),
|
||||
password_hash($request->getParam('password'), PASSWORD_DEFAULT),
|
||||
$user->id
|
||||
]);
|
||||
} else {
|
||||
DB::query('UPDATE `users` SET `email`=? WHERE `id` = ?', [
|
||||
$form->email,
|
||||
$this->database->query('UPDATE `users` SET `email`=? WHERE `id` = ?', [
|
||||
$request->getParam('email'),
|
||||
$user->id
|
||||
]);
|
||||
}
|
||||
|
||||
Session::alert('Profile updated successfully!', 'success');
|
||||
Log::info('User ' . Session::get('username') . " updated profile of $user->id.");
|
||||
$this->logger->info('User ' . Session::get('username') . " updated profile of $user->id.");
|
||||
|
||||
Flight::redirectBack();
|
||||
return $response->withRedirect('/profile');
|
||||
}
|
||||
|
||||
public function refreshToken($id): void
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param $args
|
||||
* @return Response
|
||||
* @throws NotFoundException
|
||||
* @throws UnauthorizedException
|
||||
*/
|
||||
public function refreshToken(Request $request, Response $response, $args): Response
|
||||
{
|
||||
$this->checkLogin();
|
||||
|
||||
$user = DB::query('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', $id)->fetch();
|
||||
$user = $this->database->query('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', $args['id'])->fetch();
|
||||
|
||||
if (!$user) {
|
||||
Flight::halt(404);
|
||||
return;
|
||||
throw new NotFoundException($request, $response);
|
||||
}
|
||||
|
||||
if ($user->id !== Session::get('user_id') && !Session::get('admin', false)) {
|
||||
Flight::halt(403);
|
||||
return;
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
$token = $this->generateNewToken();
|
||||
|
||||
DB::query('UPDATE `users` SET `token`=? WHERE `id` = ?', [
|
||||
$this->database->query('UPDATE `users` SET `token`=? WHERE `id` = ?', [
|
||||
$token,
|
||||
$user->id
|
||||
]);
|
||||
|
||||
Log::info('User ' . Session::get('username') . " refreshed token of user $user->id.");
|
||||
$this->logger->info('User ' . Session::get('username') . " refreshed token of user $user->id.");
|
||||
|
||||
echo $token;
|
||||
$response->getBody()->write($token);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function getShareXconfigFile($id): void
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param $args
|
||||
* @return Response
|
||||
* @throws NotFoundException
|
||||
* @throws UnauthorizedException
|
||||
*/
|
||||
public function getShareXconfigFile(Request $request, Response $response, $args): Response
|
||||
{
|
||||
$this->checkLogin();
|
||||
|
||||
$user = DB::query('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', $id)->fetch();
|
||||
$user = $this->database->query('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', $args['id'])->fetch();
|
||||
|
||||
if (!$user) {
|
||||
Flight::halt(404);
|
||||
return;
|
||||
throw new NotFoundException($request, $response);
|
||||
}
|
||||
|
||||
if ($user->id !== Session::get('user_id') && !Session::get('admin', false)) {
|
||||
Flight::halt(403);
|
||||
return;
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
$base_url = Flight::get('config')['base_url'];
|
||||
$base_url = $this->settings['base_url'];
|
||||
$json = [
|
||||
'DestinationType' => 'ImageUploader, TextUploader, FileUploader',
|
||||
'RequestURL' => "$base_url/upload",
|
||||
|
@ -319,18 +343,19 @@ class UserController extends Controller
|
|||
'ThumbnailURL' => '$json:url$/raw',
|
||||
];
|
||||
|
||||
Flight::response()->header('Content-Type', 'application/json');
|
||||
Flight::response()->header('Content-Disposition', 'attachment;filename="' . $user->username . '-ShareX.sxcu"');
|
||||
Flight::response()->sendHeaders();
|
||||
|
||||
echo json_encode($json, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
|
||||
return $response
|
||||
->withHeader('Content-Disposition', 'attachment;filename="' . $user->username . '-ShareX.sxcu"')
|
||||
->withJson($json, 200, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function generateNewToken(): string
|
||||
{
|
||||
do {
|
||||
$token = 'token_' . md5(uniqid('', true));
|
||||
} while (DB::query('SELECT COUNT(*) AS `count` FROM `users` WHERE `token` = ?', $token)->fetch()->count > 0);
|
||||
} while ($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `token` = ?', $token)->fetch()->count > 0);
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ class DB
|
|||
protected $pdo;
|
||||
|
||||
/** @var string */
|
||||
protected static $dsn = 'database.db';
|
||||
protected static $dsn = 'sqlite:database.db';
|
||||
|
||||
/** @var string */
|
||||
protected $currentDriver;
|
||||
|
@ -67,6 +67,15 @@ class DB
|
|||
return $this->currentDriver;
|
||||
}
|
||||
|
||||
public static function getInstance(): DB
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new self(self::$dsn, self::$username, self::$password);
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a query
|
||||
* @param string $query
|
||||
|
@ -76,11 +85,7 @@ class DB
|
|||
public static function query(string $query, $parameters = [])
|
||||
{
|
||||
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new self(self::$dsn, self::$username, self::$password);
|
||||
}
|
||||
|
||||
return self::$instance->doQuery($query, $parameters);
|
||||
return self::getInstance()->doQuery($query, $parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -90,11 +95,7 @@ class DB
|
|||
public static function driver(): string
|
||||
{
|
||||
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new self(self::$dsn, self::$username, self::$password);
|
||||
}
|
||||
|
||||
return self::$instance->getCurrentDriver();
|
||||
return self::getInstance()->getCurrentDriver();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -103,11 +104,8 @@ class DB
|
|||
*/
|
||||
public static function raw(): PDO
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new self(self::$dsn, self::$username, self::$password);
|
||||
}
|
||||
|
||||
return self::$instance->getPdo();
|
||||
return self::getInstance()->getPdo();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
|
||||
use Exception;
|
||||
use Throwable;
|
||||
|
||||
class AuthenticationException extends Exception
|
||||
{
|
||||
public function __construct(string $message = 'Not Authorized', int $code = 401, Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
|
||||
use Exception;
|
||||
use Throwable;
|
||||
|
||||
class NotFoundException extends Exception
|
||||
{
|
||||
public function __construct(string $message = 'Not Found', int $code = 404, Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
40
app/Middleware/AdminMiddleware.php
Normal file
40
app/Middleware/AdminMiddleware.php
Normal file
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
namespace App\Middleware;
|
||||
|
||||
use App\Exceptions\AuthenticationException;
|
||||
use App\Exceptions\UnauthorizedException;
|
||||
use App\Web\Session;
|
||||
use Slim\Http\Request;
|
||||
use Slim\Http\Response;
|
||||
|
||||
class AdminMiddleware
|
||||
{
|
||||
/** @var \Slim\Container */
|
||||
private $container;
|
||||
|
||||
public function __construct($container)
|
||||
{
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param callable $next
|
||||
* @return Response
|
||||
* @throws UnauthorizedException
|
||||
*/
|
||||
public function __invoke(Request $request, Response $response, callable $next)
|
||||
{
|
||||
if (!$this->container->database->query('SELECT `id`, `is_admin` FROM `users` WHERE `id` = ? LIMIT 1', [Session::get('user_id')])->fetch()->is_admin) {
|
||||
Session::alert('Your account is not admin anymore.', 'danger');
|
||||
Session::set('admin', false);
|
||||
Session::set('redirectTo', (isset($_SERVER['HTTPS']) ? "https" : "http") . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]");
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
return $next($request, $response);
|
||||
}
|
||||
|
||||
}
|
44
app/Middleware/AuthMiddleware.php
Normal file
44
app/Middleware/AuthMiddleware.php
Normal file
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
namespace App\Middleware;
|
||||
|
||||
use App\Exceptions\AuthenticationException;
|
||||
use App\Web\Session;
|
||||
use Slim\Http\Request;
|
||||
use Slim\Http\Response;
|
||||
|
||||
class AuthMiddleware
|
||||
{
|
||||
|
||||
/** @var \Slim\Container */
|
||||
private $container;
|
||||
|
||||
public function __construct($container)
|
||||
{
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param callable $next
|
||||
* @return Response
|
||||
*/
|
||||
public function __invoke(Request $request, Response $response, callable $next)
|
||||
{
|
||||
if (!Session::get('logged', false)) {
|
||||
Session::set('redirectTo', (isset($_SERVER['HTTPS']) ? 'https' : 'http') . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]");
|
||||
return $response->withRedirect('/login');
|
||||
}
|
||||
|
||||
if (!$this->container->database->query('SELECT `id`, `active` FROM `users` WHERE `id` = ? LIMIT 1', [Session::get('user_id')])->fetch()->active) {
|
||||
Session::alert('Your account is not active anymore.', 'danger');
|
||||
Session::set('logged', false);
|
||||
Session::set('redirectTo', (isset($_SERVER['HTTPS']) ? 'https' : 'http') . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]");
|
||||
return $response->withRedirect('/login');
|
||||
}
|
||||
|
||||
return $next($request, $response);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
|
||||
use App\Controllers\Controller;
|
||||
|
||||
trait SingletonController
|
||||
{
|
||||
protected static $instance;
|
||||
|
||||
/**
|
||||
* Return the controller instance
|
||||
* @return Controller
|
||||
*/
|
||||
public static function instance(): Controller
|
||||
{
|
||||
if (static::$instance === null) {
|
||||
static::$instance = new static();
|
||||
}
|
||||
|
||||
return static::$instance;
|
||||
}
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Web;
|
||||
|
||||
use Monolog\Formatter\LineFormatter;
|
||||
use Monolog\Handler\RotatingFileHandler;
|
||||
use Monolog\Logger;
|
||||
|
||||
class Log
|
||||
{
|
||||
/** @var Logger */
|
||||
protected $logger;
|
||||
|
||||
/** @var Log */
|
||||
protected static $instance;
|
||||
|
||||
/**
|
||||
* @return Log
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function instance(): Log
|
||||
{
|
||||
if (static::$instance === null) {
|
||||
static::$instance = new static();
|
||||
}
|
||||
|
||||
return static::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log constructor.
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->logger = new Logger(self::class);
|
||||
|
||||
$streamHandler = new RotatingFileHandler(__DIR__ . '/../../logs/log.txt', 10, Logger::DEBUG);
|
||||
$streamHandler->setFormatter(new LineFormatter("[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n", "Y-m-d H:i:s", true));
|
||||
|
||||
$this->logger->pushHandler($streamHandler);
|
||||
}
|
||||
|
||||
public static function debug(string $message, array $context = [])
|
||||
{
|
||||
self::instance()->logger->debug($message, $context);
|
||||
}
|
||||
|
||||
public static function info(string $message, array $context = [])
|
||||
{
|
||||
self::instance()->logger->info($message, $context);
|
||||
}
|
||||
|
||||
public static function warning(string $message, array $context = [])
|
||||
{
|
||||
self::instance()->logger->warning($message, $context);
|
||||
}
|
||||
|
||||
public static function error(string $message, array $context = [])
|
||||
{
|
||||
self::instance()->logger->error($message, $context);
|
||||
}
|
||||
|
||||
public static function critical(string $message, array $context = [])
|
||||
{
|
||||
self::instance()->logger->critical($message, $context);
|
||||
}
|
||||
}
|
|
@ -1,29 +1,37 @@
|
|||
<?php
|
||||
// Auth routes
|
||||
$app->group('', function () {
|
||||
$this->get('/', \App\Controllers\DashboardController::class . ':redirects');
|
||||
$this->get('/home[/page/{page}]', \App\Controllers\DashboardController::class . ':home');
|
||||
$this->get('/system', \App\Controllers\DashboardController::class . ':system')->add(\App\Middleware\AdminMiddleware::class);
|
||||
|
||||
Flight::route('GET /login', [\App\Controllers\LoginController::instance(), 'show']);
|
||||
Flight::route('POST /login', [\App\Controllers\LoginController::instance(), 'login']);
|
||||
Flight::route('GET|POST /logout', [\App\Controllers\LoginController::instance(), 'logout']);
|
||||
$this->group('', function () {
|
||||
$this->get('/users[/page/{page}]', \App\Controllers\UserController::class . ':index');
|
||||
$this->get('/user/create', \App\Controllers\UserController::class . ':create');
|
||||
$this->post('/user/create', \App\Controllers\UserController::class . ':store');
|
||||
$this->get('/user/{id}/edit', \App\Controllers\UserController::class . ':edit');
|
||||
$this->post('/user/{id}', \App\Controllers\UserController::class . ':update');
|
||||
$this->get('/user/{id}/delete', \App\Controllers\UserController::class . ':delete');
|
||||
})->add(\App\Middleware\AdminMiddleware::class);
|
||||
|
||||
Flight::route('GET /', [\App\Controllers\DashboardController::instance(), 'redirects']);
|
||||
Flight::route('GET /home(/page/@page)', [\App\Controllers\DashboardController::instance(), 'home']);
|
||||
Flight::route('GET /system', [\App\Controllers\DashboardController::instance(), 'system']);
|
||||
$this->get('/profile', \App\Controllers\UserController::class . ':profile');
|
||||
$this->post('/profile/{id}/edit', \App\Controllers\UserController::class . ':profileEdit');
|
||||
$this->post('/user/{id}/refreshToken', \App\Controllers\UserController::class . ':refreshToken');
|
||||
$this->get('/user/{id}/config/sharex', \App\Controllers\UserController::class . ':getShareXconfigFile');
|
||||
|
||||
Flight::route('GET /users(/page/@page)', [\App\Controllers\UserController::instance(), 'index']);
|
||||
Flight::route('GET /user/add', [\App\Controllers\UserController::instance(), 'create']);
|
||||
Flight::route('POST /user/add', [\App\Controllers\UserController::instance(), 'store']);
|
||||
Flight::route('GET /user/@id/edit', [\App\Controllers\UserController::instance(), 'edit']);
|
||||
Flight::route('POST /user/@id', [\App\Controllers\UserController::instance(), 'update']);
|
||||
Flight::route('GET /user/@id/delete', [\App\Controllers\UserController::instance(), 'delete']);
|
||||
Flight::route('GET /profile', [\App\Controllers\UserController::instance(), 'profile']);
|
||||
Flight::route('POST /profile/@id/edit', [\App\Controllers\UserController::instance(), 'profileEdit']);
|
||||
Flight::route('POST /user/@id/refreshToken', [\App\Controllers\UserController::instance(), 'refreshToken']);
|
||||
Flight::route('GET /user/@id/config/sharex', [\App\Controllers\UserController::instance(), 'getShareXconfigFile']);
|
||||
$this->post('/upload/{id}/publish', \App\Controllers\UploadController::class . ':togglePublish');
|
||||
$this->post('/upload/{id}/unpublish', \App\Controllers\UploadController::class . ':togglePublish');
|
||||
$this->get('/upload/{id}/raw', \App\Controllers\UploadController::class . ':getRawById')->add(\App\Middleware\AdminMiddleware::class);
|
||||
$this->post('/upload/{id}/delete', \App\Controllers\UploadController::class . ':delete');
|
||||
|
||||
Flight::route('POST /upload', [\App\Controllers\UploadController::instance(), 'upload']);
|
||||
Flight::route('POST /upload/@id/publish', [\App\Controllers\UploadController::instance(), 'togglePublish']);
|
||||
Flight::route('POST /upload/@id/unpublish', [\App\Controllers\UploadController::instance(), 'togglePublish']);
|
||||
Flight::route('GET /upload/@id/raw', [\App\Controllers\UploadController::instance(), 'getRawById']);
|
||||
Flight::route('POST /upload/@id/delete', [\App\Controllers\UploadController::instance(), 'delete']);
|
||||
Flight::route('GET /@userCode/@filename', [\App\Controllers\UploadController::instance(), 'show']);
|
||||
Flight::route('GET /@userCode/@filename/raw', [\App\Controllers\UploadController::instance(), 'showRaw']);
|
||||
Flight::route('GET /@userCode/@filename/download', [\App\Controllers\UploadController::instance(), 'download']);
|
||||
})->add(\App\Middleware\AuthMiddleware::class);
|
||||
|
||||
$app->get('/login', \App\Controllers\LoginController::class . ':show');
|
||||
$app->post('/login', \App\Controllers\LoginController::class . ':login');
|
||||
$app->map(['GET', 'POST'], '/logout', \App\Controllers\LoginController::class . ':logout');
|
||||
|
||||
$app->post('/upload', \App\Controllers\UploadController::class . ':upload');
|
||||
|
||||
$app->get('/{userCode}/{mediaCode}', \App\Controllers\UploadController::class . ':show');
|
||||
$app->get('/{userCode}/{mediaCode}/raw', \App\Controllers\UploadController::class . ':showRaw');
|
||||
$app->get('/{userCode}/{mediaCode}/download', \App\Controllers\UploadController::class . ':download');
|
|
@ -1,13 +1,19 @@
|
|||
<?php
|
||||
|
||||
use App\Database\DB;
|
||||
use App\Web\Session;
|
||||
use Monolog\Formatter\LineFormatter;
|
||||
use Monolog\Handler\RotatingFileHandler;
|
||||
use Monolog\Logger;
|
||||
use Slim\App;
|
||||
use Slim\Container;
|
||||
|
||||
// Load the config
|
||||
$config = array_replace_recursive([
|
||||
'app_name' => 'XBackBone',
|
||||
'base_url' => isset($_SERVER['HTTPS']) ? 'https://' . $_SERVER['HTTP_HOST'] : 'http://' . $_SERVER['HTTP_HOST'],
|
||||
'storage_dir' => 'storage',
|
||||
'debug' => false,
|
||||
'displayErrorDetails' => false,
|
||||
'db' => [
|
||||
'connection' => 'sqlite',
|
||||
'dsn' => 'resources/database/xbackbone.db',
|
||||
|
@ -16,68 +22,68 @@ $config = array_replace_recursive([
|
|||
],
|
||||
], require 'config.php');
|
||||
|
||||
// Set flight parameters
|
||||
Flight::set('flight.base_url', $config['base_url']);
|
||||
Flight::set('flight.log_errors', false);
|
||||
Flight::set('config', $config);
|
||||
$container = new Container(['settings' => $config]);
|
||||
|
||||
$container['logger'] = function ($container) {
|
||||
$logger = new Logger('app');
|
||||
|
||||
$streamHandler = new RotatingFileHandler(__DIR__ . '/../logs/log.txt', 10, Logger::DEBUG);
|
||||
$streamHandler->setFormatter(new LineFormatter("[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n", "Y-m-d H:i:s", true));
|
||||
|
||||
$logger->pushHandler($streamHandler);
|
||||
|
||||
return $logger;
|
||||
};
|
||||
|
||||
|
||||
// Set the database dsn
|
||||
DB::setDsn($config['db']['connection'] . ':' . $config['db']['dsn'], $config['db']['username'], $config['db']['password']);
|
||||
|
||||
// Register the Twig instance
|
||||
Flight::register('view', 'Twig_Environment',
|
||||
[new Twig_Loader_Filesystem('resources/templates'),
|
||||
[
|
||||
'cache' => 'resources/cache',
|
||||
'autoescape' => 'html',
|
||||
'debug' => $config['debug'],
|
||||
'auto_reload' => $config['debug'],
|
||||
]
|
||||
]
|
||||
);
|
||||
$container['database'] = function ($container) use (&$config) {
|
||||
return DB::getInstance();
|
||||
};
|
||||
|
||||
// Redirect back helper
|
||||
Flight::register('redirectBack', function () {
|
||||
Flight::redirect(Flight::request()->referrer);
|
||||
});
|
||||
|
||||
// Map the render call to the Twig view instance
|
||||
Flight::map('render', function (string $template, array $data = []) use (&$config) {
|
||||
Flight::view()->addGlobal('config', $config);
|
||||
Flight::view()->addGlobal('request', Flight::request());
|
||||
Flight::view()->addGlobal('alerts', App\Web\Session::getAlert());
|
||||
Flight::view()->addGlobal('session', App\Web\Session::all());
|
||||
Flight::view()->addGlobal('PLATFORM_VERSION', PLATFORM_VERSION);
|
||||
Flight::view()->display($template, $data);
|
||||
});
|
||||
$container['view'] = function ($container) use (&$config) {
|
||||
$view = new \Slim\Views\Twig('resources/templates', [
|
||||
'cache' => 'resources/cache',
|
||||
'autoescape' => 'html',
|
||||
'debug' => $config['displayErrorDetails'],
|
||||
'auto_reload' => $config['displayErrorDetails'],
|
||||
]);
|
||||
|
||||
// The application error handler
|
||||
Flight::map('error', function (Exception $exception) {
|
||||
if ($exception instanceof \App\Exceptions\AuthenticationException) {
|
||||
Flight::redirect('/login');
|
||||
return;
|
||||
}
|
||||
// Instantiate and add Slim specific extension
|
||||
$router = $container->get('router');
|
||||
$uri = \Slim\Http\Uri::createFromEnvironment(new \Slim\Http\Environment($_SERVER));
|
||||
$view->addExtension(new Slim\Views\TwigExtension($router, $uri));
|
||||
|
||||
if ($exception instanceof \App\Exceptions\UnauthorizedException) {
|
||||
Flight::response()->status(403);
|
||||
Flight::render('errors/403.twig');
|
||||
return;
|
||||
}
|
||||
$view->getEnvironment()->addGlobal('config', $config);
|
||||
$view->getEnvironment()->addGlobal('request', $container->get('request'));
|
||||
$view->getEnvironment()->addGlobal('alerts', Session::getAlert());
|
||||
$view->getEnvironment()->addGlobal('session', Session::all());
|
||||
$view->getEnvironment()->addGlobal('PLATFORM_VERSION', PLATFORM_VERSION);
|
||||
return $view;
|
||||
};
|
||||
|
||||
if ($exception instanceof \App\Exceptions\NotFoundException) {
|
||||
Flight::response()->status(404);
|
||||
Flight::render('errors/404.twig');
|
||||
return;
|
||||
}
|
||||
$container['errorHandler'] = function ($container) {
|
||||
return function (\Slim\Http\Request $request, \Slim\Http\Response $response, $exception) use (&$container) {
|
||||
|
||||
Flight::response()->status(500);
|
||||
\App\Web\Log::critical('Fatal error during app execution', [$exception->getTraceAsString()]);
|
||||
Flight::render('errors/500.twig', ['exception' => $exception]);
|
||||
});
|
||||
if ($exception instanceof \App\Exceptions\UnauthorizedException) {
|
||||
return $container->view->render($response->withStatus(403), 'errors/403.twig');
|
||||
}
|
||||
|
||||
Flight::map('notFound', function () {
|
||||
Flight::render('errors/404.twig');
|
||||
});
|
||||
$container->logger->critical('Fatal error during app execution', [$exception, $exception->getTraceAsString()]);
|
||||
return $container->view->render($response->withStatus(500), 'errors/500.twig', ['exception' => $exception]);
|
||||
};
|
||||
};
|
||||
$container['notFoundHandler'] = function ($container) {
|
||||
return function (\Slim\Http\Request $request, \Slim\Http\Response $response) use (&$container) {
|
||||
$response->withStatus(404)->withHeader('Content-Type', 'text/html');
|
||||
return $container->view->render($response, 'errors/404.twig');
|
||||
};
|
||||
};
|
||||
|
||||
$app = new App($container);
|
||||
|
||||
// Load the application routes
|
||||
require 'app/routes.php';
|
|
@ -1,14 +1,14 @@
|
|||
{
|
||||
"name": "sergix44/xbackbone",
|
||||
"version": "1.3",
|
||||
"version": "2.0",
|
||||
"description": "A lightweight ShareX PHP backend",
|
||||
"type": "project",
|
||||
"require": {
|
||||
"mikecao/flight": "^1.3",
|
||||
"league/flysystem": "^1.0.45",
|
||||
"twig/twig": "~2.0",
|
||||
"monolog/monolog": "^1.23",
|
||||
"php": ">=7.1",
|
||||
"slim/slim": "^3.0",
|
||||
"slim/twig-view": "^2.4",
|
||||
"league/flysystem": "^1.0.45",
|
||||
"monolog/monolog": "^1.23",
|
||||
"ext-json": "*",
|
||||
"ext-gd": "*",
|
||||
"ext-pdo": "*"
|
||||
|
|
429
composer.lock
generated
429
composer.lock
generated
|
@ -4,20 +4,51 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "b2d12fd3c7627ea2da10cbb0bc3cc35d",
|
||||
"content-hash": "c1434c69fd6bdbfc70c7ca0b185ffdc9",
|
||||
"packages": [
|
||||
{
|
||||
"name": "league/flysystem",
|
||||
"version": "1.0.47",
|
||||
"name": "container-interop/container-interop",
|
||||
"version": "1.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/thephpleague/flysystem.git",
|
||||
"reference": "a11e4a75f256bdacf99d20780ce42d3b8272975c"
|
||||
"url": "https://github.com/container-interop/container-interop.git",
|
||||
"reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/a11e4a75f256bdacf99d20780ce42d3b8272975c",
|
||||
"reference": "a11e4a75f256bdacf99d20780ce42d3b8272975c",
|
||||
"url": "https://api.github.com/repos/container-interop/container-interop/zipball/79cbf1341c22ec75643d841642dd5d6acd83bdb8",
|
||||
"reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"psr/container": "^1.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Interop\\Container\\": "src/Interop/Container/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "Promoting the interoperability of container objects (DIC, SL, etc.)",
|
||||
"homepage": "https://github.com/container-interop/container-interop",
|
||||
"time": "2017-02-14T19:40:03+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/flysystem",
|
||||
"version": "1.0.48",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/thephpleague/flysystem.git",
|
||||
"reference": "a6ded5b2f6055e2db97b4b859fdfca2b952b78aa"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/a6ded5b2f6055e2db97b4b859fdfca2b952b78aa",
|
||||
"reference": "a6ded5b2f6055e2db97b4b859fdfca2b952b78aa",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -88,63 +119,20 @@
|
|||
"sftp",
|
||||
"storage"
|
||||
],
|
||||
"time": "2018-09-14T15:30:29+00:00"
|
||||
},
|
||||
{
|
||||
"name": "mikecao/flight",
|
||||
"version": "v1.3.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mikecao/flight.git",
|
||||
"reference": "e146b8c0ddbc6384c5a66f23dea71743fc282289"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/mikecao/flight/zipball/e146b8c0ddbc6384c5a66f23dea71743fc282289",
|
||||
"reference": "e146b8c0ddbc6384c5a66f23dea71743fc282289",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "~4.6"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"flight/autoload.php",
|
||||
"flight/Flight.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Mike Cao",
|
||||
"email": "mike@mikecao.com",
|
||||
"homepage": "http://www.mikecao.com/",
|
||||
"role": "Original Developer"
|
||||
}
|
||||
],
|
||||
"description": "Flight is a fast, simple, extensible framework for PHP. Flight enables you to quickly and easily build RESTful web applications.",
|
||||
"homepage": "http://flightphp.com",
|
||||
"time": "2017-10-20T01:46:56+00:00"
|
||||
"time": "2018-10-15T13:53:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "monolog/monolog",
|
||||
"version": "1.23.0",
|
||||
"version": "1.24.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Seldaek/monolog.git",
|
||||
"reference": "fd8c787753b3a2ad11bc60c063cff1358a32a3b4"
|
||||
"reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Seldaek/monolog/zipball/fd8c787753b3a2ad11bc60c063cff1358a32a3b4",
|
||||
"reference": "fd8c787753b3a2ad11bc60c063cff1358a32a3b4",
|
||||
"url": "https://api.github.com/repos/Seldaek/monolog/zipball/bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266",
|
||||
"reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -209,7 +197,202 @@
|
|||
"logging",
|
||||
"psr-3"
|
||||
],
|
||||
"time": "2017-06-19T01:22:40+00:00"
|
||||
"time": "2018-11-05T09:00:11+00:00"
|
||||
},
|
||||
{
|
||||
"name": "nikic/fast-route",
|
||||
"version": "v1.3.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/nikic/FastRoute.git",
|
||||
"reference": "181d480e08d9476e61381e04a71b34dc0432e812"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/nikic/FastRoute/zipball/181d480e08d9476e61381e04a71b34dc0432e812",
|
||||
"reference": "181d480e08d9476e61381e04a71b34dc0432e812",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.4.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^4.8.35|~5.7"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"FastRoute\\": "src/"
|
||||
},
|
||||
"files": [
|
||||
"src/functions.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nikita Popov",
|
||||
"email": "nikic@php.net"
|
||||
}
|
||||
],
|
||||
"description": "Fast request router for PHP",
|
||||
"keywords": [
|
||||
"router",
|
||||
"routing"
|
||||
],
|
||||
"time": "2018-02-13T20:26:39+00:00"
|
||||
},
|
||||
{
|
||||
"name": "pimple/pimple",
|
||||
"version": "v3.2.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/silexphp/Pimple.git",
|
||||
"reference": "9e403941ef9d65d20cba7d54e29fe906db42cf32"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/silexphp/Pimple/zipball/9e403941ef9d65d20cba7d54e29fe906db42cf32",
|
||||
"reference": "9e403941ef9d65d20cba7d54e29fe906db42cf32",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.0",
|
||||
"psr/container": "^1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/phpunit-bridge": "^3.2"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "3.2.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-0": {
|
||||
"Pimple": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
}
|
||||
],
|
||||
"description": "Pimple, a simple Dependency Injection Container",
|
||||
"homepage": "http://pimple.sensiolabs.org",
|
||||
"keywords": [
|
||||
"container",
|
||||
"dependency injection"
|
||||
],
|
||||
"time": "2018-01-21T07:42:36+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/container",
|
||||
"version": "1.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/container.git",
|
||||
"reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f",
|
||||
"reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Psr\\Container\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "PHP-FIG",
|
||||
"homepage": "http://www.php-fig.org/"
|
||||
}
|
||||
],
|
||||
"description": "Common Container Interface (PHP FIG PSR-11)",
|
||||
"homepage": "https://github.com/php-fig/container",
|
||||
"keywords": [
|
||||
"PSR-11",
|
||||
"container",
|
||||
"container-interface",
|
||||
"container-interop",
|
||||
"psr"
|
||||
],
|
||||
"time": "2017-02-14T16:28:37+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/http-message",
|
||||
"version": "1.0.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/http-message.git",
|
||||
"reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
|
||||
"reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Psr\\Http\\Message\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "PHP-FIG",
|
||||
"homepage": "http://www.php-fig.org/"
|
||||
}
|
||||
],
|
||||
"description": "Common interface for HTTP messages",
|
||||
"homepage": "https://github.com/php-fig/http-message",
|
||||
"keywords": [
|
||||
"http",
|
||||
"http-message",
|
||||
"psr",
|
||||
"psr-7",
|
||||
"request",
|
||||
"response"
|
||||
],
|
||||
"time": "2016-08-06T14:39:51+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/log",
|
||||
|
@ -258,9 +441,131 @@
|
|||
],
|
||||
"time": "2016-10-10T12:19:37+00:00"
|
||||
},
|
||||
{
|
||||
"name": "slim/slim",
|
||||
"version": "3.11.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/slimphp/Slim.git",
|
||||
"reference": "d378e70431e78ee92ee32ddde61ecc72edf5dc0a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/slimphp/Slim/zipball/d378e70431e78ee92ee32ddde61ecc72edf5dc0a",
|
||||
"reference": "d378e70431e78ee92ee32ddde61ecc72edf5dc0a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"container-interop/container-interop": "^1.2",
|
||||
"nikic/fast-route": "^1.0",
|
||||
"php": ">=5.5.0",
|
||||
"pimple/pimple": "^3.0",
|
||||
"psr/container": "^1.0",
|
||||
"psr/http-message": "^1.0"
|
||||
},
|
||||
"provide": {
|
||||
"psr/http-message-implementation": "1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^4.0",
|
||||
"squizlabs/php_codesniffer": "^2.5"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Slim\\": "Slim"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Rob Allen",
|
||||
"email": "rob@akrabat.com",
|
||||
"homepage": "http://akrabat.com"
|
||||
},
|
||||
{
|
||||
"name": "Josh Lockhart",
|
||||
"email": "hello@joshlockhart.com",
|
||||
"homepage": "https://joshlockhart.com"
|
||||
},
|
||||
{
|
||||
"name": "Gabriel Manricks",
|
||||
"email": "gmanricks@me.com",
|
||||
"homepage": "http://gabrielmanricks.com"
|
||||
},
|
||||
{
|
||||
"name": "Andrew Smith",
|
||||
"email": "a.smith@silentworks.co.uk",
|
||||
"homepage": "http://silentworks.co.uk"
|
||||
}
|
||||
],
|
||||
"description": "Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs",
|
||||
"homepage": "https://slimframework.com",
|
||||
"keywords": [
|
||||
"api",
|
||||
"framework",
|
||||
"micro",
|
||||
"router"
|
||||
],
|
||||
"time": "2018-09-16T10:54:21+00:00"
|
||||
},
|
||||
{
|
||||
"name": "slim/twig-view",
|
||||
"version": "2.4.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/slimphp/Twig-View.git",
|
||||
"reference": "78386c01a97f7870462b38fff759dad649da9efc"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/slimphp/Twig-View/zipball/78386c01a97f7870462b38fff759dad649da9efc",
|
||||
"reference": "78386c01a97f7870462b38fff759dad649da9efc",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.5.0",
|
||||
"psr/http-message": "^1.0",
|
||||
"twig/twig": "^1.18|^2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^4.8|^5.7",
|
||||
"slim/slim": "^3.10"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Slim\\Views\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Josh Lockhart",
|
||||
"email": "hello@joshlockhart.com",
|
||||
"homepage": "http://joshlockhart.com"
|
||||
}
|
||||
],
|
||||
"description": "Slim Framework 3 view helper built on top of the Twig 2 templating component",
|
||||
"homepage": "http://slimframework.com",
|
||||
"keywords": [
|
||||
"framework",
|
||||
"slim",
|
||||
"template",
|
||||
"twig",
|
||||
"view"
|
||||
],
|
||||
"time": "2018-05-07T10:54:29+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
"version": "v1.9.0",
|
||||
"version": "v1.10.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-ctype.git",
|
||||
|
@ -318,16 +623,16 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/polyfill-mbstring",
|
||||
"version": "v1.9.0",
|
||||
"version": "v1.10.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
||||
"reference": "d0cd638f4634c16d8df4508e847f14e9e43168b8"
|
||||
"reference": "c79c051f5b3a46be09205c73b80b346e4153e494"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d0cd638f4634c16d8df4508e847f14e9e43168b8",
|
||||
"reference": "d0cd638f4634c16d8df4508e847f14e9e43168b8",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/c79c051f5b3a46be09205c73b80b346e4153e494",
|
||||
"reference": "c79c051f5b3a46be09205c73b80b346e4153e494",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -373,7 +678,7 @@
|
|||
"portable",
|
||||
"shim"
|
||||
],
|
||||
"time": "2018-08-06T14:22:27+00:00"
|
||||
"time": "2018-09-21T13:07:52+00:00"
|
||||
},
|
||||
{
|
||||
"name": "twig/twig",
|
||||
|
|
|
@ -5,4 +5,4 @@ define('PLATFORM_VERSION', json_decode(file_get_contents('composer.json'))->vers
|
|||
|
||||
require 'bootstrap/app.php';
|
||||
|
||||
Flight::start();
|
||||
$app->run();
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if config.debug %}
|
||||
{% if config.displayErrorDetails %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<div class="card">
|
||||
<div class="card-header">Create User</div>
|
||||
<div class="card-body">
|
||||
<form method="post" action="{{ config.base_url }}/user/add">
|
||||
<form method="post" action="{{ config.base_url }}/user/create">
|
||||
<div class="form-group row">
|
||||
<label for="email" class="col-sm-2 col-form-label">Email</label>
|
||||
<div class="col-sm-10">
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<div class="container">
|
||||
{% include 'comp/alert.twig' %}
|
||||
<div class="text-right">
|
||||
<a href="{{ config.base_url }}/user/add" class="btn btn-outline-success mb-3"><i class="fas fa-plus"></i>
|
||||
<a href="{{ config.base_url }}/user/create" class="btn btn-outline-success mb-3"><i class="fas fa-plus"></i>
|
||||
Add User</a>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
|
|
Loading…
Add table
Reference in a new issue