diff --git a/changelog/60_UNRELEASED_2020-xx-xx.md b/changelog/60_UNRELEASED_2020-xx-xx.md index 5583d016..0dc984a8 100644 --- a/changelog/60_UNRELEASED_2020-xx-xx.md +++ b/changelog/60_UNRELEASED_2020-xx-xx.md @@ -30,6 +30,7 @@ - Added a "Clear filter"-button on the stock overview page to quickly reset applied filters - It's now tracked who made a stock change (currently logged in user, visible on the stock journal page) (thanks @fipwmaqzufheoxq92ebc) - Product edit page improvements ("Save & continue" button, deleting and adding a product picuture is now possible in one go) (thanks @Ma27) +- For products with tare weight handling enabled, it's now optionally possible to consume a fixed/exact amount (just like for "normal" products) in case you don't want to weigh the whole container this time (new checkbox on the consume page) (thanks @fipwmaqzufheoxq92ebc) - Fixed that it was not possible to leave the "Barcode(s)" on the product edit page by `TAB` - Fixed that when adding products through a product picker workflow and when the created products contains special characters, the product was not preselected on the previous page (thanks @Forceu) - Fixed that when editing a product the default store was not visible / always empty regardless if the product had one set (thanks @kriddles) @@ -47,6 +48,7 @@ - Decimal amounts are now allowed (for any product, rounded by two decimal places) - "Add products that are below defined min. stock amount" always rounded up the missing amount to an integral number, this now allows decimal numbers - Added a button to add all currently in-stock but expired products to the shopping list (thanks @m-byte) +- Improved that when `FEATURE_FLAG_STOCK` is disabled, all product/stock related inputs and buttons are now hidden on the shopping list page (thanks @fipwmaqzufheoxq92ebc) ### Recipe improvements/fixes - It's now possible to print recipes (button next to the recipe title) (thanks @zsarnett) @@ -58,12 +60,16 @@ - Fixed that when editing a recipe ingredient the checkbox "Disable stock fulfillment checking for this ingredient" was not initaliased with the saved value - Fixed that when using the "+"-symbol (which appears on small screens only to expand columns which don't fit the screen) the page reloaded - Fixed that the status filter ("Enough in stock", etc.) on the recipes page did not filter recipes on the gallery tab (thanks @fipwmaqzufheoxq92ebc) +- Fixed that consuming a recipe ingredient with tare weight handling enabled consumed a wrong amount (thanks @fipwmaqzufheoxq92ebc) ### Chores improvements/fixes - Changed that not assigned chores on the chores overview page display now just a dash instead of an ellipsis in the "Assigned to" column to make this more clear (thanks @Germs2004) - Fixed (again) that weekly chores, where the next execution should be in the same week, were scheduled (not) always (but sometimes) for the next week only (thanks @shadow7412) - Fixed that the assignment type "In alphabetic order" did not work correctly (the last person in the list was always assigned next once reached) (thanks @fipwmaqzufheoxq92ebc) +### Equipment improvements +- The equipment page now will be never automatically reloaded, even when `Auto reload on external changes` is on and a change was detected (because you most probably have that page open longer to read the manual) (thanks @fipwmaqzufheoxq92ebc) + ### Calendar improvements/fixes - Events are now links to the corresponding page (thanks @zsarnett) - Fixed a PHP warning when using the "Share/Integrate calendar (iCal)" button (thanks @tsia) @@ -109,6 +115,7 @@ - Fixed that the endpoint `/objects/{entity}/{objectId}` always returned successfully, even when the given object not exists (now returns `404` when the object is not found) (thanks @fipwmaqzufheoxq92ebc) - Fixed that the endpoint `/stock/volatile` didn't include products which expire today (thanks @fipwmaqzufheoxq92ebc) - Fixed that the endpoint `/objects/{entity}` did not include Userfields for Userentities (so the effective endpoint `/objects/userobjects`) +- Fixed that the endpoint `/stock/consume` returned the response code `200` and an empty response body when `stock_entry_id` was set (consuming a specific stock entry) but invalid (now returns the response code `400`) (thanks @fipwmaqzufheoxq92ebc) - Endpoint `/calendar/ical`: Fixed that "Track date only"-chores were always set to happen at 12am (are treated as all-day events now) - Fixed (again) that CORS was broken @@ -129,6 +136,7 @@ - Replaced (again, added before in v2.7.0, then reverted in v2.7.1 due to some problems) [QuaggaJS](https://github.com/serratus/quaggaJS) (seems to be unmaintained) by [Quagga2](https://github.com/ericblade/quagga2) - More `config.php` settings (see the section `Component configuration for Quagga2`) to tweak Quagga2 (this is the component used for device camera for barcode scanning) (thanks @andrelam) - Some localization string fixes (thanks @duckfullstop) +- Fixed that XSS / HTML injection was possible through some user input fields (low severity / not really a problem as this could not be abused unauthenticated) - New translations: (thanks all the translators) - Greek (demo available at https://el.demo.grocy.info) - Korean (demo available at https://ko.demo.grocy.info) diff --git a/composer.json b/composer.json index 53bd8e34..e907617a 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,8 @@ "gettext/gettext": "^4.8", "eluceo/ical": "^0.16.0", "erusev/parsedown": "^1.7", - "gumlet/php-image-resize": "^1.9" + "gumlet/php-image-resize": "^1.9", + "ezyang/htmlpurifier": "^4.13" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index 51e6bda0..3d7c1f61 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "70c5b65f78f4eb43dac8df8dc144e56c", + "content-hash": "651fcabf083befffe196b08c8f17506b", "packages": [ { "name": "doctrine/inflector", @@ -195,6 +195,56 @@ ], "time": "2019-12-30T22:54:17+00:00" }, + { + "name": "ezyang/htmlpurifier", + "version": "v4.13.0", + "source": { + "type": "git", + "url": "https://github.com/ezyang/htmlpurifier.git", + "reference": "08e27c97e4c6ed02f37c5b2b20488046c8d90d75" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/08e27c97e4c6ed02f37c5b2b20488046c8d90d75", + "reference": "08e27c97e4c6ed02f37c5b2b20488046c8d90d75", + "shasum": "" + }, + "require": { + "php": ">=5.2" + }, + "require-dev": { + "simpletest/simpletest": "dev-master#72de02a7b80c6bb8864ef9bf66d41d2f58f826bd" + }, + "type": "library", + "autoload": { + "psr-0": { + "HTMLPurifier": "library/" + }, + "files": [ + "library/HTMLPurifier.composer.php" + ], + "exclude-from-classmap": [ + "/library/HTMLPurifier/Language/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "Edward Z. Yang", + "email": "admin@htmlpurifier.org", + "homepage": "http://ezyang.com" + } + ], + "description": "Standards compliant HTML filter written in PHP", + "homepage": "http://htmlpurifier.org/", + "keywords": [ + "html" + ], + "time": "2020-06-29T00:56:53+00:00" + }, { "name": "fig/http-message-util", "version": "1.1.4", diff --git a/controllers/BaseApiController.php b/controllers/BaseApiController.php index 74fc49bb..ed3557b0 100644 --- a/controllers/BaseApiController.php +++ b/controllers/BaseApiController.php @@ -115,4 +115,22 @@ class BaseApiController extends BaseController return $this->OpenApiSpec; } + + private static $htmlPurifierInstance = null; + + protected function GetParsedAndFilteredRequestBody($request) + { + if (self::$htmlPurifierInstance == null) + { + self::$htmlPurifierInstance = new \HTMLPurifier(\HTMLPurifier_Config::createDefault()); + } + + $requestBody = $request->getParsedBody(); + foreach ($requestBody as $key => &$value) + { + $value = self::$htmlPurifierInstance->purify($value); + } + + return $requestBody; + } } diff --git a/controllers/BatteriesApiController.php b/controllers/BatteriesApiController.php index 9f06a3ed..163b45b3 100644 --- a/controllers/BatteriesApiController.php +++ b/controllers/BatteriesApiController.php @@ -27,7 +27,7 @@ class BatteriesApiController extends BaseApiController { User::checkPermission($request, User::PERMISSION_BATTERIES_TRACK_CHARGE_CYCLE); - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); try { diff --git a/controllers/ChoresApiController.php b/controllers/ChoresApiController.php index 8220ad16..c70d4d0b 100644 --- a/controllers/ChoresApiController.php +++ b/controllers/ChoresApiController.php @@ -10,7 +10,7 @@ class ChoresApiController extends BaseApiController { try { - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); $choreId = null; @@ -60,7 +60,7 @@ class ChoresApiController extends BaseApiController public function TrackChoreExecution(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); try { diff --git a/controllers/GenericEntityApiController.php b/controllers/GenericEntityApiController.php index 320e537d..4f416b88 100644 --- a/controllers/GenericEntityApiController.php +++ b/controllers/GenericEntityApiController.php @@ -18,7 +18,7 @@ class GenericEntityApiController extends BaseApiController User::checkPermission($request, User::PERMISSION_ADMIN); } - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); try { @@ -78,7 +78,8 @@ class GenericEntityApiController extends BaseApiController { User::checkPermission($request, User::PERMISSION_ADMIN); } - $requestBody = $request->getParsedBody(); + + $requestBody = $this->GetParsedAndFilteredRequestBody($request); try { @@ -202,7 +203,7 @@ class GenericEntityApiController extends BaseApiController { User::checkPermission($request, User::PERMISSION_MASTER_DATA_EDIT); - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); try { diff --git a/controllers/LoginController.php b/controllers/LoginController.php index c80a541e..5e61535e 100644 --- a/controllers/LoginController.php +++ b/controllers/LoginController.php @@ -24,7 +24,7 @@ class LoginController extends BaseController public function ProcessLogin(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { - $postParams = $request->getParsedBody(); + $postParams = $this->GetParsedAndFilteredRequestBody($request); if (isset($postParams['username']) && isset($postParams['password'])) { diff --git a/controllers/RecipesApiController.php b/controllers/RecipesApiController.php index 5e19350b..502bb1b3 100644 --- a/controllers/RecipesApiController.php +++ b/controllers/RecipesApiController.php @@ -10,7 +10,7 @@ class RecipesApiController extends BaseApiController { User::checkPermission($request, User::PERMISSION_SHOPPINGLIST_ITEMS_ADD); - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); $excludedProductIds = null; if ($requestBody !== null && array_key_exists('excludedProductIds', $requestBody)) diff --git a/controllers/StockApiController.php b/controllers/StockApiController.php index 8e54aae2..4e4774a4 100644 --- a/controllers/StockApiController.php +++ b/controllers/StockApiController.php @@ -13,7 +13,7 @@ class StockApiController extends BaseApiController try { - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); $listId = 1; @@ -37,7 +37,7 @@ class StockApiController extends BaseApiController try { - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); $listId = 1; @@ -59,7 +59,7 @@ class StockApiController extends BaseApiController { User::checkPermission($request, User::PERMISSION_STOCK_PURCHASE); - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); try { @@ -143,7 +143,7 @@ class StockApiController extends BaseApiController try { - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); $listId = 1; $amount = 1; @@ -190,7 +190,7 @@ class StockApiController extends BaseApiController try { - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); $listId = 1; @@ -212,7 +212,7 @@ class StockApiController extends BaseApiController { User::checkPermission($request, User::PERMISSION_STOCK_CONSUME); - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); $result = null; @@ -263,7 +263,15 @@ class StockApiController extends BaseApiController $recipeId = $requestBody['recipe_id']; } - $bookingId = $this->getStockService()->ConsumeProduct($args['productId'], $requestBody['amount'], $spoiled, $transactionType, $specificStockEntryId, $recipeId, $locationId); + $consumeExact = false; + + if (array_key_exists('exact_amount', $requestBody)) + { + $consumeExact = $requestBody['exact_amount']; + } + $transactionId = null; + + $bookingId = $this->getStockService()->ConsumeProduct($args['productId'], $requestBody['amount'], $spoiled, $transactionType, $specificStockEntryId, $recipeId, $locationId, $transactionId, false, $consumeExact); return $this->ApiResponse($response, $this->getDatabase()->stock_log($bookingId)); } catch (\Exception $ex) @@ -315,7 +323,7 @@ class StockApiController extends BaseApiController { User::checkPermission($request, User::PERMISSION_STOCK_EDIT); - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); try { @@ -391,7 +399,7 @@ class StockApiController extends BaseApiController { User::checkPermission($request, User::PERMISSION_STOCK_INVENTORY); - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); try { @@ -459,7 +467,7 @@ class StockApiController extends BaseApiController { User::checkPermission($request, User::PERMISSION_STOCK_OPEN); - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); try { @@ -574,7 +582,7 @@ class StockApiController extends BaseApiController try { - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); $listId = 1; $amount = 1; @@ -656,7 +664,7 @@ class StockApiController extends BaseApiController { User::checkPermission($request, User::PERMISSION_STOCK_TRANSFER); - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); try { diff --git a/controllers/SystemApiController.php b/controllers/SystemApiController.php index 380e37d3..debadc9f 100644 --- a/controllers/SystemApiController.php +++ b/controllers/SystemApiController.php @@ -49,7 +49,7 @@ class SystemApiController extends BaseApiController { try { - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); $this->getLocalizationService()->CheckAndAddMissingTranslationToPot($requestBody['text']); return $this->EmptyApiResponse($response); diff --git a/controllers/TasksApiController.php b/controllers/TasksApiController.php index 7518c4a8..eba0f4ee 100644 --- a/controllers/TasksApiController.php +++ b/controllers/TasksApiController.php @@ -15,7 +15,7 @@ class TasksApiController extends BaseApiController { User::checkPermission($request, User::PERMISSION_TASKS_MARK_COMPLETED); - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); try { diff --git a/controllers/UsersApiController.php b/controllers/UsersApiController.php index 94446e76..2e88c27d 100644 --- a/controllers/UsersApiController.php +++ b/controllers/UsersApiController.php @@ -11,7 +11,7 @@ class UsersApiController extends BaseApiController try { User::checkPermission($request, User::PERMISSION_ADMIN); - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); $this->getDatabase()->user_permissions()->createRow([ 'user_id' => $args['userId'], @@ -32,7 +32,7 @@ class UsersApiController extends BaseApiController public function CreateUser(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { User::checkPermission($request, User::PERMISSION_USERS_CREATE); - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); try { @@ -75,7 +75,7 @@ class UsersApiController extends BaseApiController User::checkPermission($request, User::PERMISSION_USERS_EDIT); } - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); try { @@ -152,7 +152,7 @@ class UsersApiController extends BaseApiController try { User::checkPermission($request, User::PERMISSION_ADMIN); - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); $db = $this->getDatabase(); $db->user_permissions() ->where('user_id', $args['userId']) @@ -186,7 +186,7 @@ class UsersApiController extends BaseApiController { try { - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); $value = $this->getUsersService()->SetUserSetting(GROCY_USER_ID, $args['settingKey'], $requestBody['value']); return $this->EmptyApiResponse($response); diff --git a/public/js/extensions.js b/public/js/extensions.js index 9ed9e1f8..df3e80d6 100644 --- a/public/js/extensions.js +++ b/public/js/extensions.js @@ -195,8 +195,3 @@ function getQRCodeForAPIKey(apikey_type, apikey_key) } return getQRCodeForContent(content); } - -function SanitizeHtml(input) -{ - return $("
").text(input).html(); -} diff --git a/public/js/grocy_dbchangedhandling.js b/public/js/grocy_dbchangedhandling.js index 77ee6c0c..b92e7b3e 100644 --- a/public/js/grocy_dbchangedhandling.js +++ b/public/js/grocy_dbchangedhandling.js @@ -15,31 +15,34 @@ // Check if the database has changed once a minute // If a change is detected, reload the current page, but only if already idling for at least 50 seconds, // when there is no unsaved form data and when the user enabled auto reloading -setInterval(function() +if (Grocy.DbChangedHandlingEnabledForPage) { - Grocy.Api.Get('system/db-changed-time', - function(result) - { - var newDbChangedTime = moment(result.changed_time); - if (newDbChangedTime.isAfter(Grocy.DatabaseChangedTime)) + setInterval(function() + { + Grocy.Api.Get('system/db-changed-time', + function(result) { - if (Grocy.IdleTime >= 50) + var newDbChangedTime = moment(result.changed_time); + if (newDbChangedTime.isAfter(Grocy.DatabaseChangedTime)) { - if (BoolVal(Grocy.UserSettings.auto_reload_on_db_change) && $("form.is-dirty").length === 0 && !$("body").hasClass("fullscreen-card")) + if (Grocy.IdleTime >= 50) { - window.location.reload(); + if (BoolVal(Grocy.UserSettings.auto_reload_on_db_change) && $("form.is-dirty").length === 0 && !$("body").hasClass("fullscreen-card")) + { + window.location.reload(); + } } - } - Grocy.DatabaseChangedTime = newDbChangedTime; + Grocy.DatabaseChangedTime = newDbChangedTime; + } + }, + function(xhr) + { + console.error(xhr); } - }, - function(xhr) - { - console.error(xhr); - } - ); -}, 60000); + ); + }, 60000); +} Grocy.IdleTime = 0; Grocy.ResetIdleTime = function() diff --git a/public/viewjs/batteries.js b/public/viewjs/batteries.js index 71e1a216..0829ebe4 100644 --- a/public/viewjs/batteries.js +++ b/public/viewjs/batteries.js @@ -21,7 +21,7 @@ $("#search").on("keyup", Delay(function() $(document).on('click', '.battery-delete-button', function(e) { - var objectName = SanitizeHtml($(e.currentTarget).attr('data-battery-name')); + var objectName = $(e.currentTarget).attr('data-battery-name'); var objectId = $(e.currentTarget).attr('data-battery-id'); bootbox.confirm({ diff --git a/public/viewjs/chores.js b/public/viewjs/chores.js index a120bd6f..7d3ca85a 100644 --- a/public/viewjs/chores.js +++ b/public/viewjs/chores.js @@ -21,7 +21,7 @@ $("#search").on("keyup", Delay(function() $(document).on('click', '.chore-delete-button', function(e) { - var objectName = SanitizeHtml($(e.currentTarget).attr('data-chore-name')); + var objectName = $(e.currentTarget).attr('data-chore-name'); var objectId = $(e.currentTarget).attr('data-chore-id'); bootbox.confirm({ diff --git a/public/viewjs/components/productpicker.js b/public/viewjs/components/productpicker.js index f90623c2..16d4bd5f 100644 --- a/public/viewjs/components/productpicker.js +++ b/public/viewjs/components/productpicker.js @@ -162,7 +162,7 @@ $('#product_id_text_input').on('blur', function(e) Grocy.Components.ProductPicker.PopupOpen = true; bootbox.dialog({ - message: __t('"%s" could not be resolved to a product, how do you want to proceed?', SanitizeHtml(input)), + message: __t('"%s" could not be resolved to a product, how do you want to proceed?', input), title: __t('Create or assign product'), onEscape: function() { diff --git a/public/viewjs/consume.js b/public/viewjs/consume.js index e007dc55..69e772be 100644 --- a/public/viewjs/consume.js +++ b/public/viewjs/consume.js @@ -9,6 +9,7 @@ var jsonData = {}; jsonData.amount = jsonForm.amount; + jsonData.exact_amount = (jsonForm.exact_amount == "on"); jsonData.spoiled = $('#spoiled').is(':checked'); if ($("#use_specific_stock_entry").is(":checked")) @@ -70,7 +71,7 @@ $("#use_specific_stock_entry").click(); } - if (productDetails.product.enable_tare_weight_handling == 1) + if (productDetails.product.enable_tare_weight_handling == 1 && !jsonData.exact_amount) { var successMessage = __t('Removed %1$s of %2$s from stock', Math.abs(jsonForm.amount - (parseFloat(productDetails.product.tare_weight) + parseFloat(productDetails.stock_amount))) + " " + __n(jsonForm.amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural), productDetails.product.name) + '