From 634956cb2ded96b9d393d790e8041c6fc6a8fa0a Mon Sep 17 00:00:00 2001 From: SrS2225a Date: Sun, 18 Dec 2022 21:22:15 -0800 Subject: [PATCH] added support for vanity links --- app/Controllers/MediaController.php | 554 +++++++++++++++++++++ app/routes.php | 92 ++++ resources/lang/en.lang.php | 164 ++++++ resources/templates/comp/modal_vanity.twig | 19 + resources/templates/dashboard/grid.twig | 74 +++ resources/templates/dashboard/list.twig | 101 ++++ resources/templates/upload/public.twig | 167 +++++++ src/js/app.js | 249 +++++++++ 8 files changed, 1420 insertions(+) create mode 100755 app/Controllers/MediaController.php create mode 100755 app/routes.php create mode 100755 resources/lang/en.lang.php create mode 100755 resources/templates/comp/modal_vanity.twig create mode 100755 resources/templates/dashboard/grid.twig create mode 100755 resources/templates/dashboard/list.twig create mode 100755 resources/templates/upload/public.twig create mode 100755 src/js/app.js diff --git a/app/Controllers/MediaController.php b/app/Controllers/MediaController.php new file mode 100755 index 0000000..ac2488a --- /dev/null +++ b/app/Controllers/MediaController.php @@ -0,0 +1,554 @@ +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); + } +} diff --git a/app/routes.php b/app/routes.php new file mode 100755 index 0000000..519fede --- /dev/null +++ b/app/routes.php @@ -0,0 +1,92 @@ +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'); diff --git a/resources/lang/en.lang.php b/resources/lang/en.lang.php new file mode 100755 index 0000000..95c3650 --- /dev/null +++ b/resources/lang/en.lang.php @@ -0,0 +1,164 @@ + '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 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!
thank you for creating your account on %s (%s), click on the following link to activate it:

%s', + 'mail.activate_account' => '%s - Account Activation', + 'mail.recover_text' => 'Hi %s,
a password reset has been requested for your account. To complete the procedure click on the following link:

%s

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!
a new account was created for you on %s (%s), click on the following link to set a password and activate it:

%s', + 'mail.new_account_text_with_pw' => 'Hi %s!
a new account was created for you on %s (%s), with the following credentials:

Username: %s
Password: %s

Click on the following link to go to the login page:
%s', + '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' +]; diff --git a/resources/templates/comp/modal_vanity.twig b/resources/templates/comp/modal_vanity.twig new file mode 100755 index 0000000..52fa4e4 --- /dev/null +++ b/resources/templates/comp/modal_vanity.twig @@ -0,0 +1,19 @@ + \ No newline at end of file diff --git a/resources/templates/dashboard/grid.twig b/resources/templates/dashboard/grid.twig new file mode 100755 index 0000000..e8f29d5 --- /dev/null +++ b/resources/templates/dashboard/grid.twig @@ -0,0 +1,74 @@ +{% extends 'base.twig' %} + +{% block title %}{{ lang('home') }}{% endblock %} + +{% block content %} + {% include 'comp/navbar.twig' %} +
+ {% include 'comp/alert.twig' %} + {% include 'dashboard/pager_header.twig' with {'path': 'home'} %} + {% if medias|length > 0 %} +
+ {% for media in medias %} +
+
+
+
+
+
+
{{ media.size }}
+
+
+ + + {% if media.published %} + + {% else %} + + {% endif %} + + +
+
+
+ +
+ +
+
+
+ {% for tag_id, tag_name in media.tags %} + {{ tag_name }} + {% endfor %} + +
+
+
+ {% if isDisplayableImage(media.mimetype) %} +
+ {% else %} +
+ {% endif %} +
+ +
+
+ {% endfor %} +
+
+ {% include 'comp/pager.twig' with {'path': 'home'} %} +
+ {% else %} +
{{ lang('no_media') }}
+ {% endif %} +
+ {% include 'comp/modal_vanity.twig' %} + +{% endblock %} diff --git a/resources/templates/dashboard/list.twig b/resources/templates/dashboard/list.twig new file mode 100755 index 0000000..e18c6ea --- /dev/null +++ b/resources/templates/dashboard/list.twig @@ -0,0 +1,101 @@ +{% extends 'base.twig' %} + +{% block title %}{{ lang('home') }}{% endblock %} + +{% block content %} + {% include 'comp/navbar.twig' %} +
+ {% include 'comp/alert.twig' %} + {% include 'comp/modal_vanity.twig' %} + +
+
+ {% include 'dashboard/pager_header.twig' with {'path': 'home'} %} + {% if medias|length > 0 %} +
+
+
+ + + + + + + + {% if session.get('admin') %} + + {% endif %} + + + + + + {% for media in medias %} + + + + + + {% if session.get('admin') %} + + {% endif %} + + + + {% endfor %} + +
{{ lang('preview') }}{{ lang('filename') }}{{ lang('size') }}{{ lang('public') }}{{ lang('owner') }}{{ lang('date') }}
+ {% if isDisplayableImage(media.mimetype) %} + {% if media.username is not null %} + + {% else %} + + {% endif %} + {% else %} + + {% endif %} + + {{ media.filename }} +

