parent
5adb29d700
commit
748bd98abf
15 changed files with 122 additions and 53 deletions
|
@ -1,12 +1,13 @@
|
||||||
## v.3.0 (WIP)
|
## v.3.0 (WIP)
|
||||||
+ Upgraded from Slim3 to Slim 4.
|
+ Upgraded from Slim3 to Slim 4.
|
||||||
+ Added web upload.
|
+ Added web upload.
|
||||||
|
+ Added ability to add custom HTML in \<head\> tag.
|
||||||
|
+ Added ability to show a preview of PDF files.
|
||||||
+ Raw URL now accept file extensions.
|
+ Raw URL now accept file extensions.
|
||||||
+ Improved installer.
|
+ Improved installer.
|
||||||
+ Improved thumbnail generation.
|
+ Improved thumbnail generation.
|
||||||
+ Replaced videojs player with Plyr.
|
+ Replaced videojs player with Plyr.
|
||||||
+ Implemented SameSite XSS protection.
|
+ Implemented SameSite XSS protection.
|
||||||
+ Added ability to add custom HTML in <head> tag.
|
|
||||||
+ Small fixes and improvements.
|
+ Small fixes and improvements.
|
||||||
|
|
||||||
## v.2.6.6
|
## v.2.6.6
|
||||||
|
|
|
@ -28,6 +28,7 @@ class LoginController extends Controller
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @param Response $response
|
* @param Response $response
|
||||||
* @return Response
|
* @return Response
|
||||||
|
* @throws \Exception
|
||||||
*/
|
*/
|
||||||
public function login(Request $request, Response $response): Response
|
public function login(Request $request, Response $response): Response
|
||||||
{
|
{
|
||||||
|
@ -58,6 +59,30 @@ class LoginController extends Controller
|
||||||
$this->session->alert(lang('welcome', [$result->username]), 'info');
|
$this->session->alert(lang('welcome', [$result->username]), 'info');
|
||||||
$this->logger->info("User $result->username logged in.");
|
$this->logger->info("User $result->username logged in.");
|
||||||
|
|
||||||
|
if (param($request, 'remember') === 'on') {
|
||||||
|
$selector = bin2hex(random_bytes(8));
|
||||||
|
$token = bin2hex(random_bytes(32));
|
||||||
|
$expire = time() + 604800; // a week
|
||||||
|
|
||||||
|
$this->database->query('UPDATE `users` SET `remember_selector`=?, `remember_token`=?, `remember_expire`=? WHERE `id`=?', [
|
||||||
|
$selector,
|
||||||
|
password_hash($token, PASSWORD_DEFAULT),
|
||||||
|
date('Y-m-d\TH:i:s', $expire),
|
||||||
|
$result->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Workaround for php <= 7.3
|
||||||
|
if (PHP_VERSION_ID < 70300) {
|
||||||
|
setcookie('remember', "{$selector}:{$token}", $expire, '; SameSite=Lax', '', false, true);
|
||||||
|
} else {
|
||||||
|
setcookie('remember', "{$selector}:{$token}", [
|
||||||
|
'expires' => $expire,
|
||||||
|
'httponly' => true,
|
||||||
|
'samesite' => 'Lax',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($this->session->has('redirectTo')) {
|
if ($this->session->has('redirectTo')) {
|
||||||
return redirect($response, $this->session->get('redirectTo'));
|
return redirect($response, $this->session->get('redirectTo'));
|
||||||
}
|
}
|
||||||
|
@ -66,14 +91,20 @@ class LoginController extends Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @param Request $request
|
||||||
* @param Response $response
|
* @param Response $response
|
||||||
* @return Response
|
* @return Response
|
||||||
*/
|
*/
|
||||||
public function logout(Response $response): Response
|
public function logout(Request $request,Response $response): Response
|
||||||
{
|
{
|
||||||
$this->session->clear();
|
$this->session->clear();
|
||||||
$this->session->set('logged', false);
|
$this->session->set('logged', false);
|
||||||
$this->session->alert(lang('goodbye'), 'warning');
|
$this->session->alert(lang('goodbye'), 'warning');
|
||||||
|
|
||||||
|
if (!empty($request->getCookieParams()['remember'])) {
|
||||||
|
setcookie('remember', null);
|
||||||
|
}
|
||||||
|
|
||||||
return redirect($response, route('login.show'));
|
return redirect($response, route('login.show'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -250,6 +250,7 @@ class UploadController extends Controller
|
||||||
* @param string|null $ext
|
* @param string|null $ext
|
||||||
* @return Response
|
* @return Response
|
||||||
* @throws FileNotFoundException
|
* @throws FileNotFoundException
|
||||||
|
* @throws HttpBadRequestException
|
||||||
* @throws HttpNotFoundException
|
* @throws HttpNotFoundException
|
||||||
*/
|
*/
|
||||||
public function showRaw(Request $request, Response $response, string $userCode, string $mediaCode, ?string $ext = null): Response
|
public function showRaw(Request $request, Response $response, string $userCode, string $mediaCode, ?string $ext = null): Response
|
||||||
|
|
|
@ -2,50 +2,13 @@
|
||||||
|
|
||||||
namespace App\Middleware;
|
namespace App\Middleware;
|
||||||
|
|
||||||
|
use App\Controllers\Controller;
|
||||||
use App\Database\DB;
|
|
||||||
use App\Web\Lang;
|
|
||||||
use App\Web\Session;
|
|
||||||
use App\Web\View;
|
|
||||||
use DI\Container;
|
|
||||||
use League\Flysystem\Filesystem;
|
|
||||||
use Monolog\Logger;
|
|
||||||
use Psr\Http\Message\ResponseInterface as Response;
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
|
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
|
||||||
|
|
||||||
/**
|
abstract class Middleware extends Controller
|
||||||
* @property Session|null session
|
|
||||||
* @property View view
|
|
||||||
* @property DB|null database
|
|
||||||
* @property Logger|null logger
|
|
||||||
* @property Filesystem|null storage
|
|
||||||
* @property Lang lang
|
|
||||||
* @property array config
|
|
||||||
*/
|
|
||||||
abstract class Middleware
|
|
||||||
{
|
{
|
||||||
/** @var Container */
|
|
||||||
protected $container;
|
|
||||||
|
|
||||||
public function __construct(Container $container)
|
|
||||||
{
|
|
||||||
$this->container = $container;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param $name
|
|
||||||
* @return mixed|null
|
|
||||||
* @throws \DI\DependencyException
|
|
||||||
* @throws \DI\NotFoundException
|
|
||||||
*/
|
|
||||||
public function __get($name)
|
|
||||||
{
|
|
||||||
if ($this->container->has($name)) {
|
|
||||||
return $this->container->get($name);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
|
|
40
app/Middleware/RememberMiddleware.php
Normal file
40
app/Middleware/RememberMiddleware.php
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace App\Middleware;
|
||||||
|
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
|
||||||
|
|
||||||
|
class RememberMiddleware extends Middleware
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Request $request
|
||||||
|
* @param RequestHandler $handler
|
||||||
|
* @return Response
|
||||||
|
*/
|
||||||
|
public function __invoke(Request $request, RequestHandler $handler)
|
||||||
|
{
|
||||||
|
if (!$this->session->get('logged', false) && !empty($request->getCookieParams()['remember'])) {
|
||||||
|
list($selector, $token) = explode(':', $request->getCookieParams()['remember']);
|
||||||
|
|
||||||
|
$result = $this->database->query('SELECT `id`, `email`, `username`,`is_admin`, `active`, `remember_token` FROM `users` WHERE `remember_selector` = ? AND `remember_expire` > ? LIMIT 1',
|
||||||
|
[$selector, date('Y-m-d\TH:i:s', time())]
|
||||||
|
)->fetch();
|
||||||
|
|
||||||
|
if ($result && password_verify($token, $result->remember_token) && $result->active) {
|
||||||
|
$this->session->set('logged', true);
|
||||||
|
$this->session->set('user_id', $result->id);
|
||||||
|
$this->session->set('username', $result->username);
|
||||||
|
$this->session->set('admin', $result->is_admin);
|
||||||
|
$this->session->set('used_space', humanFileSize($this->getUsedSpaceByUser($result->id)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return $handler->handle($request);
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,7 +28,7 @@ if (!function_exists('humanRandomString')) {
|
||||||
* @param int $length
|
* @param int $length
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
function humanRandomString(int $length = 13): string
|
function humanRandomString(int $length = 10): string
|
||||||
{
|
{
|
||||||
$result = '';
|
$result = '';
|
||||||
$numberOffset = round($length * 0.2);
|
$numberOffset = round($length * 0.2);
|
||||||
|
|
|
@ -5,6 +5,7 @@ use App\Exception\Handlers\AppErrorHandler;
|
||||||
use App\Exception\Handlers\Renderers\HtmlErrorRenderer;
|
use App\Exception\Handlers\Renderers\HtmlErrorRenderer;
|
||||||
use App\Factories\ViewFactory;
|
use App\Factories\ViewFactory;
|
||||||
use App\Middleware\InjectMiddleware;
|
use App\Middleware\InjectMiddleware;
|
||||||
|
use App\Middleware\RememberMiddleware;
|
||||||
use App\Web\Lang;
|
use App\Web\Lang;
|
||||||
use App\Web\Session;
|
use App\Web\Session;
|
||||||
use Aws\S3\S3Client;
|
use Aws\S3\S3Client;
|
||||||
|
@ -149,7 +150,8 @@ if (!$config['debug']) {
|
||||||
$app->getRouteCollector()->setCacheFile(BASE_DIR.'resources/cache/routes.cache.php');
|
$app->getRouteCollector()->setCacheFile(BASE_DIR.'resources/cache/routes.cache.php');
|
||||||
}
|
}
|
||||||
|
|
||||||
$app->addRoutingMiddleware();
|
$app->add(InjectMiddleware::class);
|
||||||
|
$app->add(RememberMiddleware::class);
|
||||||
|
|
||||||
// Permanently redirect paths with a trailing slash to their non-trailing counterpart
|
// Permanently redirect paths with a trailing slash to their non-trailing counterpart
|
||||||
$app->add(function (Request $request, RequestHandler $handler) {
|
$app->add(function (Request $request, RequestHandler $handler) {
|
||||||
|
@ -173,7 +175,7 @@ $app->add(function (Request $request, RequestHandler $handler) {
|
||||||
return $handler->handle($request);
|
return $handler->handle($request);
|
||||||
});
|
});
|
||||||
|
|
||||||
$app->add(InjectMiddleware::class);
|
$app->addRoutingMiddleware();
|
||||||
|
|
||||||
// Configure the error handler
|
// Configure the error handler
|
||||||
$errorHandler = new AppErrorHandler($app->getCallableResolver(), $app->getResponseFactory());
|
$errorHandler = new AppErrorHandler($app->getCallableResolver(), $app->getResponseFactory());
|
||||||
|
|
|
@ -107,4 +107,5 @@ return [
|
||||||
'custom_head_html' => 'Custom HTML Head content',
|
'custom_head_html' => 'Custom HTML Head content',
|
||||||
'custom_head_html_hint' => 'This content will be added at the <head> tag on every page.',
|
'custom_head_html_hint' => 'This content will be added at the <head> tag on every page.',
|
||||||
'custom_head_set' => 'Custom Head HTML applied successfully.',
|
'custom_head_set' => 'Custom Head HTML applied successfully.',
|
||||||
|
'remember_me' => 'Remember me',
|
||||||
];
|
];
|
||||||
|
|
7
resources/schemas/mysql/mysql.4.sql
Normal file
7
resources/schemas/mysql/mysql.4.sql
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
ALTER TABLE `users`
|
||||||
|
ADD COLUMN `remember_selector` VARCHAR(16),
|
||||||
|
ADD COLUMN `remember_token` VARCHAR(256),
|
||||||
|
ADD COLUMN `remember_expire` TIMESTAMP;
|
||||||
|
|
||||||
|
ALTER TABLE `users` ADD INDEX (`remember_selector`);
|
||||||
|
|
6
resources/schemas/sqlite/sqlite.4.sql
Normal file
6
resources/schemas/sqlite/sqlite.4.sql
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
ALTER TABLE `users` ADD COLUMN `remember_selector` VARCHAR(16);
|
||||||
|
ALTER TABLE `users` ADD COLUMN `remember_token` VARCHAR(256);
|
||||||
|
ALTER TABLE `users` ADD COLUMN `remember_expire` TIMESTAMP;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS `remember_selector_index`
|
||||||
|
ON `users` (`remember_selector`);
|
|
@ -26,8 +26,8 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<form class="form-signin text-center" method="post" action="{{ route('login') }}">
|
<form class="form-signin" method="post" action="{{ route('login') }}">
|
||||||
<div class="row">
|
<div class="row text-center">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<h1 class="h3 mb-3 font-weight-normal">{{ config.app_name }}</h1>
|
<h1 class="h3 mb-3 font-weight-normal">{{ config.app_name }}</h1>
|
||||||
{% include 'comp/alert.twig' %}
|
{% include 'comp/alert.twig' %}
|
||||||
|
@ -39,9 +39,13 @@
|
||||||
<input type="text" id="username" class="form-control" placeholder="{{ lang('login.username') }}" name="username" required autofocus>
|
<input type="text" id="username" class="form-control" placeholder="{{ lang('login.username') }}" name="username" required autofocus>
|
||||||
<label for="password" class="sr-only">{{ lang('password') }}</label>
|
<label for="password" class="sr-only">{{ lang('password') }}</label>
|
||||||
<input type="password" id="password" class="form-control" placeholder="{{ lang('password') }}" name="password" required>
|
<input type="password" id="password" class="form-control" placeholder="{{ lang('password') }}" name="password" required>
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" name="remember" class="form-check-input float-left" id="remember">
|
||||||
|
<label class="form-check-label" for="remember">{{ lang('remember_me') }}</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row mt-2">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<button class="btn btn-lg btn-primary btn-block" type="submit">{{ lang('login') }}</button>
|
<button class="btn btn-lg btn-primary btn-block" type="submit">{{ lang('login') }}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -24,8 +24,8 @@
|
||||||
<a class="dropdown-item" href="{{ queryParams({'sort':'size'}) }}"><i class="fas fa-weight-hanging fa-fw"></i> {{ lang('size') }}</a>
|
<a class="dropdown-item" href="{{ queryParams({'sort':'size'}) }}"><i class="fas fa-weight-hanging fa-fw"></i> {{ lang('size') }}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a href="{{ queryParams({'order': request.param('order') is same as('ASC') ? 'DESC' : 'ASC' }) }}" class="btn btn-outline-info">
|
<a href="{{ queryParams({'order': request.queryParams['order'] is same as('ASC') ? 'DESC' : 'ASC' }) }}" class="btn btn-outline-info">
|
||||||
<i class="fas {{ request.param('order') is same as('ASC') ? 'fa-sort-amount-up' : 'fa-sort-amount-down' }}"></i>
|
<i class="fas {{ request.queryParams['order'] is same as('ASC') ? 'fa-sort-amount-up' : 'fa-sort-amount-down' }}"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -67,6 +67,7 @@
|
||||||
<audio id="player" autoplay controls loop preload="auto">
|
<audio id="player" autoplay controls loop preload="auto">
|
||||||
<source src="{{ urlFor('/' ~ media.user_code ~ '/' ~ media.code ~ '.' ~ extension ~ '/raw') }}" type="{{ media.mimetype }}">
|
<source src="{{ urlFor('/' ~ media.user_code ~ '/' ~ media.code ~ '.' ~ extension ~ '/raw') }}" type="{{ media.mimetype }}">
|
||||||
Your browser does not support HTML5 audio.
|
Your browser does not support HTML5 audio.
|
||||||
|
<a href="{{ urlFor('/' ~ media.user_code ~ '/' ~ media.code ~ '.' ~ extension ~ '/download') }}" class="btn btn-dark btn-lg"><i class="fas fa-cloud-download-alt fa-fw"></i> Download</a>
|
||||||
</audio>
|
</audio>
|
||||||
</div>
|
</div>
|
||||||
{% elseif type is same as ('video') %}
|
{% elseif type is same as ('video') %}
|
||||||
|
@ -74,8 +75,14 @@
|
||||||
<video id="player" autoplay controls loop preload="auto">
|
<video id="player" autoplay controls loop preload="auto">
|
||||||
<source src="{{ urlFor('/' ~ media.user_code ~ '/' ~ media.code ~ '.' ~ extension ~ '/raw') }}" type="{{ media.mimetype }}">
|
<source src="{{ urlFor('/' ~ media.user_code ~ '/' ~ media.code ~ '.' ~ extension ~ '/raw') }}" type="{{ media.mimetype }}">
|
||||||
Your browser does not support HTML5 video.
|
Your browser does not support HTML5 video.
|
||||||
|
<a href="{{ urlFor('/' ~ media.user_code ~ '/' ~ media.code ~ '.' ~ extension ~ '/download') }}" class="btn btn-dark btn-lg"><i class="fas fa-cloud-download-alt fa-fw"></i> Download</a>
|
||||||
</video>
|
</video>
|
||||||
</div>
|
</div>
|
||||||
|
{% elseif media.mimetype is same as ('application/pdf') %}
|
||||||
|
<object type="{{ media.mimetype }}" data="{{ urlFor('/' ~ media.user_code ~ '/' ~ media.code ~ '.' ~ extension ~ '/raw') }}" class="pdf-viewer">
|
||||||
|
Your browser does not support PDF previews.
|
||||||
|
<a href="{{ urlFor('/' ~ media.user_code ~ '/' ~ media.code ~ '.' ~ extension ~ '/download') }}" class="btn btn-dark btn-lg"><i class="fas fa-cloud-download-alt fa-fw"></i> Download</a>
|
||||||
|
</object>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
|
@ -116,7 +123,8 @@
|
||||||
<textarea type="text" class="form-control mb-2" id="telegram-share-text" onclick="this.select()">{{ media.filename }}</textarea>
|
<textarea type="text" class="form-control mb-2" id="telegram-share-text" onclick="this.select()">{{ media.filename }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-info btn-block" id="telegram-share-button" onclick="app.telegramShare()" data-url="https://telegram.me/share/url?url={{ urlFor('/' ~ media.user_code ~ '/' ~ media.code ~ '.' ~ extension) }}&text="><i class="fab fa-telegram-plane fa-lg fa-fw"></i> {{ lang("send") }}</button>
|
<button type="button" class="btn btn-info btn-block" id="telegram-share-button" onclick="app.telegramShare()" data-url="https://telegram.me/share/url?url={{ urlFor('/' ~ media.user_code ~ '/' ~ media.code ~ '.' ~ extension) }}&text=">
|
||||||
|
<i class="fab fa-telegram-plane fa-lg fa-fw"></i> {{ lang("send") }}</button>
|
||||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ lang('cancel') }}</button>
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ lang('cancel') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -35,7 +35,7 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-signin input[type="password"] {
|
.form-signin input[type="password"] {
|
||||||
margin-bottom: 10px;
|
margin-bottom: .50rem;
|
||||||
border-top-left-radius: 0;
|
border-top-left-radius: 0;
|
||||||
border-top-right-radius: 0;
|
border-top-right-radius: 0;
|
||||||
}
|
}
|
||||||
|
@ -95,7 +95,6 @@ body {
|
||||||
width: 77%;
|
width: 77%;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-audio {
|
.media-audio {
|
||||||
|
@ -106,6 +105,7 @@ body {
|
||||||
.dropzone {
|
.dropzone {
|
||||||
min-height: 300px;
|
min-height: 300px;
|
||||||
border: 2px dashed rgba(0, 0, 0, 0.3);
|
border: 2px dashed rgba(0, 0, 0, 0.3);
|
||||||
|
background: none;
|
||||||
padding: 20px 20px;
|
padding: 20px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,4 +115,9 @@ body {
|
||||||
|
|
||||||
.system-tile {
|
.system-tile {
|
||||||
font-size: 2.5rem;
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-viewer {
|
||||||
|
width: 100%;
|
||||||
|
height: 85vh
|
||||||
}
|
}
|
|
@ -25,7 +25,7 @@ var app = {
|
||||||
});
|
});
|
||||||
|
|
||||||
new ClipboardJS('.btn-clipboard');
|
new ClipboardJS('.btn-clipboard');
|
||||||
new Plyr($('#player'));
|
new Plyr($('#player'), {ratio: '16:9'});
|
||||||
|
|
||||||
$('.footer').fadeIn(600);
|
$('.footer').fadeIn(600);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue