added support for vanity links

This commit is contained in:
SrS2225a 2022-12-18 21:22:15 -08:00
commit 634956cb2d
8 changed files with 1420 additions and 0 deletions

View 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
View 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
View 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'
];

View 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">&times;</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>

View 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 %}

View 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 %}

View 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
View 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);