+ {% for tag_id, tag_name in media.tags %} + {{ tag_name }} + {% endfor %} + +

+
{{ media.size }} + {% if media.published %} + + {% else %} + + {% endif %} + {{ media.username|default('') }}{{ media.timestamp|date("d/m/Y H:i:s") }} +
+ {% if media.username is not null %} + + + + {% else %} + + {% endif %} + {% if media.published %} + + {% else %} + + {% endif %} + + +
+
+
+
+ {% include 'comp/pager.twig' with {'path': 'home'} %} +
+
+
+ {% else %} +
{{ lang('no_media') }}
+ {% endif %} +
+
+
+{% endblock %} diff --git a/resources/templates/upload/public.twig b/resources/templates/upload/public.twig new file mode 100755 index 0000000..7351c2d --- /dev/null +++ b/resources/templates/upload/public.twig @@ -0,0 +1,167 @@ +{% extends 'base.twig' %} + +{% block title %}{{ media.filename }}{% endblock %} + +{% block head %} + {% if type == 'image' %} + + {% endif %} +{% endblock %} + +{% block meta %} + + + + + {% if type == 'image' %} + + + + {% elseif type == 'video' %} + + + + + + + + + + + + + + + + {% endif %} +{% endblock %} + +{% block content %} + +
+ {% include 'comp/alert.twig' %} +
+
+ {% if delete_token is not null %} +
+
+

{{ lang('public.delete_text') }}

+
+ + {{ lang('no') }} +
+
+
+ {% endif %} + {% set typeMatched = false %} + {% if type is same as ('image') %} + {% set typeMatched = true %} +
+
+ {{ media.filename }} +
+
+ {% elseif type is same as ('text') %} + {% set typeMatched = true %} +
+
+
{{ media.text }}
+
+
+ {% elseif type is same as ('audio') %} + {% set typeMatched = true %} +
+ +
+ {% elseif type is same as ('video') %} + {% set typeMatched = true %} +
+ +
+ {% elseif media.mimetype is same as ('application/pdf') %} + {% set typeMatched = true %} + + Your browser does not support PDF previews. + Download + + {% endif %} + {% if not typeMatched %} +
+
+
+ +
+
+
+
+ {{ media.filename }} +
+
+
+
+ {{ media.size }} +
+
+ {% if media.tags is not empty %} +
+
+ {% for tag_id, tag_name in media.tags %} + {{ tag_name }} + {% endfor %} +
+
+ {% endif %} +
+ +
+
+ {% else %} +
+
+ {{ media.filename }} +
+
+ {% if media.tags is not empty %} +
+
+ {% for tag_id, tag_name in media.tags %} + {{ tag_name }} + {% endfor %} +
+
+ {% endif %} + {% endif %} +
+
+
+ {% include 'comp/modal_delete.twig' %} + {% include 'comp/modal_vanity.twig' %} +{% endblock %} diff --git a/src/js/app.js b/src/js/app.js new file mode 100755 index 0000000..2f4c44e --- /dev/null +++ b/src/js/app.js @@ -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('' + file.name + ''); + }, + 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(''); + $('#published_' + id).html(''); + }); + } 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(''); + $('#published_' + id).html(''); + }); + } + }, + 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(''); + $('#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);