Added export feature (closes #126)

Added copy mode option (closes #117)
This commit is contained in:
Sergio Brighenti 2020-02-26 12:22:25 +01:00
parent 8e85f251b8
commit 12179f1b06
18 changed files with 294 additions and 108 deletions

View file

@ -1,6 +1,8 @@
## v.3.1 (WIP)
+ Updated system settings page.
+ The theme is now re-applied after every system update.
+ Added ability to choose between default and raw url on copy.
+ Added hide by default option.
+ Updated system settings page.
+ Updated translations.
## v.3.0.2

View file

@ -58,6 +58,8 @@ class DashboardController extends Controller
->search(param($request, 'search', null))
->run($page);
$copyUrl = $this->database->query('SELECT `value` FROM `settings` WHERE `key` = \'copy_url_behavior\'')->fetch()->value;
return view()->render(
$response,
($this->session->get('admin', false) && $this->session->get('gallery_view', true)) ? 'dashboard/list.twig' : 'dashboard/grid.twig',
@ -66,6 +68,7 @@ class DashboardController extends Controller
'next' => $page < floor($query->getPages()),
'previous' => $page >= 1,
'current_page' => ++$page,
'copy_url_behavior' => $copyUrl,
]
);
}

View file

@ -0,0 +1,50 @@
<?php
namespace App\Controllers;
use League\Flysystem\FileNotFoundException;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use ZipStream\Option\Archive;
use ZipStream\ZipStream;
class ExportController extends Controller
{
/**
* @param Request $request
* @param Response $response
* @param int|null $id
* @return Response
* @throws \Slim\Exception\HttpNotFoundException
* @throws \Slim\Exception\HttpUnauthorizedException
* @throws \ZipStream\Exception\OverflowException
*/
public function downloadData(Request $request, Response $response, int $id): Response
{
$user = $this->getUser($request, $id, true);
$medias = $this->database->query('SELECT `uploads`.`filename`, `uploads`.`storage_path` FROM `uploads` WHERE `user_id` = ?', $user->id);
set_time_limit(0);
ob_end_clean();
$options = new Archive();
$options->setSendHttpHeaders(true);
$zip = new ZipStream($user->username.'-'.time().'-export.zip', $options);
$filesystem = $this->storage;
foreach ($medias as $media) {
try {
$zip->addFileFromStream($media->filename, $filesystem->readStream($media->storage_path));
} catch (FileNotFoundException $e) {
$this->logger->error('Cannot export file', ['exception' => $e]);
}
}
$zip->finish();
exit(0);
}
}

View file

@ -66,11 +66,14 @@ class MediaController extends Controller
throw new HttpNotFoundException($request);
}
$copyUrl = $this->database->query('SELECT `value` FROM `settings` WHERE `key` = \'copy_url_behavior\'')->fetch()->value;
return view()->render($response, 'upload/public.twig', [
'delete_token' => $token,
'media' => $media,
'type' => $type,
'url' => urlFor("/{$userCode}/{$mediaCode}"),
'copy_url_behavior' => $copyUrl,
]);
}

View file

