From fab8344da72acbcef9f57c93405b7ef07004163b Mon Sep 17 00:00:00 2001
From: fipwmaqzufheoxq92ebc
<29818044+fipwmaqzufheoxq92ebc@users.noreply.github.com>
Date: Sat, 29 Aug 2020 14:25:26 +0200
Subject: [PATCH] Add user-field-type "file"
---
controllers/FilesApiController.php | 124 +++++++++++++-------
localization/userfield_types.pot | 3 +
public/js/extensions.js | 3 +
public/viewjs/components/userfieldsform.js | 51 ++++++++
public/viewjs/equipmentform.js | 2 +-
routes.php | 4 +-
services/UserfieldsService.php | 1 +
views/components/userfields_tbody.blade.php | 2 +
views/components/userfieldsform.blade.php | 11 ++
9 files changed, 154 insertions(+), 47 deletions(-)
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..71db4a1d 100644
--- a/localization/userfield_types.pot
+++ b/localization/userfield_types.pot
@@ -41,3 +41,6 @@ msgstr ""
msgid "link"
msgstr ""
+
+msgid "file"
+msgstr ""
diff --git a/public/js/extensions.js b/public/js/extensions.js
index a3197eed..68c7e2b7 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 4be4febc..d1305112 100644
--- a/public/viewjs/components/userfieldsform.js
+++ b/public/viewjs/components/userfieldsform.js
@@ -28,6 +28,37 @@ 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.EndUiBusy("equipment-form");
+ 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 +110,26 @@ 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('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 086b8b2f..8de78768 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 6b12055e..de52e737 100644
--- a/routes.php
+++ b/routes.php
@@ -35,7 +35,9 @@ $app->group('', function(RouteCollectorProxy $group)
$group->get('/user/{userId}', '\Grocy\Controllers\UsersController:UserEditForm');
$group->get('/user/{userId}/permissions', '\Grocy\Controllers\UsersController:PermissionList');
- // 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..4f69c6c7 100644
--- a/services/UserfieldsService.php
+++ b/services/UserfieldsService.php
@@ -14,6 +14,7 @@ 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';
public function __construct()
{
diff --git a/views/components/userfields_tbody.blade.php b/views/components/userfields_tbody.blade.php
index 11771650..0a86e865 100644
--- a/views/components/userfields_tbody.blade.php
+++ b/views/components/userfields_tbody.blade.php
@@ -12,6 +12,8 @@
{!! 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]) }}
@else
{{ $userfieldObject->value }}
@endif
diff --git a/views/components/userfieldsform.blade.php b/views/components/userfieldsform.blade.php
index 225d4826..5ede4ecc 100644
--- a/views/components/userfieldsform.blade.php
+++ b/views/components/userfieldsform.blade.php
@@ -98,6 +98,17 @@
+ @elseif($userfield->type == \Grocy\Services\UserfieldsService::USERFIELD_TYPE_FILE)
+