added support for vanity links
This commit is contained in:
commit
634956cb2d
8 changed files with 1420 additions and 0 deletions
554
app/Controllers/MediaController.php
Executable file
554
app/Controllers/MediaController.php
Executable file
|
@ -0,0 +1,554 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Database\Repositories\UserRepository;
|
||||
use App\Web\UA;
|
||||
use GuzzleHttp\Psr7\Stream;
|
||||
use Intervention\Image\Constraint;
|
||||
use Intervention\Image\ImageManagerStatic as Image;
|
||||
use League\Flysystem\FileNotFoundException;
|
||||
use League\Flysystem\Filesystem;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Slim\Exception\HttpBadRequestException;
|
||||
use Slim\Exception\HttpNotFoundException;
|
||||
use Slim\Exception\HttpUnauthorizedException;
|
||||
|
||||
class MediaController extends Controller
|
||||
{
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param string $userCode
|
||||
* @param string $mediaCode
|
||||
* @param string|null $token
|
||||
*
|
||||
* @return Response
|
||||
* @throws HttpNotFoundException
|
||||
* @throws \Twig\Error\LoaderError
|
||||
* @throws \Twig\Error\RuntimeError
|
||||
* @throws \Twig\Error\SyntaxError
|
||||
* @throws FileNotFoundException
|
||||
*
|
||||
*/
|
||||
public function show(
|
||||
Request $request,
|
||||
Response $response,
|
||||
string $userCode,
|
||||
string $mediaCode,
|
||||
string $token = null
|
||||
): Response {
|
||||
$media = $this->getMedia($userCode, $mediaCode, true);
|
||||
|
||||
if (!$media || (!$media->published && $this->session->get('user_id') !== $media->user_id && !$this->session->get(
|
||||
'admin',
|
||||
false
|
||||
))) {
|
||||
throw new HttpNotFoundException($request);
|
||||
}
|
||||
|
||||
$filesystem = $this->storage;
|
||||
|
||||
$userAgent = $request->getHeaderLine('User-Agent');
|
||||
$mime = $filesystem->getMimetype($media->storage_path);
|
||||
|
||||
try {
|
||||
$media->mimetype = $mime;
|
||||
$media->extension = pathinfo($media->filename, PATHINFO_EXTENSION);
|
||||
$size = $filesystem->getSize($media->storage_path);
|
||||
|
||||
$type = explode('/', $media->mimetype)[0];
|
||||
if ($type === 'image' && !isDisplayableImage($media->mimetype)) {
|
||||
$type = 'application';
|
||||
$media->mimetype = 'application/octet-stream';
|
||||
}
|
||||
if ($type === 'text') {
|
||||
if ($size <= (500 * 1024)) { // less than 500 KB
|
||||
$media->text = $filesystem->read($media->storage_path);
|
||||
} else {
|
||||
$type = 'application';
|
||||
$media->mimetype = 'application/octet-stream';
|
||||
}
|
||||
}
|
||||
$media->size = humanFileSize($size);
|
||||
} catch (FileNotFoundException $e) {
|
||||
throw new HttpNotFoundException($request);
|
||||
}
|
||||
|
||||
if (
|
||||
UA::isBot($userAgent) &&
|
||||
!(
|
||||
// embed if enabled
|
||||
(UA::embedsLinks($userAgent) &&
|
||||
isEmbeddable($mime) &&
|
||||
$this->getSetting('image_embeds') === 'on') ||
|
||||
// if the file is too large to be displayed as non embedded
|
||||
(UA::embedsLinks($userAgent) &&
|
||||
isEmbeddable($mime) &&
|
||||
$size >= (8 * 1024 * 1024))
|
||||
)
|
||||
) {
|
||||
return $this->streamMedia($request, $response, $filesystem, $media);
|
||||
}
|
||||
|
||||
return view()->render($response, 'upload/public.twig', [
|
||||
'delete_token' => $token,
|
||||
'media' => $media,
|
||||
'type' => $type,
|
||||
'url' => urlFor(glue($userCode, $mediaCode)),
|
||||
'copy_raw' => $this->session->get('copy_raw', false),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param int $id
|
||||
*
|
||||
* @return Response
|
||||
* @throws HttpNotFoundException
|
||||
*
|
||||
* @throws FileNotFoundException
|
||||
*/
|
||||
public function getRawById(Request $request, Response $response, int $id): Response
|
||||
{
|
||||
$media = $this->database->query('SELECT * FROM `uploads` WHERE `id` = ? LIMIT 1', $id)->fetch();
|
||||
|
||||
if (!$media) {
|
||||
throw new HttpNotFoundException($request);
|
||||
}
|
||||
|
||||
return $this->streamMedia($request, $response, $this->storage, $media);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param string $userCode
|
||||
* @param string $mediaCode
|
||||
* @param string|null $ext
|
||||
*
|
||||
* @return Response
|
||||
* @throws HttpBadRequestException
|
||||
* @throws HttpNotFoundException
|
||||
*
|
||||
* @throws FileNotFoundException
|
||||
*/
|
||||
public function getRaw(
|
||||
Request $request,
|
||||
Response $response,
|
||||
string $userCode,
|
||||
string $mediaCode,
|
||||
?string $ext = null
|
||||
): Response {
|
||||
$media = $this->getMedia($userCode, $mediaCode, false);
|
||||
|
||||
if (!$media || (!$media->published && $this->session->get('user_id') !== $media->user_id && !$this->session->get(
|
||||
'admin',
|
||||
false
|
||||
))) {
|
||||
throw new HttpNotFoundException($request);
|
||||
}
|
||||
|
||||
if ($ext !== null && pathinfo($media->filename, PATHINFO_EXTENSION) !== $ext) {
|
||||
throw new HttpBadRequestException($request);
|
||||
}
|
||||
|
||||
if (must_be_escaped($this->storage->getMimetype($media->storage_path))) {
|
||||
$response = $this->streamMedia($request, $response, $this->storage, $media);
|
||||
return $response->withHeader('Content-Type', 'text/plain');
|
||||
}
|
||||
|
||||
return $this->streamMedia($request, $response, $this->storage, $media);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param string $userCode
|
||||
* @param string $mediaCode
|
||||
*
|
||||
* @return Response
|
||||
* @throws HttpNotFoundException
|
||||
*
|
||||
* @throws FileNotFoundException
|
||||
*/
|
||||
public function download(Request $request, Response $response, string $userCode, string $mediaCode): Response
|
||||
{
|
||||
$media = $this->getMedia($userCode, $mediaCode, false);
|
||||
|
||||
if (!$media || (!$media->published && $this->session->get('user_id') !== $media->user_id && !$this->session->get(
|
||||
'admin',
|
||||
false
|
||||
))) {
|
||||
throw new HttpNotFoundException($request);
|
||||
}
|
||||
|
||||
return $this->streamMedia($request, $response, $this->storage, $media, 'attachment');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param string $vanity
|
||||
* @param string $id
|
||||
*
|
||||
* @return Response
|
||||
* @throws HttpNotFoundException
|
||||
* @throws HttpBadRequestException
|
||||
*/
|
||||
public function createVanity(Request $request, Response $response, int $id): Response
|
||||
{
|
||||
if (!$this->session->get('admin')) {
|
||||
$media = $this->database->query('SELECT * FROM `uploads` WHERE `id` = ? LIMIT 1', $id)->fetch();
|
||||
} else {
|
||||
$media = $this->database->query(
|
||||
'SELECT * FROM `uploads` WHERE `id` = ? AND `user_id` = ? LIMIT 1',
|
||||
[$id, $this->session->get('user_id')]
|
||||
)->fetch();
|
||||
}
|
||||
|
||||
$data = $request->getParsedBody();
|
||||
$vanity = $data['vanity'];
|
||||
$vanity = strtolower(preg_replace('/[^a-z0-9]+/', '-', $vanity));
|
||||
|
||||
if (!$media) {
|
||||
throw new HttpNotFoundException($request);
|
||||
} else if ($vanity === '' || $media->code === $vanity) {
|
||||
throw new HttpBadRequestException($request);
|
||||
}
|
||||
|
||||
$this->database->query(
|
||||
'UPDATE `uploads` SET `code` = ? WHERE `id` = ?',
|
||||
[$vanity, $media->id]
|
||||
);
|
||||
|
||||
$this->logger->info('User '.$this->session->get('username').' created a vanity link for media '.$media->id);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param int $id
|
||||
*
|
||||
* @return Response
|
||||
* @throws HttpNotFoundException
|
||||
*
|
||||
*/
|
||||
public function togglePublish(Request $request, Response $response, int $id): Response
|
||||
{
|
||||
if ($this->session->get('admin')) {
|
||||
$media = $this->database->query('SELECT * FROM `uploads` WHERE `id` = ? LIMIT 1', $id)->fetch();
|
||||
} else {
|
||||
$media = $this->database->query(
|
||||
'SELECT * FROM `uploads` WHERE `id` = ? AND `user_id` = ? LIMIT 1',
|
||||
[$id, $this->session->get('user_id')]
|
||||
)->fetch();
|
||||
}
|
||||
|
||||
if (!$media) {
|
||||
throw new HttpNotFoundException($request);
|
||||
}
|
||||
|
||||
$this->database->query(
|
||||
'UPDATE `uploads` SET `published`=? WHERE `id`=?',
|
||||
[$media->published ? 0 : 1, $media->id]
|
||||
);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param int $id
|
||||
*
|
||||
* @return Response
|
||||
* @throws HttpNotFoundException
|
||||
* @throws HttpUnauthorizedException
|
||||
*/
|
||||
public function delete(Request $request, Response $response, int $id): Response
|
||||
{
|
||||
$media = $this->database->query('SELECT * FROM `uploads` WHERE `id` = ? LIMIT 1', $id)->fetch();
|
||||
|
||||
if (!$media) {
|
||||
throw new HttpNotFoundException($request);
|
||||
}
|
||||
|
||||
if (!$this->session->get('admin', false) && $media->user_id !== $this->session->get('user_id')) {
|
||||
throw new HttpUnauthorizedException($request);
|
||||
}
|
||||
|
||||
$this->deleteMedia($request, $media->storage_path, $id, $media->user_id);
|
||||
$this->logger->info('User '.$this->session->get('username').' deleted a media.', [$id]);
|
||||
|
||||
if ($media->user_id === $this->session->get('user_id')) {
|
||||
$user = make(UserRepository::class)->get($request, $media->user_id, true);
|
||||
$this->setSessionQuotaInfo($user->current_disk_quota, $user->max_disk_quota);
|
||||
}
|
||||
|
||||
if ($request->getMethod() === 'GET') {
|
||||
return redirect($response, route('home'));
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param string $userCode
|
||||
* @param string $mediaCode
|
||||
* @param string $token
|
||||
*
|
||||
* @return Response
|
||||
* @throws HttpUnauthorizedException
|
||||
*
|
||||
* @throws HttpNotFoundException
|
||||
*/
|
||||
public function deleteByToken(
|
||||
Request $request,
|
||||
Response $response,
|
||||
string $userCode,
|
||||
string $mediaCode,
|
||||
string $token
|
||||
): Response {
|
||||
$media = $this->getMedia($userCode, $mediaCode, false);
|
||||
|
||||
if (!$media) {
|
||||
throw new HttpNotFoundException($request);
|
||||
}
|
||||
|
||||
$user = $this->database->query('SELECT `id`, `active` FROM `users` WHERE `token` = ? LIMIT 1', $token)->fetch();
|
||||
|
||||
if (!$user) {
|
||||
$this->session->alert(lang('token_not_found'), 'danger');
|
||||
|
||||
return redirect($response, $request->getHeaderLine('Referer'));
|
||||
}
|
||||
|
||||
if (!$user->active) {
|
||||
$this->session->alert(lang('account_disabled'), 'danger');
|
||||
|
||||
return redirect($response, $request->getHeaderLine('Referer'));
|
||||
}
|
||||
|
||||
if ($this->session->get('admin', false) || $user->id === $media->user_id) {
|
||||
$this->deleteMedia($request, $media->storage_path, $media->mediaId, $user->id);
|
||||
$this->logger->info('User '.$user->username.' deleted a media via token.', [$media->mediaId]);
|
||||
} else {
|
||||
throw new HttpUnauthorizedException($request);
|
||||
}
|
||||
|
||||
return redirect($response, route('home'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param string $storagePath
|
||||
* @param int $id
|
||||
*
|
||||
* @param int $userId
|
||||
* @return void
|
||||
* @throws HttpNotFoundException
|
||||
*/
|
||||
protected function deleteMedia(Request $request, string $storagePath, int $id, int $userId)
|
||||
{
|
||||
try {
|
||||
$size = $this->storage->getSize($storagePath);
|
||||
$this->storage->delete($storagePath);
|
||||
$this->updateUserQuota($request, $userId, $size, true);
|
||||
} catch (FileNotFoundException $e) {
|
||||
throw new HttpNotFoundException($request);
|
||||
} finally {
|
||||
$this->database->query('DELETE FROM `uploads` WHERE `id` = ?', $id);
|
||||
$this->database->query('DELETE FROM `tags` WHERE `tags`.`id` NOT IN (SELECT `uploads_tags`.`tag_id` FROM `uploads_tags`)');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $userCode
|
||||
* @param $mediaCode
|
||||
*
|
||||
* @param bool $withTags
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getMedia($userCode, $mediaCode, $withTags = false)
|
||||
{
|
||||
$mediaCode = pathinfo($mediaCode)['filename'];
|
||||
|
||||
$media = $this->database->query(
|
||||
'SELECT `uploads`.*, `users`.*, `users`.`id` AS `userId`, `uploads`.`id` AS `mediaId` FROM `uploads` INNER JOIN `users` ON `uploads`.`user_id` = `users`.`id` WHERE `user_code` = ? AND `uploads`.`code` = ? LIMIT 1',
|
||||
[
|
||||
$userCode,
|
||||
$mediaCode,
|
||||
]
|
||||
)->fetch();
|
||||
|
||||
if (!$withTags || !$media) {
|
||||
return $media;
|
||||
}
|
||||
|
||||
$media->tags = [];
|
||||
foreach ($this->database->query(
|
||||
'SELECT `tags`.`id`, `tags`.`name` FROM `uploads_tags` INNER JOIN `tags` ON `uploads_tags`.`tag_id` = `tags`.`id` WHERE `uploads_tags`.`upload_id` = ?',
|
||||
$media->mediaId
|
||||
) as $tag) {
|
||||
$media->tags[$tag->id] = $tag->name;
|
||||
}
|
||||
|
||||
return $media;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 {
|
||||
set_time_limit(0);
|
||||
$this->session->close();
|
||||
$mime = $storage->getMimetype($media->storage_path);
|
||||
|
||||
if ((param($request, 'width') !== null || param($request, 'height') !== null) && explode(
|
||||
'/',
|
||||
$mime
|
||||
)[0] === 'image') {
|
||||
return $this->makeThumbnail(
|
||||
$storage,
|
||||
$media,
|
||||
param($request, 'width'),
|
||||
param($request, 'height'),
|
||||
$disposition
|
||||
);
|
||||
}
|
||||
|
||||
$stream = new Stream($storage->readStream($media->storage_path));
|
||||
|
||||
if (!in_array(explode('/', $mime)[0], ['image', 'video', 'audio']) || $disposition === 'attachment') {
|
||||
return $response->withHeader('Content-Type', $mime)
|
||||
->withHeader('Content-Disposition', $disposition.'; filename="'.$media->filename.'"')
|
||||
->withHeader('Content-Length', $stream->getSize())
|
||||
->withBody($stream);
|
||||
}
|
||||
|
||||
if (isset($request->getServerParams()['HTTP_RANGE'])) {
|
||||
return $this->handlePartialRequest(
|
||||
$response,
|
||||
$stream,
|
||||
$request->getServerParams()['HTTP_RANGE'],
|
||||
$disposition,
|
||||
$media,
|
||||
$mime
|
||||
);
|
||||
}
|
||||
|
||||
return $response->withHeader('Content-Type', $mime)
|
||||
->withHeader('Content-Length', $stream->getSize())
|
||||
->withHeader('Accept-Ranges', 'bytes')
|
||||
->withBody($stream);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Filesystem $storage
|
||||
* @param $media
|
||||
* @param null $width
|
||||
* @param null $height
|
||||
* @param string $disposition
|
||||
*
|
||||
* @return Response
|
||||
* @throws FileNotFoundException
|
||||
*
|
||||
*/
|
||||
protected function makeThumbnail(
|
||||
Filesystem $storage,
|
||||
$media,
|
||||
$width = null,
|
||||
$height = null,
|
||||
string $disposition = 'inline'
|
||||
) {
|
||||
return Image::make($storage->readStream($media->storage_path))
|
||||
->resize($width, $height, function (Constraint $constraint) {
|
||||
$constraint->aspectRatio();
|
||||
})
|
||||
->resizeCanvas($width, $height, 'center')
|
||||
->psrResponse('png')
|
||||
->withHeader(
|
||||
'Content-Disposition',
|
||||
$disposition.';filename="scaled-'.pathinfo($media->filename, PATHINFO_FILENAME).'.png"'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Response $response
|
||||
* @param Stream $stream
|
||||
* @param string $range
|
||||
* @param string $disposition
|
||||
* @param $media
|
||||
* @param $mime
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
protected function handlePartialRequest(
|
||||
Response $response,
|
||||
Stream $stream,
|
||||
string $range,
|
||||
string $disposition,
|
||||
$media,
|
||||
$mime
|
||||
) {
|
||||
$end = $stream->getSize() - 1;
|
||||
[, $range] = explode('=', $range, 2);
|
||||
|
||||
if (strpos($range, ',') !== false) {
|
||||
return $response->withHeader('Content-Type', $mime)
|
||||
->withHeader('Content-Disposition', $disposition.'; filename="'.$media->filename.'"')
|
||||
->withHeader('Content-Length', $stream->getSize())
|
||||
->withHeader('Accept-Ranges', 'bytes')
|
||||
->withHeader('Content-Range', "0,{$stream->getSize()}")
|
||||
->withStatus(416)
|
||||
->withBody($stream);
|
||||
}
|
||||
|
||||
if ($range === '-') {
|
||||
$start = $stream->getSize() - (int) substr($range, 1);
|
||||
} else {
|
||||
$range = explode('-', $range);
|
||||
$start = (int) $range[0];
|
||||
$end = (isset($range[1]) && is_numeric($range[1])) ? (int) $range[1] : $stream->getSize();
|
||||
}
|
||||
|
||||
if ($end > $stream->getSize() - 1) {
|
||||
$end = $stream->getSize() - 1;
|
||||
}
|
||||
$stream->seek($start);
|
||||
|
||||
header("Content-Type: $mime");
|
||||
header('Content-Length: '.($end - $start + 1));
|
||||
header('Accept-Ranges: bytes');
|
||||
header("Content-Range: bytes $start-$end/{$stream->getSize()}");
|
||||
|
||||
http_response_code(206);
|
||||
ob_end_clean();
|
||||
|
||||
fpassthru($stream->detach());
|
||||
|
||||
exit(0);
|
||||
}
|
||||
}
|
92
app/routes.php
Executable file
92
app/routes.php
Executable file
|
@ -0,0 +1,92 @@
|
|||
<?php
|
||||
|
||||
use App\Controllers\AdminController;
|
||||
use App\Controllers\Auth\LoginController;
|
||||
use App\Controllers\Auth\PasswordRecoveryController;
|
||||
use App\Controllers\Auth\RegisterController;
|
||||
use App\Controllers\ClientController;
|
||||
use App\Controllers\DashboardController;
|
||||
use App\Controllers\ExportController;
|
||||
use App\Controllers\MediaController;
|
||||
use App\Controllers\ProfileController;
|
||||
use App\Controllers\SettingController;
|
||||
use App\Controllers\TagController;
|
||||
use App\Controllers\UpgradeController;
|
||||
use App\Controllers\UploadController;
|
||||
use App\Controllers\UserController;
|
||||
use App\Middleware\AdminMiddleware;
|
||||
use App\Middleware\AuthMiddleware;
|
||||
use App\Middleware\CheckForMaintenanceMiddleware;
|
||||
use Slim\Routing\RouteCollectorProxy;
|
||||
|
||||
global $app;
|
||||
$app->group('', function (RouteCollectorProxy $group) {
|
||||
$group->get('/home[/page/{page}]', [DashboardController::class, 'home'])->setName('home');
|
||||
$group->get('/upload', [UploadController::class, 'uploadWebPage'])->setName('upload.web.show');
|
||||
$group->post('/upload/web', [UploadController::class, 'uploadWeb'])->setName('upload.web');
|
||||
$group->get('/home/switchView', [DashboardController::class, 'switchView'])->setName('switchView');
|
||||
|
||||
$group->group('', function (RouteCollectorProxy $group) {
|
||||
$group->get('/system/deleteOrphanFiles', [AdminController::class, 'deleteOrphanFiles'])->setName('system.deleteOrphanFiles');
|
||||
$group->get('/system/recalculateUserQuota', [AdminController::class, 'recalculateUserQuota'])->setName('system.recalculateUserQuota');
|
||||
|
||||
$group->get('/system/themes', [AdminController::class, 'getThemes'])->setName('theme');
|
||||
|
||||
$group->post('/system/settings/save', [SettingController::class, 'saveSettings'])->setName('settings.save');
|
||||
|
||||
$group->post('/system/upgrade', [UpgradeController::class, 'upgrade'])->setName('system.upgrade');
|
||||
$group->get('/system/checkForUpdates', [UpgradeController::class, 'checkForUpdates'])->setName('system.checkForUpdates');
|
||||
$group->get('/system/changelog', [UpgradeController::class, 'changelog'])->setName('system.changelog');
|
||||
|
||||
$group->get('/system', [AdminController::class, 'system'])->setName('system');
|
||||
|
||||
$group->get('/users[/page/{page}]', [UserController::class, 'index'])->setName('user.index');
|
||||
})->add(AdminMiddleware::class);
|
||||
|
||||
$group->group('/user', function (RouteCollectorProxy $group) {
|
||||
$group->get('/create', [UserController::class, 'create'])->setName('user.create');
|
||||
$group->post('/create', [UserController::class, 'store'])->setName('user.store');
|
||||
$group->get('/{id}/edit', [UserController::class, 'edit'])->setName('user.edit');
|
||||
$group->post('/{id}', [UserController::class, 'update'])->setName('user.update');
|
||||
$group->get('/{id}/delete', [UserController::class, 'delete'])->setName('user.delete');
|
||||
$group->get('/{id}/clear', [UserController::class, 'clearUserMedia'])->setName('user.clear');
|
||||
})->add(AdminMiddleware::class);
|
||||
|
||||
$group->get('/profile', [ProfileController::class, 'profile'])->setName('profile');
|
||||
$group->post('/profile/{id}', [ProfileController::class, 'profileEdit'])->setName('profile.update');
|
||||
$group->post('/user/{id}/refreshToken', [UserController::class, 'refreshToken'])->setName('refreshToken');
|
||||
$group->get('/user/{id}/config/sharex', [ClientController::class, 'getShareXConfig'])->setName('config.sharex');
|
||||
$group->get('/user/{id}/config/script', [ClientController::class, 'getBashScript'])->setName('config.script');
|
||||
|
||||
$group->get('/user/{id}/export', [ExportController::class, 'downloadData'])->setName('export.data');
|
||||
|
||||
$group->post('/upload/{id}/publish', [MediaController::class, 'togglePublish'])->setName('upload.publish');
|
||||
$group->post('/upload/{id}/unpublish', [MediaController::class, 'togglePublish'])->setName('upload.unpublish');
|
||||
$group->map(['PUT', 'POST', 'GET'], '/upload/{id}/vanity', [MediaController::class, 'createVanity'])->setName('upload.vanity');
|
||||
$group->get('/upload/{id}/raw', [MediaController::class, 'getRawById'])->add(AdminMiddleware::class)->setName('upload.raw');
|
||||
$group->map(['GET', 'POST'], '/upload/{id}/delete', [MediaController::class, 'delete'])->setName('upload.delete');
|
||||
|
||||
$group->post('/tag/add', [TagController::class, 'addTag'])->setName('tag.add');
|
||||
$group->post('/tag/remove', [TagController::class, 'removeTag'])->setName('tag.remove');
|
||||
})->add(App\Middleware\CheckForMaintenanceMiddleware::class)->add(AuthMiddleware::class);
|
||||
|
||||
$app->get('/', [DashboardController::class, 'redirects'])->setName('root');
|
||||
$app->get('/register', [RegisterController::class, 'registerForm'])->setName('register.show');
|
||||
$app->post('/register', [RegisterController::class, 'register'])->setName('register');
|
||||
$app->get('/activate/{activateToken}', [RegisterController::class, 'activateUser'])->setName('activate');
|
||||
$app->get('/recover', [PasswordRecoveryController::class, 'recover'])->setName('recover');
|
||||
$app->post('/recover/mail', [PasswordRecoveryController::class, 'recoverMail'])->setName('recover.mail');
|
||||
$app->get('/recover/password/{resetToken}', [PasswordRecoveryController::class, 'recoverPasswordForm'])->setName('recover.password.view');
|
||||
$app->post('/recover/password/{resetToken}', [PasswordRecoveryController::class, 'recoverPassword'])->setName('recover.password');
|
||||
$app->get('/login', [LoginController::class, 'show'])->setName('login.show');
|
||||
$app->post('/login', [LoginController::class, 'login'])->setName('login');
|
||||
$app->map(['GET', 'POST'], '/logout', [LoginController::class, 'logout'])->setName('logout');
|
||||
|
||||
$app->post('/upload', [UploadController::class, 'uploadEndpoint'])->setName('upload');
|
||||
|
||||
$app->get('/user/{token}/config/screencloud', [ClientController::class, 'getScreenCloudConfig'])->setName('config.screencloud')->add(CheckForMaintenanceMiddleware::class);
|
||||
$app->get('/{userCode}/{mediaCode}', [MediaController::class, 'show'])->setName('public');
|
||||
$app->get('/{userCode}/{mediaCode}/delete/{token}', [MediaController::class, 'show'])->setName('public.delete.show')->add(CheckForMaintenanceMiddleware::class);
|
||||
$app->post('/{userCode}/{mediaCode}/delete/{token}', [MediaController::class, 'deleteByToken'])->setName('public.delete')->add(CheckForMaintenanceMiddleware::class);
|
||||
$app->get('/{userCode}/{mediaCode}/raw[.{ext}]', [MediaController::class, 'getRaw'])->setName('public.raw');
|
||||
$app->get('/{userCode}/{mediaCode}/download', [MediaController::class, 'download'])->setName('public.download');
|
164
resources/lang/en.lang.php
Executable file
164
resources/lang/en.lang.php
Executable file
|
@ -0,0 +1,164 @@
|
|||
<?php
|
||||
return [
|
||||
'lang' => 'English',
|
||||
'enforce_language' => 'Enforce language',
|
||||
'yes' => 'Yes',
|
||||
'no' => 'No',
|
||||
'send' => 'Send',
|
||||
'no_media' => 'No media found.',
|
||||
'login.username' => 'Username or E-Mail',
|
||||
'password' => 'Password',
|
||||
'login' => 'Login',
|
||||
'username' => 'Username',
|
||||
'home' => 'Home',
|
||||
'users' => 'Users',
|
||||
'system' => 'System',
|
||||
'profile' => 'Profile',
|
||||
'logout' => 'Logout',
|
||||
'pager.next' => 'Next',
|
||||
'pager.previous' => 'Previous',
|
||||
'copy_link' => 'Copy link',
|
||||
'public.telegram' => 'Share on Telegram',
|
||||
'public.delete_text' => 'Are you sure you want to delete this item? You will not be able to recover it',
|
||||
'preview' => 'Preview',
|
||||
'filename' => 'Filename',
|
||||
'size' => 'Size',
|
||||
'public' => 'Public',
|
||||
'owner' => 'Owner',
|
||||
'date' => 'Date',
|
||||
'raw' => 'Show raw',
|
||||
'download' => 'Download',
|
||||
'upload' => 'Upload',
|
||||
'delete' => 'Delete',
|
||||
'confirm' => 'Confirm',
|
||||
'vanity_url' => 'Custom URL',
|
||||
'publish' => 'Publish',
|
||||
'hide' => 'Hide',
|
||||
'files' => 'Files',
|
||||
'orphaned_files' => 'Orphaned Files',
|
||||
'theme' => 'Theme',
|
||||
'click_to_load' => 'Click to load…',
|
||||
'apply' => 'Apply',
|
||||
'save' => 'Save',
|
||||
'used' => 'Used',
|
||||
'php_info' => 'PHP Informations',
|
||||
'system_settings' => 'System Settings',
|
||||
'user.create' => 'Create User',
|
||||
'user.edit' => 'Edit User',
|
||||
'is_active' => 'Is active',
|
||||
'is_admin' => 'Is administrator',
|
||||
'your_profile' => 'Your Profile',
|
||||
'token' => 'Token',
|
||||
'copy' => 'Copy',
|
||||
'copied' => 'Copied to clipboard!',
|
||||
'update' => 'Update',
|
||||
'edit' => 'Edit',
|
||||
'client_config' => 'Client Configuration',
|
||||
'user_code' => 'User Code',
|
||||
'active' => 'Active',
|
||||
'admin' => 'Admin',
|
||||
'reg_date' => 'Registration Date',
|
||||
'none' => 'None',
|
||||
'open' => 'Open',
|
||||
'confirm_string' => 'Are you sure?',
|
||||
'installed' => 'Installation completed successfully!',
|
||||
'bad_login' => 'Wrong credentials.',
|
||||
'account_disabled' => 'Your account is disabled.',
|
||||
'welcome' => 'Welcome, %s!',
|
||||
'goodbye' => 'Goodbye!',
|
||||
'token_not_found' => 'Token specified not found.',
|
||||
'email_required' => 'E-mail address required.',
|
||||
'email_taken' => 'The e-mail address is already in use.',
|
||||
'username_required' => 'The username is required.',
|
||||
'username_taken' => 'The username is already taken.',
|
||||
'password_required' => 'The password is required.',
|
||||
'user_created' => 'User "%s" created!',
|
||||
'user_updated' => 'User "%s" updated!',
|
||||
'profile_updated' => 'Profile updated successfully!',
|
||||
'user_deleted' => 'User deleted.',
|
||||
'cannot_delete' => 'You cannot delete yourself.',
|
||||
'cannot_demote' => 'You cannot demote yourself.',
|
||||
'cannot_write_file' => 'The destination path is not writable.',
|
||||
'deleted_orphans' => 'Successfully deleted %d orphaned files.',
|
||||
'switch_to' => 'Switch to',
|
||||
'gallery' => 'Gallery',
|
||||
'table' => 'Table',
|
||||
'dotted_search' => 'Search…',
|
||||
'order_by' => 'Order by…',
|
||||
'time' => 'Time',
|
||||
'name' => 'Name',
|
||||
'maintenance' => 'Maintenance',
|
||||
'clean_orphaned_uploads' => 'Clean Orphaned Uploads',
|
||||
'path_not_writable' => 'The output path is not writable.',
|
||||
'already_latest_version' => 'You already have the latest version.',
|
||||
'new_version_available' => 'New version %s available!',
|
||||
'cannot_retrieve_file' => 'Cannot retrieve the file.',
|
||||
'file_size_no_match' => 'The downloaded file doesn\'t match the correct file size.',
|
||||
'check_for_updates' => 'Check for updates',
|
||||
'upgrade' => 'Upgrade',
|
||||
'updates' => 'Updates',
|
||||
'maintenance_in_progress' => 'Platform under maintenance, try again later…',
|
||||
'cancel' => 'Cancel',
|
||||
'auto_set' => 'Set automatically',
|
||||
'default_lang_behavior' => 'XBackBone will try to match the browser language by default (the fallback is English).',
|
||||
'prerelease_channel' => 'Prerelease Channel',
|
||||
'no_upload_token' => 'You don\'t have a personal upload token. (Generate one and try again.)',
|
||||
'drop_to_upload' => 'Click or drop your files here to upload.',
|
||||
'donation' => 'Donation',
|
||||
'donate_text' => 'If you like XBackBone, consider a donation to support development!',
|
||||
'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_set' => 'Custom HTML head applied.',
|
||||
'remember_me' => 'Remember me',
|
||||
'please_wait' => 'Please wait…',
|
||||
'dont_close' => 'Do not close this tab until completion.',
|
||||
'register_enabled' => 'Registrations enabled',
|
||||
'hide_by_default' => 'Hide media by default',
|
||||
'copy_url_behavior' => 'Copy URL mode',
|
||||
'settings_saved' => 'System settings saved!',
|
||||
'export_data' => 'Export data',
|
||||
'password_recovery' => 'Recover password',
|
||||
'no_account' => 'Don\'t have an account?',
|
||||
'register' => 'Register',
|
||||
'register_success' => 'The account has been created, a confirmation e-mail has been sent.',
|
||||
'default_user_quota' => 'Default User Quota',
|
||||
'max_user_quota' => 'Max User Quota',
|
||||
'invalid_quota' => 'Invalid values as default user quota.',
|
||||
'mail.activate_text' => 'Hi %s!<br>thank you for creating your account on %s (<a href="%s">%s</a>), click on the following link to activate it:<br><br><a href="%s">%s</a>',
|
||||
'mail.activate_account' => '%s - Account Activation',
|
||||
'mail.recover_text' => 'Hi %s,<br>a password reset has been requested for your account. To complete the procedure click on the following link:<br><br><a href="%s">%s</a><br><br>If it wasn\'t you who requested the password reset, simply ignore this e-mail.',
|
||||
'mail.recover_password' => '%s - Password Recovery',
|
||||
'recover_email_sent' => 'If present, a recovery e-mail was sent to the specified account.',
|
||||
'account_activated' => 'Account activated, now you can login!',
|
||||
'quota_enabled' => 'Enable user quota',
|
||||
'password_repeat' => 'Repeat Password',
|
||||
'password_match' => 'Password and repeat password must be the same.',
|
||||
'password_restored' => 'Password reset.',
|
||||
'recalculate_user_quota' => 'Recalculate user quota from disk',
|
||||
'quota_recalculated' => 'User quota recalculated from the disk successfully.',
|
||||
'used_space' => 'Used Space',
|
||||
'delete_selected' => 'Delete Selected',
|
||||
'delete_all' => 'Delete All',
|
||||
'clear_account' => 'Clear Account',
|
||||
'account_media_deleted' => 'All media in the account have been deleted.',
|
||||
'danger_zone' => 'Danger Zone',
|
||||
'recaptcha_failed' => 'reCAPTCHA Failed',
|
||||
'recaptcha_enabled' => 'reCAPTCHA Enabled',
|
||||
'recaptcha_keys_required' => 'All reCAPTCHA keys are required.',
|
||||
'only_recaptcha_v3' => 'Only reCAPTCHA v3 is supported.',
|
||||
'recaptcha_site_key' => 'reCAPTCHA Site Key',
|
||||
'recaptcha_secret_key' => 'reCAPTCHA Secret Key',
|
||||
'send_notification' => 'Send E-mail Notification',
|
||||
'mail.new_account' => '%s - New Account Creation',
|
||||
'mail.new_account_text_with_reset' => 'Hi %s!<br>a new account was created for you on %s (<a href="%s">%s</a>), click on the following link to set a password and activate it:<br><br><a href="%s">%s</a>',
|
||||
'mail.new_account_text_with_pw' => 'Hi %s!<br>a new account was created for you on %s (<a href="%s">%s</a>), with the following credentials:<br><br>Username: %s<br>Password: %s<br><br>Click on the following link to go to the login page:<br><a href="%s">%s</a>',
|
||||
'user_create_password' => 'If leaved empty, you might want to send a notification to the user e-mail address.',
|
||||
'ldap_cant_connect' => 'Can\'t connect to the LDAP auth server.',
|
||||
'upload_max_file_size' => 'The max file size is currently %s.',
|
||||
'no_tags' => 'No tags added',
|
||||
'auto_tagging' => 'Auto upload tagging',
|
||||
'zip_ext_not_loaded' => 'The required "zip" extension is not loaded',
|
||||
'changelog' => 'Changelog',
|
||||
'show_changelog' => 'Show changelog',
|
||||
'image_embeds' => 'Embed images'
|
||||
];
|
19
resources/templates/comp/modal_vanity.twig
Executable file
19
resources/templates/comp/modal_vanity.twig
Executable file
|
@ -0,0 +1,19 @@
|
|||
<div class="modal fade" id="modalVanity" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog modal-sm">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{{ lang('vanity_url') }}</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="text" class="form-control" id="modalVanity-input" >
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-primary media-vanity" id="modalVanity-link"><i class="fas fa-check fa-fw"></i> {{ lang('confirm') }}</>
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ lang('no') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
74
resources/templates/dashboard/grid.twig
Executable file
74
resources/templates/dashboard/grid.twig
Executable file
|
@ -0,0 +1,74 @@
|
|||
{% extends 'base.twig' %}
|
||||
|
||||
{% block title %}{{ lang('home') }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'comp/navbar.twig' %}
|
||||
<div class="container">
|
||||
{% include 'comp/alert.twig' %}
|
||||
{% include 'dashboard/pager_header.twig' with {'path': 'home'} %}
|
||||
{% if medias|length > 0 %}
|
||||
<div class="row">
|
||||
{% for media in medias %}
|
||||
<div class="col-md-4 bulk-selector" id="media_{{ media.id }}" data-id="{{ media.id }}">
|
||||
<div class="card mb-4 shadow-sm">
|
||||
<div class="card-body image-card p-0">
|
||||
<div class="overlay">
|
||||
<div class="overlay-rows">
|
||||
<div class="overlay-rows-top">
|
||||
<div class="pl-3 pt-2d5"><span class="badge badge-dark shadow-lg">{{ media.size }}</span></div>
|
||||
<div class="text-right pr-3 pt-2d5">
|
||||
<div class="btn-group shadow-lg">
|
||||
<button type="button" class="btn btn-sm btn-success btn-clipboard" data-toggle="tooltip" title="{{ lang('copy_link') }}" data-clipboard-text="{{ urlFor(glue(media.user_code, media.code) ~ (copy_raw ? '/raw.' ~ media.extension : '.' ~ media.extension)) }}">
|
||||
<i class="fas fa-link"></i>
|
||||
</button>
|
||||
<a href="{{ urlFor('/' ~ media.user_code ~ '/' ~ media.code ~ '.' ~ media.extension ~ '/download') }}" class="btn btn-sm btn-secondary" data-toggle="tooltip" title="{{ lang('download') }}"><i class="fas fa-cloud-download-alt"></i></a>
|
||||
{% if media.published %}
|
||||
<a class="btn btn-sm btn-warning publish-toggle" data-toggle="tooltip" title="{{ lang('hide') }}" data-id="{{ media.id }}" data-published="{{ media.published }}"><i class="fas fa-times-circle"></i></a>
|
||||
{% else %}
|
||||
<a class="btn btn-sm btn-info publish-toggle" data-toggle="tooltip" title="{{ lang('publish') }}" data-id="{{ media.id }}" data-published="{{ media.published }}"><i class="fas fa-check-circle"></i></a>
|
||||
{% endif %}
|
||||
<button class="btn btn-primary btn-sm public-vanity" data-link="{{ route('upload.delete', {'id': media.id}) }}" data-id="{{ media.id }}" data-toggle="tooltip" title="{{ lang('vanity') }}"><i class="fas fa-star"></i></button>
|
||||
<button type="button" class="btn btn-sm btn-danger media-delete" data-link="{{ route('upload.delete', {'id': media.id}) }}" data-id="{{ media.id }}" data-toggle="tooltip" title="{{ lang('delete') }}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a class="btn btn-link btn-block text-light overlay-rows-center" href="{{ urlFor('/' ~ media.user_code ~ '/' ~ media.code ~ '.' ~ media.extension) }}" target="_blank">
|
||||
<div>
|
||||
<i class="fas fa-external-link-alt fa-2x text-shadow-link"></i>
|
||||
</div>
|
||||
</a>
|
||||
<div class="overlay-rows-bottom pl-3 pr-3 pb-1">
|
||||
{% for tag_id, tag_name in media.tags %}
|
||||
<a href="{{ queryParams({'tag':tag_id}) }}" class="badge badge-pill badge-light shadow-sm tag-item mr-1" data-id="{{ tag_id }}" data-media="{{ media.id }}" title="{{ tag_name }}">{{ tag_name }}</a>
|
||||
{% endfor %}
|
||||
<a href="javascript:void(0);" class="badge badge-pill badge-success shadow-sm tag-add mr-1" data-id="{{ media.id }}"><i class="fas fa-plus fa-sm fa-fw"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if isDisplayableImage(media.mimetype) %}
|
||||
<div class="content-image" style="background-image: url({{ urlFor('/' ~ media.user_code ~ '/' ~ media.code ~ '.' ~ media.extension ~ '/raw?height=267') }});"></div>
|
||||
{% else %}
|
||||
<div class="text-center" style="font-size: 178px;"><i class="far {{ mime2font(media.mimetype) }} mb-4 mt-4"></i></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-between">
|
||||
<span class="user-title" title="{{ media.filename }}">{{ media.filename }}</span>
|
||||
<small>{{ media.timestamp|date("d/m/Y H:i") }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="d-flex justify-content-center">
|
||||
{% include 'comp/pager.twig' with {'path': 'home'} %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center text-muted"><i>{{ lang('no_media') }}</i></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include 'comp/modal_vanity.twig' %}
|
||||
|
||||
{% endblock %}
|
101
resources/templates/dashboard/list.twig
Executable file
101
resources/templates/dashboard/list.twig
Executable file
|
@ -0,0 +1,101 @@
|
|||
{% extends 'base.twig' %}
|
||||
|
||||
{% block title %}{{ lang('home') }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'comp/navbar.twig' %}
|
||||
<div class="container">
|
||||
{% include 'comp/alert.twig' %}
|
||||
{% include 'comp/modal_vanity.twig' %}
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
{% include 'dashboard/pager_header.twig' with {'path': 'home'} %}
|
||||
{% if medias|length > 0 %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ lang('preview') }}</th>
|
||||
<th>{{ lang('filename') }}</th>
|
||||
<th>{{ lang('size') }}</th>
|
||||
<th>{{ lang('public') }}</th>
|
||||
{% if session.get('admin') %}
|
||||
<th>{{ lang('owner') }}</th>
|
||||
{% endif %}
|
||||
<th>{{ lang('date') }}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for media in medias %}
|
||||
<tr id="media_{{ media.id }}" class="bulk-selector" data-id="{{ media.id }}">
|
||||
<td class="text-center">
|
||||
{% if isDisplayableImage(media.mimetype) %}
|
||||
{% if media.username is not null %}
|
||||
<img src="{{ urlFor('/' ~ media.user_code ~ '/' ~ media.code ~ '.' ~ media.extension ~ '/raw?width=84&height=42') }}" class="img-fluid rounded">
|
||||
{% else %}
|
||||
<img src="{{ route('upload.raw', {'id': media.id}) }}" class="img-fluid rounded">
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<i class="far {{ mime2font(media.mimetype) }} fa-2x"></i>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="text-maxlen">{{ media.filename }}</span>
|
||||
<p>
|
||||
{% for tag_id, tag_name in media.tags %}
|
||||
<a href="{{ queryParams({'tag':tag_id}) }}" class="badge badge-pill badge-light shadow-sm tag-item mr-1" data-id="{{ tag_id }}" data-media="{{ media.id }}" title="{{ tag_name }}">{{ tag_name }}</a>
|
||||
{% endfor %}
|
||||
<a href="javascript:void(0)" class="badge badge-pill badge-success shadow-sm tag-add" data-id="{{ media.id }}"><i class="fas fa-plus fa-sm fa-fw"></i></a>
|
||||
</p>
|
||||
</td>
|
||||
<td>{{ media.size }}</td>
|
||||
<td id="published_{{ media.id }}" class="text-center">
|
||||
{% if media.published %}
|
||||
<span class="badge badge-success"><i class="fas fa-check"></i></span>
|
||||
{% else %}
|
||||
<span class="badge badge-danger"><i class="fas fa-times"></i></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if session.get('admin') %}
|
||||
<td>{{ media.username|default('<None>') }}</td>
|
||||
{% endif %}
|
||||
<td>{{ media.timestamp|date("d/m/Y H:i:s") }}</td>
|
||||
<td class="text-right">
|
||||
<div class="btn-group">
|
||||
{% if media.username is not null %}
|
||||
<a href="{{ urlFor('/' ~ media.user_code ~ '/' ~ media.code ~ '.' ~ media.extension) }}" class="btn btn-sm btn-outline-secondary" data-toggle="tooltip" title="{{ lang('open') }}" target="_blank"><i class="fas fa-external-link-alt"></i></a>
|
||||
<a href="{{ urlFor('/' ~ media.user_code ~ '/' ~ media.code ~ '.' ~ media.extension ~ '/download') }}" class="btn btn-sm btn-outline-primary" data-toggle="tooltip" title="{{ lang('download') }}"><i class="fas fa-cloud-download-alt"></i></a>
|
||||
<a href="javascript:void(0)" class="btn btn-sm btn-outline-success btn-clipboard" data-toggle="tooltip" title="{{ lang('copy_link') }}" data-clipboard-text="{{ urlFor(glue(media.user_code, media.code) ~ (copy_raw ? '/raw.' ~ media.extension : '.' ~ media.extension)) }}"><i class="fas fa-link"></i></a>
|
||||
{% else %}
|
||||
<a href="{{ route('upload.raw', {'id': media.id}) }}" class="btn btn-sm btn-outline-dark" data-toggle="tooltip" title="{{ lang('raw') }}" target="_blank"><i class="fas fa-external-link-alt"></i></a>
|
||||
{% endif %}
|
||||
{% if media.published %}
|
||||
<a href="javascript:void(0)" class="btn btn-sm btn-outline-warning publish-toggle" data-toggle="tooltip" title="{{ lang('hide') }}" data-id="{{ media.id }}" data-published="{{ media.published }}"><i class="fas fa-times-circle"></i></a>
|
||||
{% else %}
|
||||
<a href="javascript:void(0)" class="btn btn-sm btn-outline-info publish-toggle" data-toggle="tooltip" title="{{ lang('publish') }}" data-id="{{ media.id }}" data-published="{{ media.published }}"><i class="fas fa-check-circle"></i></a>
|
||||
{% endif %}
|
||||
<a href="javascript:void(0)" class="btn btn-sm btn-outline-info public-vanity" data-id="{{ media.id }}" data-toggle="tooltip" title="{{ lang('vanity') }}"><i class="fas fa-star"></i></a>
|
||||
<a href="javascript:void(0)" class="btn btn-sm btn-outline-danger media-delete" data-link="{{ route('upload.delete', {'id': media.id}) }}" data-id="{{ media.id }}" data-toggle="tooltip" title="{{ lang('delete') }}"><i class="fas fa-trash"></i></a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="d-flex justify-content-center">
|
||||
{% include 'comp/pager.twig' with {'path': 'home'} %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center text-muted"><i>{{ lang('no_media') }}</i></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
167
resources/templates/upload/public.twig
Executable file
167
resources/templates/upload/public.twig
Executable file
|
@ -0,0 +1,167 @@
|
|||
{% extends 'base.twig' %}
|
||||
|
||||
{% block title %}{{ media.filename }}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
{% if type == 'image' %}
|
||||
<link rel="preload" href="{{ url }}/raw" as="{{ type }}">
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block meta %}
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta property="og:type" content="website"/>
|
||||
<meta id="embed-title" property="og:title" content="{{ media.filename }} ({{ media.size }})">
|
||||
<meta id="embed-desc" property="og:description" content="{{ lang('date') }}: {{ media.timestamp }}">
|
||||
{% if type == 'image' %}
|
||||
<meta id="embed-image" property="og:image" content="{{ url }}/raw">
|
||||
<meta id="discord" name="twitter:image" content="{{ url }}/raw">
|
||||
<meta id="image-src" name="twitter:image:src" content="{{ url }}/raw">
|
||||
{% elseif type == 'video' %}
|
||||
<meta name="twitter:card" content="player" />
|
||||
<meta name="twitter:title" content="{{ media.filename }} ({{ media.size }})" />
|
||||
<meta name="twitter:image" content="0" />
|
||||
<meta name="twitter:player:stream" content="{{ url }}/raw" />
|
||||
<meta name="twitter:player:width" content="720" />
|
||||
<meta name="twitter:player:height" content="480" />
|
||||
<meta name="twitter:player:stream:content_type" content="{{ media.mimetype }}" />
|
||||
|
||||
<meta property="og:url" content="{{ url }}/raw" />
|
||||
<meta property="og:video" content="{{ url }}/raw" />
|
||||
<meta property="og:video:secure_url" content="{{ url }}/raw" />
|
||||
<meta property="og:video:type" content="{{ media.mimetype }}" />
|
||||
<meta property="og:video:width" content="720" />
|
||||
<meta property="og:video:height" content="480" />
|
||||
<meta property="og:image" content="0" />
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<nav class="navbar navbar-dark bg-primary navbar-expand-md mb-4">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="{{ route('root') }}">{{ config.app_name }}</a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarCollapse">
|
||||
<div class="ml-auto">
|
||||
<a href="javascript:void(0)" class="btn btn-success my-2 my-sm-0 btn-clipboard" data-toggle="tooltip" title="{{ lang('copy_link') }}" data-clipboard-text="{{ urlFor(glue(media.user_code, media.code) ~ (copy_raw ? '/raw.' ~ media.extension : '.' ~ media.extension)) }}"><i class="fas fa-link fa-lg fa-fw"></i></a>
|
||||
<a href="{{ url }}/raw" class="btn btn-secondary my-2 my-sm-0" data-toggle="tooltip" title="{{ lang('raw') }}"><i class="fas fa-file-alt fa-lg fa-fw"></i></a>
|
||||
<a href="{{ url }}/download" class="btn btn-warning my-2 my-sm-0" data-toggle="tooltip" title="{{ lang('download') }}"><i class="fas fa-cloud-download-alt fa-lg fa-fw"></i></a>
|
||||
{% if session.get('logged') %}
|
||||
<a href="javascript:void(0)" class="btn btn-primary my-2 my-sm-0 public-vanity" data-link="{{ route('upload.vanity', {'id': media.mediaId}) }}" data-id="{{ media.mediaId }}" data-toggle="tooltip" title="{{ lang('vanity') }}"><i class="fas fa-star fa-lg fa-fw"></i></a>
|
||||
<a href="javascript:void(0)" class="btn btn-danger my-2 my-sm-0 public-delete" data-link="{{ route('upload.delete', {'id': media.mediaId}) }}" data-toggle="tooltip" title="{{ lang('delete') }}"><i class="fas fa-trash fa-lg fa-fw"></i></a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container-fluid">
|
||||
{% include 'comp/alert.twig' %}
|
||||
<div class="row">
|
||||
<div class="col-md-12 justify-content-center">
|
||||
{% if delete_token is not null %}
|
||||
<form method="post" action="{{ url }}/delete/{{ delete_token }}">
|
||||
<div class="text-center mb-4">
|
||||
<p>{{ lang('public.delete_text') }}</p>
|
||||
<div class="btn-group">
|
||||
<button type="submit" class="btn btn-danger"><i class="fas fa-trash"></i> {{ lang('yes') }}</button>
|
||||
<a href="{{ url }}" class="btn btn-secondary">{{ lang('no') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% set typeMatched = false %}
|
||||
{% if type is same as ('image') %}
|
||||
{% set typeMatched = true %}
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-12">
|
||||
<img src="{{ url }}/raw" class="img-thumbnail rounded mx-auto d-block" alt="{{ media.filename }}">
|
||||
</div>
|
||||
</div>
|
||||
{% elseif type is same as ('text') %}
|
||||
{% set typeMatched = true %}
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-12">
|
||||
<pre><code>{{ media.text }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
{% elseif type is same as ('audio') %}
|
||||
{% set typeMatched = true %}
|
||||
<div class="media-player media-audio">
|
||||
<audio id="player" autoplay controls loop preload="auto">
|
||||
<source src="{{ url }}/raw" type="{{ media.mimetype }}">
|
||||
Your browser does not support HTML5 audio.
|
||||
<a href="{{ url }}/download" class="btn btn-dark btn-lg"><i class="fas fa-cloud-download-alt fa-fw"></i> Download</a>
|
||||
</audio>
|
||||
</div>
|
||||
{% elseif type is same as ('video') %}
|
||||
{% set typeMatched = true %}
|
||||
<div class="media-player">
|
||||
<video id="player" autoplay controls loop preload="auto">
|
||||
<source src="{{ url }}/raw" type="{{ media.mimetype }}">
|
||||
Your browser does not support HTML5 video.
|
||||
<a href="{{ url }}/download" class="btn btn-dark btn-lg"><i class="fas fa-cloud-download-alt fa-fw"></i> Download</a>
|
||||
</video>
|
||||
</div>
|
||||
{% elseif media.mimetype is same as ('application/pdf') %}
|
||||
{% set typeMatched = true %}
|
||||
<object type="{{ media.mimetype }}" data="{{ url }}/raw" class="pdf-viewer">
|
||||
Your browser does not support PDF previews.
|
||||
<a href="{{ url }}/download" class="btn btn-dark btn-lg"><i class="fas fa-cloud-download-alt fa-fw"></i> Download</a>
|
||||
</object>
|
||||
{% endif %}
|
||||
{% if not typeMatched %}
|
||||
<div class="text-center">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-12">
|
||||
<i class="far {{ mime2font(media.mimetype) }} fa-10x"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<b>{{ media.filename }}</b>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{{ media.size }}
|
||||
</div>
|
||||
</div>
|
||||
{% if media.tags is not empty %}
|
||||
<div class="row mt-1 mb-2">
|
||||
<div class="col-md-12 text-center">
|
||||
{% for tag_id, tag_name in media.tags %}
|
||||
<span class="badge badge-pill badge-primary shadow-sm mr-1" title="{{ tag_name }}">{{ tag_name }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-12">
|
||||
<a href="{{ url }}/download" class="btn btn-dark btn-lg"><i class="fas fa-cloud-download-alt fa-fw"></i> Download</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="row mt-1">
|
||||
<div class="col-md-12 text-center">
|
||||
{{ media.filename }}
|
||||
</div>
|
||||
</div>
|
||||
{% if media.tags is not empty %}
|
||||
<div class="row mt-2">
|
||||
<div class="col-md-12 text-center">
|
||||
{% for tag_id, tag_name in media.tags %}
|
||||
<span class="badge badge-pill badge-primary shadow-sm mr-1" title="{{ tag_name }}">{{ tag_name }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'comp/modal_delete.twig' %}
|
||||
{% include 'comp/modal_vanity.twig' %}
|
||||
{% endblock %}
|
249
src/js/app.js
Executable file
249
src/js/app.js
Executable file
|
@ -0,0 +1,249 @@
|
|||
var app = {
|
||||
init: function () {
|
||||
Dropzone.options.uploadDropzone = {
|
||||
paramName: 'upload',
|
||||
maxFilesize: window.AppConfig.max_upload_size / Math.pow(1024, 2), // MB
|
||||
dictDefaultMessage: window.AppConfig.lang.dropzone,
|
||||
error: function (file, response) {
|
||||
this.defaultOptions.error(file, response.message);
|
||||
},
|
||||
totaluploadprogress: function (uploadProgress) {
|
||||
var text = Math.round(uploadProgress) + '%';
|
||||
$('#uploadProgess').css({'width': text}).text(text);
|
||||
},
|
||||
queuecomplete: function () {
|
||||
$('#uploadProgess').css({'width': '0%'}).text('');
|
||||
},
|
||||
success: function (file, response) {
|
||||
$(file.previewElement)
|
||||
.find('.dz-filename')
|
||||
.children()
|
||||
.html('<a href="' + response.url + '">' + file.name + '</a>');
|
||||
},
|
||||
timeout: 0
|
||||
};
|
||||
},
|
||||
run: function () {
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
$('[data-toggle="popover"]').popover();
|
||||
|
||||
$('.user-delete').click(app.modalDelete);
|
||||
$('.public-delete').click(app.modalDelete);
|
||||
$('.public-vanity').click(app.modalVanity);
|
||||
$('.media-delete').click(app.mediaDelete);
|
||||
$('.publish-toggle').click(app.publishToggle);
|
||||
|
||||
$('.refresh-token').click(app.refreshToken);
|
||||
$('#themes').mousedown(app.loadThemes);
|
||||
$('.checkForUpdatesButton').click(app.checkForUpdates);
|
||||
|
||||
$('.bulk-selector').contextmenu(app.bulkSelect);
|
||||
$('#bulk-delete').click(app.bulkDelete);
|
||||
|
||||
$('.tag-add').click(app.addTag);
|
||||
$('.tag-item').contextmenu(app.removeTag);
|
||||
|
||||
|
||||
$('.alert').not('.alert-permanent').fadeTo(10000, 500).slideUp(500, function () {
|
||||
$('.alert').slideUp(500);
|
||||
});
|
||||
|
||||
new ClipboardJS('.btn-clipboard');
|
||||
new Plyr($('#player'), {ratio: '16:9'});
|
||||
|
||||
$('.footer').fadeIn(600);
|
||||
|
||||
console.log('Application is ready.');
|
||||
},
|
||||
modalDelete: function () {
|
||||
$('#modalDelete-link').attr('href', $(this).data('link'));
|
||||
$('#modalDelete').modal('show');
|
||||
},
|
||||
modalVanity: function () {
|
||||
var id = $(this).data('id');
|
||||
$('#modalVanity').modal('show');
|
||||
$('#modalVanity-link').click(function () {
|
||||
var vanity = $('#modalVanity-input').val();
|
||||
var $callerButton = $(this);
|
||||
$.post(window.AppConfig.base_url + '/upload/' + id + '/vanity', {vanity: vanity}, function () {
|
||||
$callerButton.tooltip('dispose');
|
||||
window.location.href = window.AppConfig.base_url + '/home';
|
||||
});
|
||||
})
|
||||
},
|
||||
publishToggle: function () {
|
||||
console.error('publishToggle');
|
||||
var id = $(this).data('id');
|
||||
var $callerButton = $(this);
|
||||
var isOutline = false;
|
||||
if ($(this).data('published')) {
|
||||
isOutline = $callerButton.hasClass('btn-outline-warning');
|
||||
$.post(window.AppConfig.base_url + '/upload/' + id + '/unpublish', function () {
|
||||
$callerButton
|
||||
.data('published', false)
|
||||
.tooltip('dispose')
|
||||
.attr('title', window.AppConfig.lang.publish)
|
||||
.tooltip()
|
||||
.removeClass(isOutline ? 'btn-outline-warning' : 'btn-warning')
|
||||
.addClass(isOutline ? 'btn-outline-info' : 'btn-info')
|
||||
.html('<i class="fas fa-check-circle"></i>');
|
||||
$('#published_' + id).html('<span class="badge badge-danger"><i class="fas fa-times"></i></span>');
|
||||
});
|
||||
} else {
|
||||
isOutline = $callerButton.hasClass('btn-outline-info');
|
||||
$.post(window.AppConfig.base_url + '/upload/' + id + '/publish', function () {
|
||||
$callerButton
|
||||
.data('published', true)
|
||||
.tooltip('dispose')
|
||||
.attr('title', window.AppConfig.lang.hide)
|
||||
.tooltip()
|
||||
.removeClass(isOutline ? 'btn-outline-info' : 'btn-info')
|
||||
.addClass(isOutline ? 'btn-outline-warning' : 'btn-warning')
|
||||
.html('<i class="fas fa-times-circle"></i>');
|
||||
$('#published_' + id).html('<span class="badge badge-success"><i class="fas fa-check"></i></span>');
|
||||
});
|
||||
}
|
||||
},
|
||||
mediaDelete: function () {
|
||||
console.log('mediaDelete');
|
||||
var id = $(this).data('id');
|
||||
var $callerButton = $(this);
|
||||
$.post(window.AppConfig.base_url + '/upload/' + id + '/delete', function () {
|
||||
$callerButton.tooltip('dispose');
|
||||
$('#media_' + id).fadeOut(200, function () {
|
||||
$(this).remove();
|
||||
});
|
||||
});
|
||||
},
|
||||
refreshToken: function () {
|
||||
var id = $(this).data('id');
|
||||
$.post(window.AppConfig.base_url + '/user/' + id + '/refreshToken', function (data) {
|
||||
$('#token').val(data);
|
||||
});
|
||||
},
|
||||
loadThemes: function (e) {
|
||||
e.preventDefault();
|
||||
var $themes = $('#themes');
|
||||
$.get(window.AppConfig.base_url + '/system/themes', function (data) {
|
||||
$themes.empty();
|
||||
$.each(data, function (key, value) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = value;
|
||||
opt.innerHTML = key;
|
||||
if (value === null) {
|
||||
opt.disabled = true;
|
||||
}
|
||||
$themes.append(opt);
|
||||
});
|
||||
});
|
||||
$themes.unbind('mousedown');
|
||||
},
|
||||
checkForUpdates: function () {
|
||||
$('#checkForUpdatesMessage').empty().html('<i class="fas fa-spinner fa-pulse fa-3x"></i>');
|
||||
$('#doUpgradeButton').prop('disabled', true);
|
||||
$.get(window.AppConfig.base_url + '/system/checkForUpdates?prerelease=' + $(this).data('prerelease'), function (data) {
|
||||
$('#checkForUpdatesMessage').empty().text(data.message);
|
||||
if (data.upgrade) {
|
||||
$('#doUpgradeButton').prop('disabled', false);
|
||||
} else {
|
||||
$('#doUpgradeButton').prop('disabled', true);
|
||||
}
|
||||
});
|
||||
},
|
||||
bulkSelect: function (e) {
|
||||
e.preventDefault();
|
||||
$(this).toggleClass('bg-light').toggleClass('text-danger').toggleClass('bulk-selected');
|
||||
var $bulkDelete = $('#bulk-delete');
|
||||
if ($bulkDelete.hasClass('disabled')) {
|
||||
$bulkDelete.removeClass('disabled');
|
||||
}
|
||||
},
|
||||
bulkDelete: function () {
|
||||
$('.bulk-selected').each(function (index, media) {
|
||||
$.post(window.AppConfig.base_url + '/upload/' + $(media).data('id') + '/delete', function () {
|
||||
$(media).fadeOut(200, function () {
|
||||
$(this).remove();
|
||||
});
|
||||
});
|
||||
});
|
||||
$(this).addClass('disabled');
|
||||
},
|
||||
addTag: function (e) {
|
||||
var $caller = $(this);
|
||||
var $newAddTag = $caller.clone()
|
||||
.click(app.addTag)
|
||||
.appendTo($caller.parent());
|
||||
|
||||
var tagInput = $(document.createElement('input'))
|
||||
.addClass('form-control form-control-verysm tag-input')
|
||||
.attr('data-id', $caller.data('id'))
|
||||
.attr('maxlength', 32)
|
||||
.css('width', '90px')
|
||||
.attr('onchange', 'this.value = this.value.toLowerCase();')
|
||||
.keydown(function (e) {
|
||||
if (e.keyCode === 13) { // enter -> save tag
|
||||
app.saveTag.call($(this)); // change context
|
||||
return false;
|
||||
}
|
||||
if (e.keyCode === 32) { // space -> save and add new tag
|
||||
$newAddTag.click();
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.focusout(app.saveTag);
|
||||
|
||||
$caller.off()
|
||||
.removeClass('badge-success badge-light')
|
||||
.html(tagInput)
|
||||
.children()
|
||||
.focus();
|
||||
},
|
||||
saveTag: function () {
|
||||
var tag = $(this).val();
|
||||
var mediaId = $(this).data('id');
|
||||
var $parent = $(this).parent();
|
||||
if (tag === '') {
|
||||
$parent.remove();
|
||||
return false;
|
||||
}
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: window.AppConfig.base_url + '/tag/add' + window.location.search,
|
||||
data: {'tag': tag, 'mediaId': mediaId},
|
||||
dataType: 'json',
|
||||
success: function (data) {
|
||||
if (!data.limitReached) {
|
||||
$parent.replaceWith(
|
||||
$(document.createElement('a'))
|
||||
.addClass('badge badge-pill badge-light shadow-sm tag-item mr-1')
|
||||
.attr('data-id', data.tagId)
|
||||
.attr('data-media', mediaId)
|
||||
.attr('href', data.href)
|
||||
.contextmenu(app.removeTag)
|
||||
.text(tag)
|
||||
);
|
||||
} else {
|
||||
$parent.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
removeTag: function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
var $tag = $(this);
|
||||
|
||||
$.post(window.AppConfig.base_url + '/tag/remove', {
|
||||
'tagId': $tag.data('id'),
|
||||
'mediaId': $tag.data('media')
|
||||
}, function (data) {
|
||||
$tag.remove();
|
||||
if (data.deleted) {
|
||||
$('#dropdown-tag-list > a[data-id="' + $tag.data('id') + '"]').remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
app.init();
|
||||
$(document).ready(app.run);
|
Loading…
Reference in a new issue