@ -0,0 +1,80 @@
<?php
namespace App\Controllers;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Exception\HttpNotFoundException;
use Slim\Exception\HttpUnauthorizedException;
class ProfileController extends Controller
{
/**
* @param Request $request
* @param Response $response
*
* @throws HttpNotFoundException
* @throws HttpUnauthorizedException
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*
* @return Response
*/
public function profile(Request $request, Response $response): Response
{
$user = $this->getUser($request, $this->session->get('user_id'), true);
return view()->render($response, 'user/edit.twig', [
'profile' => true,
'user' => $user,
]);
}
/**
* @param Request $request
* @param Response $response
* @param int $id
*
* @throws HttpNotFoundException
* @throws HttpUnauthorizedException
*
* @return Response
*/
public function profileEdit(Request $request, Response $response, int $id): Response
{
if (param($request, 'email') === null) {
$this->session->alert(lang('email_required'), 'danger');
return redirect($response, route('profile'));
}
$user = $this->getUser($request, $id, true);
if ($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `email` = ? AND `email` <> ?', [param($request, 'email'), $user->email])->fetch()->count > 0) {
$this->session->alert(lang('email_taken'), 'danger');
return redirect($response, route('profile'));
}
if (param($request, 'password') !== null && !empty(param($request, 'password'))) {
$this->database->query('UPDATE `users` SET `email`=?, `password`=? WHERE `id` = ?', [
param($request, 'email'),
password_hash(param($request, 'password'), PASSWORD_DEFAULT),
$user->id,
]);
} else {
$this->database->query('UPDATE `users` SET `email`=? WHERE `id` = ?', [
param($request, 'email'),
$user->id,
]);
}
$this->session->alert(lang('profile_updated'), 'success');
$this->logger->info('User '.$this->session->get('username')." updated profile of $user->id.");
return redirect($response, route('profile'));
}
}

View file

