From 07beee93a922d34813181715bca749da47a10b08 Mon Sep 17 00:00:00 2001 From: fipwmaqzufheoxq92ebc <29818044+fipwmaqzufheoxq92ebc@users.noreply.github.com> Date: Mon, 31 Aug 2020 20:07:46 +0200 Subject: [PATCH] Add user-field-type "file" (#977) * Add user-field-type "file" * Add userfield-type "picture" * Also limit image height on userfieldsform * Prevent empty userfields (cause warnings in tables after deleting a file) * Show files in dialogs Co-authored-by: Bernd Bestel --- controllers/FilesApiController.php | 124 +++++++++++++------- localization/userfield_types.pot | 6 + migrations/0112.sql | 16 +++ public/js/extensions.js | 3 + public/viewjs/components/userfieldsform.js | 52 ++++++++ public/viewjs/equipmentform.js | 2 +- routes.php | 4 +- services/UserfieldsService.php | 4 +- views/components/userfields_tbody.blade.php | 7 ++ views/components/userfieldsform.blade.php | 22 ++++ 10 files changed, 192 insertions(+), 48 deletions(-) create mode 100644 migrations/0112.sql diff --git a/controllers/FilesApiController.php b/controllers/FilesApiController.php index 06d06cdc..f0620a61 100644 --- a/controllers/FilesApiController.php +++ b/controllers/FilesApiController.php @@ -2,8 +2,8 @@ namespace Grocy\Controllers; -use Grocy\Controllers\Users\User; use \Grocy\Services\FilesService; +use Slim\Exception\HttpNotFoundException; class FilesApiController extends BaseApiController { @@ -16,14 +16,7 @@ class FilesApiController extends BaseApiController { try { - if (IsValidFileName(base64_decode($args['fileName']))) - { - $fileName = base64_decode($args['fileName']); - } - else - { - throw new \Exception('Invalid filename'); - } + $fileName = $this->checkFileName($args['fileName']); $data = $request->getBody()->getContents(); file_put_contents($this->getFilesService()->GetFilePath($args['group'], $fileName), $data); @@ -40,41 +33,9 @@ class FilesApiController extends BaseApiController { try { - if (IsValidFileName(base64_decode($args['fileName']))) - { - $fileName = base64_decode($args['fileName']); - } - else - { - throw new \Exception('Invalid filename'); - } + $fileName = $this->checkFileName($args['fileName']); - $forceServeAs = null; - if (isset($request->getQueryParams()['force_serve_as']) && !empty($request->getQueryParams()['force_serve_as'])) - { - $forceServeAs = $request->getQueryParams()['force_serve_as']; - } - - if ($forceServeAs == FilesService::FILE_SERVE_TYPE_PICTURE) - { - $bestFitHeight = null; - if (isset($request->getQueryParams()['best_fit_height']) && !empty($request->getQueryParams()['best_fit_height']) && is_numeric($request->getQueryParams()['best_fit_height'])) - { - $bestFitHeight = $request->getQueryParams()['best_fit_height']; - } - - $bestFitWidth = null; - if (isset($request->getQueryParams()['best_fit_width']) && !empty($request->getQueryParams()['best_fit_width']) && is_numeric($request->getQueryParams()['best_fit_width'])) - { - $bestFitWidth = $request->getQueryParams()['best_fit_width']; - } - - $filePath = $this->getFilesService()->DownscaleImage($args['group'], $fileName, $bestFitHeight, $bestFitWidth); - } - else - { - $filePath = $this->getFilesService()->GetFilePath($args['group'], $fileName); - } + $filePath = $this->getFilePath($args['group'], $fileName, $request->getQueryParams()); if (file_exists($filePath)) { @@ -85,12 +46,39 @@ class FilesApiController extends BaseApiController } else { - return $this->GenericErrorResponse($response, 'File not found', 404); + throw new HttpNotFoundException($request, 'File not found'); } } catch (\Exception $ex) { - return $this->GenericErrorResponse($response, $ex->getMessage()); + throw new HttpNotFoundException($request, $ex->getMessage(), $ex); + } + } + + public function ShowFile(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) + { + try + { + $fileInfo = explode('_', $args['fileName']); + $fileName = $this->checkFileName($fileInfo[1]); + + $filePath = $this->getFilePath($args['group'], base64_decode($fileInfo[0]), $request->getQueryParams()); + + if (file_exists($filePath)) + { + $response->write(file_get_contents($filePath)); + $response = $response->withHeader('Cache-Control', 'max-age=2592000'); + $response = $response->withHeader('Content-Type', mime_content_type($filePath)); + return $response->withHeader('Content-Disposition', 'inline; filename="' . $fileName . '"'); + } + else + { + throw new HttpNotFoundException($request, 'File not found'); + } + } + catch (\Exception $ex) + { + throw new HttpNotFoundException($request, $ex->getMessage(), $ex); } } @@ -120,4 +108,50 @@ class FilesApiController extends BaseApiController return $this->GenericErrorResponse($response, $ex->getMessage()); } } + + /** + * @param string $group The group the requested files belongs to. + * @param string $fileName The name of the requested file. + * @param array $queryParams Parameter, e.g. for scaling. Optional. + * @return string + */ + protected function getFilePath(string $group, string $fileName, array $queryParams = []) + { + $forceServeAs = null; + if (isset($queryParams['force_serve_as']) && !empty($queryParams['force_serve_as'])) { + $forceServeAs = $queryParams['force_serve_as']; + } + + if ($forceServeAs == FilesService::FILE_SERVE_TYPE_PICTURE) { + $bestFitHeight = null; + if (isset($queryParams['best_fit_height']) && !empty($queryParams['best_fit_height']) && is_numeric($queryParams['best_fit_height'])) { + $bestFitHeight = $queryParams['best_fit_height']; + } + + $bestFitWidth = null; + if (isset($queryParams['best_fit_width']) && !empty($queryParams['best_fit_width']) && is_numeric($queryParams['best_fit_width'])) { + $bestFitWidth = $queryParams['best_fit_width']; + } + + $filePath = $this->getFilesService()->DownscaleImage($group, $fileName, $bestFitHeight, $bestFitWidth); + } else { + $filePath = $this->getFilesService()->GetFilePath($group, $fileName); + } + return $filePath; + } + + /** + * @param string $fileName base64-encoded file-name + * @return false|string the decoded file-name + * @throws \Exception if the file-name is invalid. + */ + protected function checkFileName(string $fileName) + { + if (IsValidFileName(base64_decode($fileName))) { + $fileName = base64_decode($fileName); + } else { + throw new \Exception('Invalid filename'); + } + return $fileName; + } } diff --git a/localization/userfield_types.pot b/localization/userfield_types.pot index 59c3e04a..97ed306e 100644 --- a/localization/userfield_types.pot +++ b/localization/userfield_types.pot @@ -41,3 +41,9 @@ msgstr "" msgid "link" msgstr "" + +msgid "file" +msgstr "" + +msgid "image" +msgstr "" diff --git a/migrations/0112.sql b/migrations/0112.sql new file mode 100644 index 00000000..71ff4ae5 --- /dev/null +++ b/migrations/0112.sql @@ -0,0 +1,16 @@ +DELETE FROM userfield_values +WHERE IFNULL(value, '') = ''; + +CREATE TRIGGER prevent_empty_userfields_INS AFTER INSERT ON userfield_values +BEGIN + DELETE FROM userfield_values + WHERE id = NEW.id + AND IFNULL(value, '') = ''; +END; + +CREATE TRIGGER prevent_empty_userfields_UPD2 AFTER UPDATE ON userfield_values +BEGIN + DELETE FROM userfield_values + WHERE id = NEW.id + AND IFNULL(value, '') = ''; +END; diff --git a/public/js/extensions.js b/public/js/extensions.js index 87adf511..53baefe2 100644 --- a/public/js/extensions.js +++ b/public/js/extensions.js @@ -172,3 +172,6 @@ function animateCSS(selector, animationName, callback, speed = "faster") nodes.on('animationend', handleAnimationEnd); } +function RandomString() { + return Math.random().toString(36).substring(2, 100) + Math.random().toString(36).substring(2, 100); +} diff --git a/public/viewjs/components/userfieldsform.js b/public/viewjs/components/userfieldsform.js index abc8fabb..00d6c14c 100644 --- a/public/viewjs/components/userfieldsform.js +++ b/public/viewjs/components/userfieldsform.js @@ -28,6 +28,36 @@ Grocy.Components.UserfieldsForm.Save = function(success, error) jsonData[fieldName] = "1"; } } + else if (input.attr("type") == "file") + { + var old_file = input.data('old-file') + if (old_file) { + Grocy.Api.Delete('files/userfiles/' + old_file, null, null, + function (xhr) { + Grocy.FrontendHelpers.ShowGenericError('Could not delete file', xhr); + }); + jsonData[fieldName] = ""; + } + if (input[0].files.length > 0){ + // Files service requires an extension + var fileName = RandomString() + '.' + input[0].files[0].name.split('.').reverse()[0]; + + jsonData[fieldName] = btoa(fileName) + '_' + btoa(input[0].files[0].name); + Grocy.Api.UploadFile(input[0].files[0], 'userfiles', fileName, + function (result) + { + }, + function (xhr) + { + Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) + } + ); + } + else + { + //jsonData[fieldName] = null; + } + } else if ($(this).hasAttr("multiple")) { jsonData[fieldName] = $(this).val().join(","); @@ -79,6 +109,28 @@ Grocy.Components.UserfieldsForm.Load = function() input.val(value.split(",")); $(".selectpicker").selectpicker("render"); } + if (input.attr('type') == "file") + { + if (value != null && !value.isEmpty()) { + var file_name = atob(value.split('_')[1]); + var file_src = value.split('_')[0]; + input.hide(); + var file_info = input.siblings('.userfield-file'); + file_info.removeClass('d-none'); + file_info.find('a.userfield-current-file') + .attr('href', U('/files/userfiles/' + value)) + .text(file_name); + file_info.find('img.userfield-current-file') + .attr('src', U('/files/userfiles/' + value + '?force_serve_as=picture&best_fit_width=250&best_fit_height=250')) + file_info.find('button.userfield-file-delete').click( + function () { + file_info.addClass('d-none'); + input.data('old-file', file_src); + input.show(); + } + ); + } + } else { input.val(value); diff --git a/public/viewjs/equipmentform.js b/public/viewjs/equipmentform.js index 54f449bd..bfe7a765 100644 --- a/public/viewjs/equipmentform.js +++ b/public/viewjs/equipmentform.js @@ -7,7 +7,7 @@ if ($("#instruction-manual")[0].files.length > 0) { - var someRandomStuff = Math.random().toString(36).substring(2, 100) + Math.random().toString(36).substring(2, 100); + var someRandomStuff = RandomString(); jsonData.instruction_manual_file_name = someRandomStuff + $("#instruction-manual")[0].files[0].name; } diff --git a/routes.php b/routes.php index d7272a09..63b01aa9 100644 --- a/routes.php +++ b/routes.php @@ -34,7 +34,9 @@ $app->group('', function(RouteCollectorProxy $group) $group->get('/user/{userId}/permissions', '\Grocy\Controllers\UsersController:PermissionList'); $group->get('/usersettings', '\Grocy\Controllers\UsersController:UserSettings'); - // Stock routes + $group->get('/files/{group}/{fileName}', '\Grocy\Controllers\FilesApiController:ShowFile'); + + // Stock routes if (GROCY_FEATURE_FLAG_STOCK) { $group->get('/stockoverview', '\Grocy\Controllers\StockController:Overview'); diff --git a/services/UserfieldsService.php b/services/UserfieldsService.php index 0625ee1d..34b55824 100644 --- a/services/UserfieldsService.php +++ b/services/UserfieldsService.php @@ -14,8 +14,10 @@ class UserfieldsService extends BaseService const USERFIELD_TYPE_PRESET_LIST = 'preset-list'; const USERFIELD_TYPE_PRESET_CHECKLIST = 'preset-checklist'; const USERFIELD_TYPE_LINK = 'link'; + const USERFIELD_TYPE_FILE = 'file'; + const USERFIELD_TYPE_IMAGE = 'image'; - public function __construct() + public function __construct() { parent::__construct(); } diff --git a/views/components/userfields_tbody.blade.php b/views/components/userfields_tbody.blade.php index 11771650..7b2029df 100644 --- a/views/components/userfields_tbody.blade.php +++ b/views/components/userfields_tbody.blade.php @@ -12,6 +12,13 @@ {!! str_replace(',', '
', $userfieldObject->value) !!} @elseif($userfield->type == \Grocy\Services\UserfieldsService::USERFIELD_TYPE_LINK) {{ $userfieldObject->value }} + @elseif($userfield->type == \Grocy\Services\UserfieldsService::USERFIELD_TYPE_FILE) + {{ base64_decode(explode('_', $userfieldObject->value)[1]) }} + @elseif($userfield->type == \Grocy\Services\UserfieldsService::USERFIELD_TYPE_IMAGE) + + {{ base64_decode(explode('_', $userfieldObject->value)[1]) }} + @else {{ $userfieldObject->value }} @endif diff --git a/views/components/userfieldsform.blade.php b/views/components/userfieldsform.blade.php index 225d4826..c35e12b6 100644 --- a/views/components/userfieldsform.blade.php +++ b/views/components/userfieldsform.blade.php @@ -98,6 +98,28 @@ + @elseif($userfield->type == \Grocy\Services\UserfieldsService::USERFIELD_TYPE_FILE) +
+ + +
+ + +
+
+ @elseif($userfield->type == \Grocy\Services\UserfieldsService::USERFIELD_TYPE_IMAGE) +
+ + +
+ {{ $userfield->name }} + +
+
@endif @endforeach