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)
+
+
+
@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)
+