@ -17,13 +17,13 @@ class SettingController extends Controller
*/
public function saveSettings(Request $request, Response $response): Response
{
$this->settingUpdate('register_enabled', param($request, 'register_enabled', 'off'));
$this->settingUpdate('hide_by_default', param($request, 'hide_by_default', 'off'));
$this->settingUpdate('copy_url_behavior', param($request, 'copy_url_behavior', 'off'));
$this->updateSetting('register_enabled', param($request, 'register_enabled', 'off'));
$this->updateSetting('hide_by_default', param($request, 'hide_by_default', 'off'));
$this->updateSetting('copy_url_behavior', param($request, 'copy_url_behavior') === null ? 'default' : 'raw');
$this->applyTheme($request);
$this->applyLang($request);
$this->saveCustomHead($request);
$this->updateSetting('custom_head', param($request, 'custom_head'));
$this->session->alert(lang('settings_saved'));
@ -36,28 +36,12 @@ class SettingController extends Controller
public function applyLang(Request $request)
{
if (param($request, 'lang') !== 'auto') {
if (!$this->database->query('SELECT `value` FROM `settings` WHERE `key` = \'lang\'')->fetch()) {
$this->database->query('INSERT INTO `settings`(`key`, `value`) VALUES (\'lang\', ?)', param($request, 'lang'));
} else {
$this->database->query('UPDATE `settings` SET `value`=? WHERE `key` = \'lang\'', param($request, 'lang'));
}
$this->updateSetting('copy_url_behavior', param($request, 'lang'));
} else {
$this->database->query('DELETE FROM `settings` WHERE `key` = \'lang\'');
}
}
/**
* @param Request $request
*/
public function saveCustomHead(Request $request)
{
if ($request->getAttribute('custom_head_key_present')) {
$this->database->query('UPDATE `settings` SET `value`=? WHERE `key` = \'custom_head\'', param($request, 'custom_head'));
} else {
$this->database->query('INSERT INTO `settings`(`key`, `value`) VALUES (\'custom_head\', ?)', param($request, 'custom_head'));
}
}
/**
* @param Request $request
@ -72,7 +56,12 @@ class SettingController extends Controller
file_put_contents(BASE_DIR.'static/bootstrap/css/bootstrap.min.css', file_get_contents(param($request, 'css')));
}
$this->settingUpdate('css', param($request, 'css'));
// if is default, remove setting
if (param($request, 'css') !== 'https://bootswatch.com/_vendor/bootstrap/dist/css/bootstrap.min.css'){
$this->updateSetting('css', param($request, 'css'));
} else {
$this->database->query('DELETE FROM `settings` WHERE `key` = \'css\'');
}
}
}
@ -80,7 +69,7 @@ class SettingController extends Controller
* @param $key
* @param null $value
*/
private function settingUpdate($key, $value = null)
private function updateSetting($key, $value = null)
{
if (!$this->database->query('SELECT `value` FROM `settings` WHERE `key` = '.$this->database->getPdo()->quote($key))->fetch()) {
$this->database->query('INSERT INTO `settings`(`key`, `value`) VALUES ('.$this->database->getPdo()->quote($key).', ?)', $value);

View file

@ -237,73 +237,6 @@ class UserController extends Controller
return redirect($response, route('user.index'));
}
/**
* @param Request $request
* @param Response $response
*
* @throws HttpNotFoundException
* @throws HttpUnauthorizedException
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*
* @return Response
*/
public function profile(Request $request, Response $response): Response
{
$user = $this->getUser($request, $this->session->get('user_id'), true);
return view()->render($response, 'user/edit.twig', [
'profile' => true,
'user' => $user,
]);
}
/**
* @param Request $request
* @param Response $response
* @param int $id
*
* @throws HttpNotFoundException
* @throws HttpUnauthorizedException
*
* @return Response
*/
public function profileEdit(Request $request, Response $response, int $id): Response
{
if (param($request, 'email') === null) {
$this->session->alert(lang('email_required'), 'danger');
return redirect($response, route('profile'));
}
$user = $this->getUser($request, $id, true);
if ($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `email` = ? AND `email` <> ?', [param($request, 'email'), $user->email])->fetch()->count > 0) {
$this->session->alert(lang('email_taken'), 'danger');
return redirect($response, route('profile'));
}
if (param($request, 'password') !== null && !empty(param($request, 'password'))) {
$this->database->query('UPDATE `users` SET `email`=?, `password`=? WHERE `id` = ?', [
param($request, 'email'),
password_hash(param($request, 'password'), PASSWORD_DEFAULT),
$user->id,
]);
} else {
$this->database->query('UPDATE `users` SET `email`=? WHERE `id` = ?', [
param($request, 'email'),
$user->id,
]);
}
$this->session->alert(lang('profile_updated'), 'success');
$this->logger->info('User '.$this->session->get('username')." updated profile of $user->id.");
return redirect($response, route('profile'));
}
/**
* @param Request $request
* @param Response $response

View file

@ -19,6 +19,6 @@ class InjectMiddleware extends Middleware
$head = $this->database->query('SELECT `value` FROM `settings` WHERE `key` = \'custom_head\'')->fetch();
$this->view->getTwig()->addGlobal('customHead', $head->value ?? null);
return $handler->handle($request->withAttribute('custom_head_key_present', isset($head->value)));
return $handler->handle($request);
}
}

View file

@ -4,8 +4,10 @@
use App\Controllers\AdminController;
use App\Controllers\ClientController;
use App\Controllers\DashboardController;
use App\Controllers\ExportController;
use App\Controllers\LoginController;
use App\Controllers\MediaController;
use App\Controllers\ProfileController;
use App\Controllers\SettingController;
use App\Controllers\ThemeController;
use App\Controllers\UpgradeController;
@ -46,12 +48,14 @@ $app->group('', function (RouteCollectorProxy $group) {
$group->get('/{id}/delete', [UserController::class, 'delete'])->setName('user.delete');
})->add(AdminMiddleware::class);
$group->get('/profile', [UserController::class, 'profile'])->setName('profile');
$group->post('/profile/{id}', [UserController::class, 'profileEdit'])->setName('profile.update');
$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->get('/upload/{id}/raw', [MediaController::class, 'getRawById'])->add(AdminMiddleware::class)->setName('upload.raw');

View file

@ -5,23 +5,23 @@
"type": "project",
"require": {
"php": ">=7.1",
"ext-gd": "*",
"ext-intl": "*",
"ext-json": "*",
"ext-gd": "*",
"ext-pdo": "*",
"ext-zip": "*",
"slim/slim": "^4.0",
"php-di/slim-bridge": "^3.0",
"twig/twig": "^2.12",
"guzzlehttp/psr7": "^1.6",
"league/flysystem": "^1.0.45",
"monolog/monolog": "^1.23",
"http-interop/http-factory-guzzle": "^1.0",
"intervention/image": "^2.4",
"league/flysystem": "^1.0.45",
"league/flysystem-aws-s3-v3": "^1.0",
"maennchen/zipstream-php": "^2.0",
"monolog/monolog": "^1.23",
"php-di/slim-bridge": "^3.0",
"slim/slim": "^4.0",
"spatie/flysystem-dropbox": "^1.0",
"superbalist/flysystem-google-storage": "^7.2",
"http-interop/http-factory-guzzle": "^1.0"
"twig/twig": "^2.12"
},
"config": {
"optimize-autoloader": true,

111
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "9e8abd66e7b27ece61e41aae2cc27114",
"content-hash": "17be54724e0928ed7049b18bbcb181b9",
"packages": [
{
"name": "aws/aws-sdk-php",
@ -894,6 +894,67 @@
"description": "Flysystem adapter for the AWS S3 SDK v3.x",
"time": "2020-02-23T13:31:58+00:00"
},
{
"name": "maennchen/zipstream-php",
"version": "2.0.0",
"source": {
"type": "git",
"url": "https://github.com/maennchen/ZipStream-PHP.git",
"reference": "9ceee828f9620b2e5c075e551ec7ed8a7035ac95"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/9ceee828f9620b2e5c075e551ec7ed8a7035ac95",
"reference": "9ceee828f9620b2e5c075e551ec7ed8a7035ac95",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"myclabs/php-enum": "^1.5",
"php": ">= 7.1",
"psr/http-message": "^1.0"
},
"require-dev": {
"ext-zip": "*",
"guzzlehttp/guzzle": ">= 6.3",
"mikey179/vfsstream": "^1.6",
"phpunit/phpunit": ">= 7.5"
},
"type": "library",
"autoload": {
"psr-4": {
"ZipStream\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Paul Duncan",
"email": "pabs@pablotron.org"
},
{
"name": "Jonatan Männchen",
"email": "jonatan@maennchen.ch"
},
{
"name": "Jesse Donat",
"email": "donatj@gmail.com"
},
{
"name": "András Kolesár",
"email": "kolesar@kolesar.hu"
}
],
"description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.",
"keywords": [
"stream",
"zip"
],
"time": "2020-02-23T01:48:39+00:00"
},
{
"name": "monolog/monolog",
"version": "1.25.3",
@ -1029,6 +1090,52 @@
],
"time": "2019-12-30T18:03:34+00:00"
},
{
"name": "myclabs/php-enum",
"version": "1.7.6",
"source": {
"type": "git",
"url": "https://github.com/myclabs/php-enum.git",
"reference": "5f36467c7a87e20fbdc51e524fd8f9d1de80187c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/php-enum/zipball/5f36467c7a87e20fbdc51e524fd8f9d1de80187c",
"reference": "5f36467c7a87e20fbdc51e524fd8f9d1de80187c",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": ">=7.1"
},
"require-dev": {
"phpunit/phpunit": "^7",
"squizlabs/php_codesniffer": "1.*",
"vimeo/psalm": "^3.8"
},
"type": "library",
"autoload": {
"psr-4": {
"MyCLabs\\Enum\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP Enum contributors",
"homepage": "https://github.com/myclabs/php-enum/graphs/contributors"
}
],
"description": "PHP Enum implementation",
"homepage": "http://github.com/myclabs/php-enum",
"keywords": [
"enum"
],
"time": "2020-02-14T08:15:52+00:00"
},
{
"name": "nikic/fast-route",
"version": "v1.3.0",
@ -3326,9 +3433,9 @@
"prefer-lowest": false,
"platform": {
"php": ">=7.1",
"ext-gd": "*",
"ext-intl": "*",
"ext-json": "*",
"ext-gd": "*",
"ext-pdo": "*",
"ext-zip": "*"
},

View file

@ -217,6 +217,12 @@ $app->post('/', function (Request $request, Response $response, Filesystem $stor
return redirect($response, '/install');
}
// re-apply the previous theme if is present
$css = $db->query('SELECT `value` FROM `settings` WHERE `key` = \'css\'')->fetch()->value;
if ($css) {
file_put_contents(BASE_DIR.'static/bootstrap/css/bootstrap.min.css', file_get_contents($css));
}
// post install cleanup
cleanDirectory(__DIR__.'/../resources/cache');
cleanDirectory(__DIR__.'/../resources/sessions');

View file

@ -115,4 +115,5 @@ return [
'hide_by_default' => 'Hide uploads by default',
'copy_url_behavior' => 'Copy URL mode',
'settings_saved' => 'System settings saved!',
'export_data' => 'Export Data'
];

View file

@ -23,7 +23,7 @@
<div class="col-12">
<span class="badge badge-dark shadow-lg">{{ media.size }}</span>
<div class="btn-group shadow-lg float-right">
<button type="button" class="btn btn-sm btn-success btn-clipboard" data-toggle="tooltip" title="{{ lang('copy_link') }}" data-clipboard-text="{{ urlFor('/' ~ media.user_code ~ '/' ~ media.code ~ '.' ~ media.extension) }}">
<button type="button" class="btn btn-sm btn-success btn-clipboard" data-toggle="tooltip" title="{{ lang('copy_link') }}" data-clipboard-text="{{ urlFor('/' ~ media.user_code ~ '/' ~ media.code ~ '.' ~ media.extension) }}{{ copy_url_behavior == 'raw' ? '/raw' }}">
<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>

View file

@ -56,7 +56,7 @@
{% 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('/' ~ media.user_code ~ '/' ~ media.code ~ '.' ~ media.extension) }}"><i class="fas fa-link"></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('/' ~ media.user_code ~ '/' ~ media.code ~ '.' ~ media.extension) }}{{ copy_url_behavior == 'raw' ? '/raw' }}"><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 %}

View file

@ -77,7 +77,7 @@
<div class="form-group row">
<label for="custom_head" class="col-sm-4 col-form-label">{{ lang('copy_url_behavior') }}</label>
<div class="col-sm-8">
<input type="checkbox" name="copy_url_behavior" data-toggle="toggle" data-off="Default URL" data-on="Raw URL" data-onstyle="primary" data-offstyle="secondary" {{ copy_url_behavior == 'on' ? 'checked' }}>
<input type="checkbox" name="copy_url_behavior" data-toggle="toggle" data-off="Default URL" data-on="Raw URL" data-onstyle="primary" data-offstyle="secondary" {{ copy_url_behavior == 'raw' ? 'checked' }}>
</div>
</div>

View file

@ -17,7 +17,7 @@
</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="{{ url }}"><i class="fas fa-link fa-lg fa-fw"></i></a>
<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="{{ url }}{{ copy_url_behavior == 'raw' ? '/raw' }}"><i class="fas fa-link fa-lg fa-fw"></i></a>
<a href="javascript:void(0)" class="btn btn-info my-2 my-sm-0" data-toggle="tooltip" title="{{ lang('public.telegram') }}" onclick="$('#modalTelegramShare').modal('toggle')"><i class="fab fa-telegram-plane 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>

View file

@ -57,6 +57,14 @@
</div>
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label">{{ lang('export_data') }}</label>
<div class="col-sm-10">
<div class="btn-group">
<a href="{{ route('export.data', {'id': user.id}) }}" class="btn btn-lg btn-outline-warning"><i class="fas fa-fw fa-file-archive"></i> {{ lang('download') }}</a>
</div>
</div>
</div>
{% if not profile %}
<div class="form-group row">
<div class="col-sm-2"></div>