From f696b1d997f3b834a1cc6afac368bb492446b24c Mon Sep 17 00:00:00 2001 From: evotodi Date: Mon, 16 Feb 2026 12:25:54 -0500 Subject: [PATCH 1/5] WIP Refactor header generation for improved modularity; add transfer barcode handling and logging integration Signed-off-by: evotodi --- .gitignore | 3 + api/index.php | 40 +- barcodes.php | 102 +++ composer.json | 3 +- composer.lock | 1247 +++++++++++++++++++++++++-------- config-dist.php | 7 + incl/api.inc.php | 206 +++++- incl/configProcessing.inc.php | 11 +- incl/curl.inc.php | 17 +- incl/db.inc.php | 53 +- incl/js/JsBarcode.all.min.js | 2 + incl/js/scripts_barcodes.js | 22 + incl/js/scripts_settings.js | 32 + incl/logging.inc.php | 181 +++++ incl/modules/dbUpgrade.php | 32 +- incl/processing.inc.php | 136 +++- incl/webui.inc.php | 92 ++- menu/settings.php | 21 +- openapi.json | 12 +- screen.php | 21 +- wsserver.php | 16 +- 21 files changed, 1878 insertions(+), 378 deletions(-) create mode 100644 barcodes.php create mode 100644 incl/js/JsBarcode.all.min.js create mode 100644 incl/js/scripts_barcodes.js create mode 100644 incl/js/scripts_settings.js create mode 100644 incl/logging.inc.php diff --git a/.gitignore b/.gitignore index 9e8bc371..5b9358c2 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ data/config.php data/users.db /vendor/ .idea +/.devDocker/ +/.run/Dev Docker.run.xml +/data/logs/ diff --git a/api/index.php b/api/index.php index e1e9e486..cb7c8ee7 100644 --- a/api/index.php +++ b/api/index.php @@ -21,6 +21,7 @@ require_once __DIR__ . "/../incl/db.inc.php"; require_once __DIR__ . "/../incl/processing.inc.php"; require_once __DIR__ . "/../incl/config.inc.php"; +require_once __DIR__ . "/../incl/logging.inc.php"; //removes Get parameters $requestedUrl = strtok($_SERVER["REQUEST_URI"], '?'); @@ -37,11 +38,12 @@ $api->checkIfAuthorized(); $api->execute($requestedUrl); - class BBuddyApi { private $routes = array(); + private \Monolog\Logger $logger; + /** * Checks if authorized * @return bool True if authorized, or dies if not @@ -60,11 +62,13 @@ function checkIfAuthorized(): bool { $apiKey = $_GET["apikey"]; if ($apiKey == "") + $procLog->error("Unauthorized API call. No API key provided."); self::sendUnauthorizedAndDie(); if (DatabaseConnection::getInstance()->isValidApiKey($apiKey)) return true; else + $procLog->error("Unauthorized API call. Invalid API key provided."); self::sendUnauthorizedAndDie(); return false; } @@ -77,11 +81,14 @@ static function sendUnauthorizedAndDie(): void { function execute(string $url): void { global $CONFIG; + $this->logger->debug("API call: " . $url); + //Turn off all error reporting, as it could cause problems with parsing json clientside if (!$CONFIG->IS_DEBUG) error_reporting(0); if (!isset($this->routes[$url])) { + $this->logger->warning("API call not found: " . $url); self::sendResult(self::createResultArray(null, "API call not found", 404), 404); } else { $this->routes[$url]->execute(); @@ -90,6 +97,8 @@ function execute(string $url): void { function __construct() { + $this->logger = bb_logger('api'); + $this->logger->info("Barcode Buddy Version " . BB_VERSION_READABLE); $this->initRoutes(); } @@ -115,7 +124,6 @@ function addRoute(ApiRoute $route): void { } private function initRoutes(): void { - $this->addRoute(new ApiRoute("/action/scan", function () { $barcode = ""; if (isset($_GET["text"])) @@ -124,23 +132,29 @@ private function initRoutes(): void { $barcode = $_GET["add"]; if (isset($_POST["barcode"])) $barcode = $_POST["barcode"]; - if ($barcode == "") + if ($barcode == "") { + $this->logger->warning("No barcode supplied"); return self::createResultArray(null, "No barcode supplied", 400); - else { + } else { $bestBefore = null; $price = null; if (isset($_POST["bestBeforeInDays"]) && $_POST["bestBeforeInDays"] != null) { - if (is_numeric($_POST["bestBeforeInDays"])) + if (is_numeric($_POST["bestBeforeInDays"])) { $bestBefore = $_POST["bestBeforeInDays"]; - else + } else { + $this->logger->warning("Invalid parameter bestBeforeInDays: needs to be type int"); return self::createResultArray(null, "Invalid parameter bestBeforeInDays: needs to be type int", 400); + } } if (isset($_POST["price"]) && $_POST["price"] != null) { - if (is_numeric($_POST["price"])) + if (is_numeric($_POST["price"])) { $price = $_POST["price"]; - else + } else { + $this->logger->warning("Invalid parameter price: needs to be type float"); return self::createResultArray(null, "Invalid parameter price: needs to be type float", 400); + } } + $this->logger->debug(sprintf("Scanning barcode: %s with bestBefore: %d days, price: %.2f", $barcode, $bestBefore, $price)); $result = processNewBarcode(sanitizeString($barcode), $bestBefore, $price); return self::createResultArray(array("result" => sanitizeString($result))); } @@ -159,10 +173,11 @@ private function initRoutes(): void { else if (isset($_POST["state"])) $state = $_POST["state"]; - //Also check if value is a valid range (STATE_CONSUME the lowest and STATE_CONSUME_ALL the highest value) - if (!is_numeric($state) || $state < STATE_CONSUME || $state > STATE_CONSUME_ALL) + //Also check if value is a valid range (STATE_CONSUME the lowest and STATE_TXFR the highest value) + if (!is_numeric($state) || $state < STATE_CONSUME || $state > STATE_TXFR) { + $this->logger->warning("Invalid state provided"); return self::createResultArray(null, "Invalid state provided", 400); - else { + } else { DatabaseConnection::getInstance()->setTransactionState(intval($state)); return self::createResultArray(); } @@ -178,7 +193,8 @@ private function initRoutes(): void { "BARCODE_GS" => $config["BARCODE_GS"], "BARCODE_Q" => $config["BARCODE_Q"], "BARCODE_AS" => $config["BARCODE_AS"], - "BARCODE_CA" => $config["BARCODE_CA"] + "BARCODE_CA" => $config["BARCODE_CA"], + "BARCODE_TXFR" => $config["BARCODE_TXFR"], )); })); diff --git a/barcodes.php b/barcodes.php new file mode 100644 index 00000000..64b08c27 --- /dev/null +++ b/barcodes.php @@ -0,0 +1,102 @@ +checkIfAuthenticated(true, true); + +// Get mode and validate it +$mode = MODE_LOCATION; +if (isset($_GET)) { + if (isset($_GET["mode"])) { + $mode = $_GET["mode"]; + } +} +if (!in_array($mode, [MODE_QUANTITY, MODE_LOCATION])) { + die("Invalid mode"); +} + +// Generate the page +$webUi = new WebUiGenerator(MENU_GENERIC); + +$webUi->addBaseHeader( + null, + false, + true, + "\n"); + +switch ($mode) { + case MODE_QUANTITY: + getHtmlQuantityTable($webUi); + break; + case MODE_LOCATION: + getHtmlLocationTable($webUi); + break; +} + +$webUi->printHtml(); + + +function getHtmlLocationTable(WebUiGenerator $webUi): void +{ + $config = BBConfig::getInstance(); + $locations = API::getLocations(); + + // Generate the HTML + $html = new UiEditor(true, null, "barcodes"); + $html->addHtml("
"); + + foreach ($locations as $location) { + $html->addDiv("id\" alt=\"$location->id\"/>", null, "flex-settings-child"); + + } + + $html->addHtml('
'); + $webUi->addHtml($html->getHtml()); + + // Generate the JS + $webUi->addScript("generateLocationBarcodes();"); +} + + +function getHtmlQuantityTable(WebUiGenerator $webUi): void +{ + $config = BBConfig::getInstance(); + + // Get quantity start and end + $startQty = isset($_GET['startQty']) ? intval($_GET['startQty']) : 1; + $endQty = isset($_GET['endQty']) ? intval($_GET['endQty']) : 10; + + // Generate the HTML + $html = new UiEditor(true, null, "barcodes"); + $html->addHtml("
"); + + for ($i = $startQty; $i <= $endQty; $i++) { + $html->addDiv("\"$i\"/", null, "flex-settings-child"); + } + + $html->addHtml('
'); + $webUi->addHtml($html->getHtml()); + + // Generate the JS + $webUi->addScript("generateQuantityBarcodes();"); +} \ No newline at end of file diff --git a/composer.json b/composer.json index c0ef1d41..e5c69f08 100644 --- a/composer.json +++ b/composer.json @@ -6,6 +6,7 @@ "require": { "ext-redis": "*", "ext-sockets": "*", - "ext-curl": "*" + "ext-curl": "*", + "monolog/monolog": "^2.9" } } diff --git a/composer.lock b/composer.lock index 357e0428..ff5316c6 100644 --- a/composer.lock +++ b/composer.lock @@ -1,52 +1,200 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "bf9c4fdd657f9f4f1fed224955837374", - "packages": [], - "packages-dev": [ + "content-hash": "4abcac97636a44b61cad6015750d8348", + "packages": [ { - "name": "amphp/amp", - "version": "v2.5.2", + "name": "monolog/monolog", + "version": "2.11.0", "source": { "type": "git", - "url": "https://github.com/amphp/amp.git", - "reference": "efca2b32a7580087adb8aabbff6be1dc1bb924a9" + "url": "https://github.com/Seldaek/monolog.git", + "reference": "37308608e599f34a1a4845b16440047ec98a172a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/amp/zipball/efca2b32a7580087adb8aabbff6be1dc1bb924a9", - "reference": "efca2b32a7580087adb8aabbff6be1dc1bb924a9", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/37308608e599f34a1a4845b16440047ec98a172a", + "reference": "37308608e599f34a1a4845b16440047ec98a172a", "shasum": "" }, "require": { - "php": ">=7" + "php": ">=7.2", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "1.0.0 || 2.0.0 || 3.0.0" }, "require-dev": { - "amphp/php-cs-fixer-config": "dev-master", - "amphp/phpunit-util": "^1", + "aws/aws-sdk-php": "^2.4.9 || ^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", "ext-json": "*", - "jetbrains/phpstorm-stubs": "^2019.3", - "phpunit/phpunit": "^6.0.9 | ^7", - "psalm/phar": "^3.11@dev", - "react/promise": "^2" + "graylog2/gelf-php": "^1.4.2 || ^2@dev", + "guzzlehttp/guzzle": "^7.4", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8 || ^2.0", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "phpspec/prophecy": "^1.15", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8.5.38 || ^9.6.19", + "predis/predis": "^1.1 || ^2.0", + "rollbar/rollbar": "^1.3 || ^2 || ^3", + "ruflin/elastica": "^7", + "swiftmailer/swiftmailer": "^5.3|^6.0", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.x-dev" + "dev-main": "2.x-dev" } }, "autoload": { "psr-4": { - "Amp\\": "lib" + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/2.11.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2026-01-01T13:05:00+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + } + ], + "packages-dev": [ + { + "name": "amphp/amp", + "version": "v2.6.5", + "source": { + "type": "git", + "url": "https://github.com/amphp/amp.git", + "reference": "d7dda98dae26e56f3f6fcfbf1c1f819c9a993207" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/amp/zipball/d7dda98dae26e56f3f6fcfbf1c1f819c9a993207", + "reference": "d7dda98dae26e56f3f6fcfbf1c1f819c9a993207", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "amphp/phpunit-util": "^1", + "ext-json": "*", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^7 | ^8 | ^9", + "react/promise": "^2", + "vimeo/psalm": "^3.12" + }, + "type": "library", + "autoload": { "files": [ "lib/functions.php", "lib/Internal/functions.php" - ] + ], + "psr-4": { + "Amp\\": "lib" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -71,7 +219,7 @@ } ], "description": "A non-blocking concurrency framework for PHP applications.", - "homepage": "http://amphp.org/amp", + "homepage": "https://amphp.org/amp", "keywords": [ "async", "asynchronous", @@ -83,20 +231,31 @@ "non-blocking", "promise" ], - "time": "2021-01-10T17:06:37+00:00" + "support": { + "irc": "irc://irc.freenode.org/amphp", + "issues": "https://github.com/amphp/amp/issues", + "source": "https://github.com/amphp/amp/tree/v2.6.5" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-09-03T19:41:28+00:00" }, { "name": "amphp/byte-stream", - "version": "v1.8.0", + "version": "v1.8.2", "source": { "type": "git", "url": "https://github.com/amphp/byte-stream.git", - "reference": "f0c20cf598a958ba2aa8c6e5a71c697d652c7088" + "reference": "4f0e968ba3798a423730f567b1b50d3441c16ddc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/byte-stream/zipball/f0c20cf598a958ba2aa8c6e5a71c697d652c7088", - "reference": "f0c20cf598a958ba2aa8c6e5a71c697d652c7088", + "url": "https://api.github.com/repos/amphp/byte-stream/zipball/4f0e968ba3798a423730f567b1b50d3441c16ddc", + "reference": "4f0e968ba3798a423730f567b1b50d3441c16ddc", "shasum": "" }, "require": { @@ -112,18 +271,13 @@ "psalm/phar": "^3.11.4" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, "autoload": { - "psr-4": { - "Amp\\ByteStream\\": "lib" - }, "files": [ "lib/functions.php" - ] + ], + "psr-4": { + "Amp\\ByteStream\\": "lib" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -140,7 +294,7 @@ } ], "description": "A stream abstraction to make working with non-blocking I/O simple.", - "homepage": "http://amphp.org/byte-stream", + "homepage": "https://amphp.org/byte-stream", "keywords": [ "amp", "amphp", @@ -149,20 +303,30 @@ "non-blocking", "stream" ], - "time": "2020-06-29T18:35:05+00:00" + "support": { + "issues": "https://github.com/amphp/byte-stream/issues", + "source": "https://github.com/amphp/byte-stream/tree/v1.8.2" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-04-13T18:00:56+00:00" }, { "name": "composer/package-versions-deprecated", - "version": "1.11.99.1", + "version": "1.11.99.5", "source": { "type": "git", "url": "https://github.com/composer/package-versions-deprecated.git", - "reference": "7413f0b55a051e89485c5cb9f765fe24bb02a7b6" + "reference": "b4f54f74ef3453349c24a845d22392cd31e65f1d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/package-versions-deprecated/zipball/7413f0b55a051e89485c5cb9f765fe24bb02a7b6", - "reference": "7413f0b55a051e89485c5cb9f765fe24bb02a7b6", + "url": "https://api.github.com/repos/composer/package-versions-deprecated/zipball/b4f54f74ef3453349c24a845d22392cd31e65f1d", + "reference": "b4f54f74ef3453349c24a845d22392cd31e65f1d", "shasum": "" }, "require": { @@ -204,28 +368,117 @@ } ], "description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)", - "time": "2020-11-11T10:22:58+00:00" + "support": { + "issues": "https://github.com/composer/package-versions-deprecated/issues", + "source": "https://github.com/composer/package-versions-deprecated/tree/1.11.99.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2022-01-17T14:14:24+00:00" + }, + { + "name": "composer/pcre", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "67a32d7d6f9f560b726ab25a061b38ff3a80c560" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/67a32d7d6f9f560b726ab25a061b38ff3a80c560", + "reference": "67a32d7d6f9f560b726ab25a061b38ff3a80c560", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.3", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/1.0.1" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2022-01-21T20:24:37+00:00" }, { "name": "composer/semver", - "version": "3.2.4", + "version": "3.4.4", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "a02fdf930a3c1c3ed3a49b5f63859c0c20e10464" + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/a02fdf930a3c1c3ed3a49b5f63859c0c20e10464", - "reference": "a02fdf930a3c1c3ed3a49b5f63859c0c20e10464", + "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", "shasum": "" }, "require": { "php": "^5.3.2 || ^7.0 || ^8.0" }, "require-dev": { - "phpstan/phpstan": "^0.12.54", - "symfony/phpunit-bridge": "^4.2 || ^5" + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" }, "type": "library", "extra": { @@ -266,28 +519,46 @@ "validation", "versioning" ], - "time": "2020-11-13T08:59:24+00:00" + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.4" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2025-08-20T19:15:30+00:00" }, { "name": "composer/xdebug-handler", - "version": "1.4.5", + "version": "2.0.5", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "f28d44c286812c714741478d968104c5e604a1d4" + "reference": "9e36aeed4616366d2b690bdce11f71e9178c579a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/f28d44c286812c714741478d968104c5e604a1d4", - "reference": "f28d44c286812c714741478d968104c5e604a1d4", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/9e36aeed4616366d2b690bdce11f71e9178c579a", + "reference": "9e36aeed4616366d2b690bdce11f71e9178c579a", "shasum": "" }, "require": { + "composer/pcre": "^1", "php": "^5.3.2 || ^7.0 || ^8.0", - "psr/log": "^1.0" + "psr/log": "^1 || ^2 || ^3" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 8" + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^4.2 || ^5.0 || ^6.0" }, "type": "library", "autoload": { @@ -310,7 +581,26 @@ "Xdebug", "performance" ], - "time": "2020-11-13T08:04:11+00:00" + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/2.0.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2022-02-24T20:20:32+00:00" }, { "name": "dnoegel/php-xdg-base-dir", @@ -343,24 +633,76 @@ "MIT" ], "description": "implementation of xdg base directory specification for php", + "support": { + "issues": "https://github.com/dnoegel/php-xdg-base-dir/issues", + "source": "https://github.com/dnoegel/php-xdg-base-dir/tree/v0.1.1" + }, "time": "2019-12-04T15:06:13+00:00" }, + { + "name": "doctrine/deprecations", + "version": "1.1.6", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=14" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12 || ^14", + "phpstan/phpstan": "1.4.10 || 2.1.30", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.6" + }, + "time": "2026-02-07T07:09:04+00:00" + }, { "name": "felixfbecker/advanced-json-rpc", - "version": "v3.2.0", + "version": "v3.2.1", "source": { "type": "git", "url": "https://github.com/felixfbecker/php-advanced-json-rpc.git", - "reference": "06f0b06043c7438959dbdeed8bb3f699a19be22e" + "reference": "b5f37dbff9a8ad360ca341f3240dc1c168b45447" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/felixfbecker/php-advanced-json-rpc/zipball/06f0b06043c7438959dbdeed8bb3f699a19be22e", - "reference": "06f0b06043c7438959dbdeed8bb3f699a19be22e", + "url": "https://api.github.com/repos/felixfbecker/php-advanced-json-rpc/zipball/b5f37dbff9a8ad360ca341f3240dc1c168b45447", + "reference": "b5f37dbff9a8ad360ca341f3240dc1c168b45447", "shasum": "" }, "require": { - "netresearch/jsonmapper": "^1.0 || ^2.0", + "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", "php": "^7.1 || ^8.0", "phpdocumentor/reflection-docblock": "^4.3.4 || ^5.0.0" }, @@ -384,20 +726,24 @@ } ], "description": "A more advanced JSONRPC implementation", - "time": "2021-01-10T17:48:47+00:00" + "support": { + "issues": "https://github.com/felixfbecker/php-advanced-json-rpc/issues", + "source": "https://github.com/felixfbecker/php-advanced-json-rpc/tree/v3.2.1" + }, + "time": "2021-06-11T22:34:44+00:00" }, { "name": "felixfbecker/language-server-protocol", - "version": "v1.5.0", + "version": "v1.5.3", "source": { "type": "git", "url": "https://github.com/felixfbecker/php-language-server-protocol.git", - "reference": "85e83cacd2ed573238678c6875f8f0d7ec699541" + "reference": "a9e113dbc7d849e35b8776da39edaf4313b7b6c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/85e83cacd2ed573238678c6875f8f0d7ec699541", - "reference": "85e83cacd2ed573238678c6875f8f0d7ec699541", + "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/a9e113dbc7d849e35b8776da39edaf4313b7b6c9", + "reference": "a9e113dbc7d849e35b8776da39edaf4313b7b6c9", "shasum": "" }, "require": { @@ -436,20 +782,24 @@ "php", "server" ], - "time": "2020-10-23T13:55:30+00:00" + "support": { + "issues": "https://github.com/felixfbecker/php-language-server-protocol/issues", + "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/v1.5.3" + }, + "time": "2024-04-30T00:40:11+00:00" }, { "name": "netresearch/jsonmapper", - "version": "v2.1.0", + "version": "v4.5.0", "source": { "type": "git", "url": "https://github.com/cweiske/jsonmapper.git", - "reference": "e0f1e33a71587aca81be5cffbb9746510e1fe04e" + "reference": "8e76efb98ee8b6afc54687045e1b8dba55ac76e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/e0f1e33a71587aca81be5cffbb9746510e1fe04e", - "reference": "e0f1e33a71587aca81be5cffbb9746510e1fe04e", + "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/8e76efb98ee8b6afc54687045e1b8dba55ac76e5", + "reference": "8e76efb98ee8b6afc54687045e1b8dba55ac76e5", "shasum": "" }, "require": { @@ -457,10 +807,10 @@ "ext-pcre": "*", "ext-reflection": "*", "ext-spl": "*", - "php": ">=5.6" + "php": ">=7.1" }, "require-dev": { - "phpunit/phpunit": "~4.8.35 || ~5.7 || ~6.4 || ~7.0", + "phpunit/phpunit": "~7.5 || ~8.0 || ~9.0 || ~10.0", "squizlabs/php_codesniffer": "~3.5" }, "type": "library", @@ -482,7 +832,12 @@ } ], "description": "Map nested JSON structures onto PHP classes", - "time": "2020-04-16T18:48:43+00:00" + "support": { + "email": "cweiske@cweiske.de", + "issues": "https://github.com/cweiske/jsonmapper/issues", + "source": "https://github.com/cweiske/jsonmapper/tree/v4.5.0" + }, + "time": "2024-09-08T10:13:13+00:00" }, { "name": "nikic/php-parser", @@ -534,6 +889,10 @@ "parser", "php" ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v4.12.0" + }, "time": "2021-07-21T10:44:31+00:00" }, { @@ -583,6 +942,10 @@ "xml", "xml conversion" ], + "support": { + "issues": "https://github.com/nullivex/lib-array2xml/issues", + "source": "https://github.com/nullivex/lib-array2xml/tree/master" + }, "time": "2019-03-29T20:06:56+00:00" }, { @@ -632,31 +995,43 @@ "reflection", "static analysis" ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, "time": "2020-06-27T09:03:43+00:00" }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.2.2", + "version": "5.6.6", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556" + "reference": "5cee1d3dfc2d2aa6599834520911d246f656bcb8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/069a785b2141f5bcf49f3e353548dc1cce6df556", - "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/5cee1d3dfc2d2aa6599834520911d246f656bcb8", + "reference": "5cee1d3dfc2d2aa6599834520911d246f656bcb8", "shasum": "" }, "require": { + "doctrine/deprecations": "^1.1", "ext-filter": "*", - "php": "^7.2 || ^8.0", + "php": "^7.4 || ^8.0", "phpdocumentor/reflection-common": "^2.2", - "phpdocumentor/type-resolver": "^1.3", - "webmozart/assert": "^1.9.1" + "phpdocumentor/type-resolver": "^1.7", + "phpstan/phpdoc-parser": "^1.7|^2.0", + "webmozart/assert": "^1.9.1 || ^2" }, "require-dev": { - "mockery/mockery": "~1.3.2" + "mockery/mockery": "~1.3.5 || ~1.6.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^5.26" }, "type": "library", "extra": { @@ -680,32 +1055,45 @@ }, { "name": "Jaap van Otterdijk", - "email": "account@ijaap.nl" + "email": "opensource@ijaap.nl" } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2020-09-03T19:13:55+00:00" + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.6" + }, + "time": "2025-12-22T21:13:58+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "1.4.0", + "version": "1.12.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0" + "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", - "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/92a98ada2b93d9b201a613cb5a33584dde25f195", + "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0", - "phpdocumentor/reflection-common": "^2.0" + "doctrine/deprecations": "^1.0", + "php": "^7.3 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^1.18|^2.0" }, "require-dev": { - "ext-tokenizer": "*" + "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^9.5", + "rector/rector": "^0.13.9", + "vimeo/psalm": "^4.25" }, "type": "library", "extra": { @@ -729,83 +1117,80 @@ } ], "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", - "time": "2020-09-17T18:55:26+00:00" + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.12.0" + }, + "time": "2025-11-21T15:09:14+00:00" }, { - "name": "psr/container", - "version": "1.0.0", + "name": "phpstan/phpdoc-parser", + "version": "2.3.2", "source": { "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", - "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a004701b11273a26cd7955a61d67a7f1e525a45a", + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": "^7.4 || ^8.0" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" }, + "type": "library", "autoload": { "psr-4": { - "Psr\\Container\\": "src/" + "PHPStan\\PhpDocParser\\": [ + "src/" + ] } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", - "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" - ], - "time": "2017-02-14T16:28:37+00:00" + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.2" + }, + "time": "2026-01-25T14:56:51+00:00" }, { - "name": "psr/log", - "version": "1.1.3", + "name": "psr/container", + "version": "1.1.2", "source": { "type": "git", - "url": "https://github.com/php-fig/log.git", - "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc" + "url": "https://github.com/php-fig/container.git", + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc", - "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc", + "url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea", + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=7.4.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1.x-dev" - } - }, "autoload": { "psr-4": { - "Psr\\Log\\": "Psr/Log/" + "Psr\\Container\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -815,43 +1200,49 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], - "description": "Common interface for logging libraries", - "homepage": "https://github.com/php-fig/log", + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", "keywords": [ - "log", - "psr", - "psr-3" + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" ], - "time": "2020-03-23T09:12:05+00:00" + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/1.1.2" + }, + "time": "2021-11-05T16:50:12+00:00" }, { "name": "sebastian/diff", - "version": "3.0.3", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211" + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/14f72dd46eaf2f2293cbe79c93cc0bc43161a211", - "reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^7.5 || ^8.0", - "symfony/process": "^2 || ^3.3 || ^4" + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -881,27 +1272,38 @@ "unidiff", "unified diff" ], - "time": "2020-11-30T07:59:04+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:30:58+00:00" }, { "name": "symfony/console", - "version": "v5.2.1", + "version": "v5.3.16", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "47c02526c532fb381374dab26df05e7313978976" + "reference": "2e322c76cdccb302af6b275ea2207169c8355328" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/47c02526c532fb381374dab26df05e7313978976", - "reference": "47c02526c532fb381374dab26df05e7313978976", + "url": "https://api.github.com/repos/symfony/console/zipball/2e322c76cdccb302af6b275ea2207169c8355328", + "reference": "2e322c76cdccb302af6b275ea2207169c8355328", "shasum": "" }, "require": { "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1", "symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-php73": "^1.8", - "symfony/polyfill-php80": "^1.15", + "symfony/polyfill-php80": "^1.16", "symfony/service-contracts": "^1.1|^2", "symfony/string": "^5.1" }, @@ -913,10 +1315,10 @@ "symfony/process": "<4.4" }, "provide": { - "psr/log-implementation": "1.0" + "psr/log-implementation": "1.0|2.0" }, "require-dev": { - "psr/log": "~1.0", + "psr/log": "^1|^2", "symfony/config": "^4.4|^5.0", "symfony/dependency-injection": "^4.4|^5.0", "symfony/event-dispatcher": "^4.4|^5.0", @@ -953,7 +1355,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Console Component", + "description": "Eases the creation of beautiful and testable command line interfaces", "homepage": "https://symfony.com", "keywords": [ "cli", @@ -961,45 +1363,129 @@ "console", "terminal" ], - "time": "2020-12-18T08:03:05+00:00" + "support": { + "source": "https://github.com/symfony/console/tree/v5.3.16" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-03-01T08:24:05+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v2.5.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/605389f2a7e5625f273b53960dc46aeaf9c62918", + "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "2.5-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:11:13+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.22.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "c6c942b1ac76c82448322025e084cadc56048b4e" + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/c6c942b1ac76c82448322025e084cadc56048b4e", - "reference": "c6c942b1ac76c82448322025e084cadc56048b4e", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" }, "suggest": { "ext-ctype": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.22-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1023,45 +1509,63 @@ "polyfill", "portable" ], - "time": "2021-01-07T16:49:33+00:00" + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.22.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "267a9adeb8ecb8071040a740930e077cdfb987af" + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/267a9adeb8ecb8071040a740930e077cdfb987af", - "reference": "267a9adeb8ecb8071040a740930e077cdfb987af", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "suggest": { "ext-intl": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.22-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Intl\\Grapheme\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1087,45 +1591,63 @@ "portable", "shim" ], - "time": "2021-01-07T16:49:33+00:00" + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-27T09:58:17+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.22.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "6e971c891537eb617a00bb07a43d182a6915faba" + "reference": "3833d7255cc303546435cb650316bff708a1c75c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/6e971c891537eb617a00bb07a43d182a6915faba", - "reference": "6e971c891537eb617a00bb07a43d182a6915faba", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "suggest": { "ext-intl": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.22-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Intl\\Normalizer\\": "" - }, "files": [ "bootstrap.php" ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, "classmap": [ "Resources/stubs" ] @@ -1154,45 +1676,67 @@ "portable", "shim" ], - "time": "2021-01-07T17:09:11+00:00" + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.22.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "f377a3dd1fde44d37b9831d68dc8dea3ffd28e13" + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/f377a3dd1fde44d37b9831d68dc8dea3ffd28e13", - "reference": "f377a3dd1fde44d37b9831d68dc8dea3ffd28e13", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", "shasum": "" }, "require": { - "php": ">=7.1" + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" }, "suggest": { "ext-mbstring": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.22-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1217,42 +1761,60 @@ "portable", "shim" ], - "time": "2021-01-07T16:49:33+00:00" + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.22.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2" + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/a678b42e92f86eca04b7fa4c0f6f19d097fb69e2", - "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f68c03565dcaaf25a890667542e8bd75fe7e5bb", + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.22-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php73\\": "" - }, "files": [ "bootstrap.php" ], + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, "classmap": [ "Resources/stubs" ] @@ -1279,42 +1841,60 @@ "portable", "shim" ], - "time": "2021-01-07T16:49:33+00:00" + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.22.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91" + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dc3063ba22c2a1fd2f45ed856374d79114998f91", - "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.22-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" - }, "files": [ "bootstrap.php" ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, "classmap": [ "Resources/stubs" ] @@ -1345,37 +1925,62 @@ "portable", "shim" ], - "time": "2021-01-07T16:49:33+00:00" + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-02T08:10:11+00:00" }, { "name": "symfony/service-contracts", - "version": "v2.2.0", + "version": "v2.5.4", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1" + "reference": "f37b419f7aea2e9abf10abd261832cace12e3300" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d15da7ba4957ffb8f1747218be9e1a121fd298a1", - "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f37b419f7aea2e9abf10abd261832cace12e3300", + "reference": "f37b419f7aea2e9abf10abd261832cace12e3300", "shasum": "" }, "require": { "php": ">=7.2.5", - "psr/container": "^1.0" + "psr/container": "^1.1", + "symfony/deprecation-contracts": "^2.1|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" }, "suggest": { "symfony/service-implementation": "" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "2.2-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "2.5-dev" } }, "autoload": { @@ -1407,20 +2012,37 @@ "interoperability", "standards" ], - "time": "2020-09-07T11:33:47+00:00" + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v2.5.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:11:13+00:00" }, { "name": "symfony/string", - "version": "v5.2.1", + "version": "v5.4.47", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "5bd67751d2e3f7d6f770c9154b8fbcb2aa05f7ed" + "reference": "136ca7d72f72b599f2631aca474a4f8e26719799" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/5bd67751d2e3f7d6f770c9154b8fbcb2aa05f7ed", - "reference": "5bd67751d2e3f7d6f770c9154b8fbcb2aa05f7ed", + "url": "https://api.github.com/repos/symfony/string/zipball/136ca7d72f72b599f2631aca474a4f8e26719799", + "reference": "136ca7d72f72b599f2631aca474a4f8e26719799", "shasum": "" }, "require": { @@ -1431,20 +2053,23 @@ "symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-php80": "~1.15" }, + "conflict": { + "symfony/translation-contracts": ">=3.0" + }, "require-dev": { - "symfony/error-handler": "^4.4|^5.0", - "symfony/http-client": "^4.4|^5.0", + "symfony/error-handler": "^4.4|^5.0|^6.0", + "symfony/http-client": "^4.4|^5.0|^6.0", "symfony/translation-contracts": "^1.1|^2", - "symfony/var-exporter": "^4.4|^5.0" + "symfony/var-exporter": "^4.4|^5.0|^6.0" }, "type": "library", "autoload": { - "psr-4": { - "Symfony\\Component\\String\\": "" - }, "files": [ "Resources/functions.php" ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, "exclude-from-classmap": [ "/Tests/" ] @@ -1463,7 +2088,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony String component", + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", "homepage": "https://symfony.com", "keywords": [ "grapheme", @@ -1473,7 +2098,24 @@ "utf-8", "utf8" ], - "time": "2020-12-05T07:33:16+00:00" + "support": { + "source": "https://github.com/symfony/string/tree/v5.4.47" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-10T20:33:58+00:00" }, { "name": "vimeo/psalm", @@ -1544,20 +2186,20 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.x-dev", - "dev-3.x": "3.x-dev", + "dev-1.x": "1.x-dev", "dev-2.x": "2.x-dev", - "dev-1.x": "1.x-dev" + "dev-3.x": "3.x-dev", + "dev-master": "4.x-dev" } }, "autoload": { - "psr-4": { - "Psalm\\": "src/Psalm/" - }, "files": [ "src/functions.php", "src/spl_object_id.php" - ] + ], + "psr-4": { + "Psalm\\": "src/Psalm/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1574,34 +2216,43 @@ "inspection", "php" ], + "support": { + "issues": "https://github.com/vimeo/psalm/issues", + "source": "https://github.com/vimeo/psalm/tree/4.9.0" + }, "time": "2021-07-30T21:23:45+00:00" }, { "name": "webmozart/assert", - "version": "1.9.1", + "version": "1.12.1", "source": { "type": "git", "url": "https://github.com/webmozarts/assert.git", - "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389" + "reference": "9be6926d8b485f55b9229203f962b51ed377ba68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389", - "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/9be6926d8b485f55b9229203f962b51ed377ba68", + "reference": "9be6926d8b485f55b9229203f962b51ed377ba68", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0 || ^8.0", - "symfony/polyfill-ctype": "^1.8" - }, - "conflict": { - "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<3.9.1" + "ext-ctype": "*", + "ext-date": "*", + "ext-filter": "*", + "php": "^7.2 || ^8.0" }, - "require-dev": { - "phpunit/phpunit": "^4.8.36 || ^7.5.13" + "suggest": { + "ext-intl": "", + "ext-simplexml": "", + "ext-spl": "" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, "autoload": { "psr-4": { "Webmozart\\Assert\\": "src/" @@ -1623,7 +2274,11 @@ "check", "validate" ], - "time": "2020-07-08T17:02:28+00:00" + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.12.1" + }, + "time": "2025-10-29T15:56:20+00:00" }, { "name": "webmozart/path-util", @@ -1669,16 +2324,24 @@ } ], "description": "A robust cross-platform utility for normalizing, comparing and modifying file paths.", + "support": { + "issues": "https://github.com/webmozart/path-util/issues", + "source": "https://github.com/webmozart/path-util/tree/2.3.0" + }, + "abandoned": "symfony/filesystem", "time": "2015-12-17T08:42:14+00:00" } ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { - "ext-redis": "*" + "ext-redis": "*", + "ext-sockets": "*", + "ext-curl": "*" }, - "platform-dev": [] + "platform-dev": {}, + "plugin-api-version": "2.9.0" } diff --git a/config-dist.php b/config-dist.php index a766b531..6e0c8d3c 100644 --- a/config-dist.php +++ b/config-dist.php @@ -49,6 +49,12 @@ //Enable debug output const IS_DEBUG = false; +// Monolog log level (debug, info, notice, warning, error, critical, alert, emergency) +const LOG_LEVEL = "info"; + +// If true, all channels log into data/logs/bbuddy.log. If false, each channel logs into its own file. +const LOG_COMBINED = true; + //Enable to hide the Grocy link in the header const HIDE_LINK_GROCY = false; @@ -92,6 +98,7 @@ //"BARCODE_GS" => "BBUDDY-I", //"BARCODE_Q" => "BBUDDY-Q-", //"BARCODE_AS" => "BBUDDY-AS", + //"BARCODE_TXFR" => "BBUDDY-TXFR-", //"REVERT_TIME" => "10", //"REVERT_SINGLE" => "1", //"MORE_VERBOSE" => "1", diff --git a/incl/api.inc.php b/incl/api.inc.php index 45b1c56c..3b49acaa 100755 --- a/incl/api.inc.php +++ b/incl/api.inc.php @@ -22,13 +22,16 @@ require_once __DIR__ . "/config.inc.php"; require_once __DIR__ . "/redis.inc.php"; require_once __DIR__ . "/curl.inc.php"; +require_once __DIR__ . "/logging.inc.php"; const API_O_BARCODES = 'objects/product_barcodes'; const API_O_PRODUCTS = 'objects/products'; const API_STOCK_PRODUCTS = 'stock/products'; const API_ALL_PRODUCTS = 'stock'; const API_SHOPPINGLIST = 'stock/shoppinglist/'; +const API_OBJECTS = 'objects'; const API_CHORES = 'objects/chores'; +const API_LOCATIONS = 'objects/locations'; const API_STOCK = 'stock/products'; const API_STOCK_BY_BARCODE = 'stock/products/by-barcode/'; const API_CHORE_EXECUTE = 'chores/'; @@ -42,18 +45,20 @@ const DISPLAY_DEBUG = false; +$logApi = bb_logger('api'); class GrocyProduct { - public $id; - public $name; - public $barcodes = null; - public $unit = null; - public $stockAmount = "0"; - public $isTare; - public $tareWeight; - public $quFactor; + public int $id; + public ?string $name; + public ?array $barcodes = null; + public ?string $unit = null; + public int $stockAmount = 0; + public bool $isTare = false; + public ?string $tareWeight; + public float $quFactor; public $defaultBestBeforeDays; public $creationDate; + public GrocyLocation $location; public static function parseProductInfoStock(array $infoArray): GrocyProduct { checkIfNumeric($infoArray["product"]["id"]); @@ -69,12 +74,20 @@ public static function parseProductInfoStock(array $infoArray): GrocyProduct { $result->barcodes = $infoArray["product_barcodes"]; if (isset($infoArray["product"]["qu_conversion_factor_purchase_to_stock"])) - $result->quFactor = sanitizeString($infoArray["product"]["qu_conversion_factor_purchase_to_stock"]); + $result->quFactor = floatval(sanitizeString($infoArray["product"]["qu_conversion_factor_purchase_to_stock"])); else - $result->quFactor = 1; + $result->quFactor = 1.0; if (sanitizeString($infoArray["stock_amount"]) != null) - $result->stockAmount = sanitizeString($infoArray["stock_amount"]); + $result->stockAmount = intval(sanitizeString($infoArray["stock_amount"])); + + $result->location = new GrocyLocation(); + if (isset($infoArray['location'])) { + $result->location = GrocyLocation::parseLocationObject($infoArray['location']); + } elseif (isset($infoArray['location_id'])) { + $result->location->id = $infoArray['location_id']; + } + return $result; } @@ -90,6 +103,83 @@ public static function parseProductInfoObjects(array $infoArray): GrocyProduct { $result->quFactor = 1; //FIXME qu_conversion_factor_purchase_to_stock was removed, might break QU conversion $result->defaultBestBeforeDays = $infoArray["default_best_before_days"]; $result->creationDate = $infoArray["row_created_timestamp"]; + + $result->location = new GrocyLocation(); + if (isset($infoArray['location'])) { + $result->location = GrocyLocation::parseLocationObject($infoArray['location']); + } elseif (isset($infoArray['location_id'])) { + $result->location->id = $infoArray['location_id']; + } + + return $result; + } +} + +class GrocyLocation { + public ?int $id = null; + public string $name = ''; + public ?string $description = null; + public bool $isFreezer = false; + + public function toString(): string + { + return empty($this->name) ? $this->id : $this->name; + } + + public static function parseLocationObject(array $locationArray): GrocyLocation { + $result = new GrocyLocation(); + + // If Grocy returns no location (empty array), just return defaults + if (empty($locationArray)) { + return $result; + } + + if (array_key_exists('id', $locationArray) && $locationArray['id'] !== null && $locationArray['id'] !== '') { + checkIfNumeric((string) $locationArray['id']); + $result->id = (int) $locationArray['id']; + } + + if (array_key_exists('name', $locationArray) && $locationArray['name'] !== null) { + $result->name = sanitizeString((string) $locationArray['name']) ?? ''; + } + + if (array_key_exists('description', $locationArray) && $locationArray['description'] !== null) { + $result->description = sanitizeString((string) $locationArray['description']) ?? ''; + } + + if (array_key_exists('is_freezer', $locationArray)) { + $result->isFreezer = ((string) $locationArray['is_freezer'] === '1'); + } + + return $result; + } + + public static function parseProductLocationObject(array $locationArray): GrocyLocation { + $result = new GrocyLocation(); + + // If Grocy returns no location (empty array), just return defaults + if (empty($locationArray)) { + return $result; + } + + // Grocy may returns an array of locations + if (!array_key_exists('id', $locationArray) && isset($locationArray[0]) && is_array($locationArray[0])) { + $locationArray = $locationArray[0]; + } + + if (array_key_exists('location_id', $locationArray) && $locationArray['location_id'] !== null && $locationArray['location_id'] !== '') { + checkIfNumeric((string) $locationArray['location_id']); + $result->id = (int) $locationArray['location_id']; + } + + if (array_key_exists('location_name', $locationArray) && $locationArray['location_name'] !== null) { + $result->name = sanitizeString((string) $locationArray['location_name']) ?? ''; + } + + if (array_key_exists('location_is_freezer', $locationArray)) { + $result->isFreezer = ((string) $locationArray['location_is_freezer'] === '1'); + } + return $result; } } @@ -97,6 +187,9 @@ public static function parseProductInfoObjects(array $infoArray): GrocyProduct { class ApiInternalErrorException extends Exception { } +class TransferException extends Exception { +} + class API { /** @@ -670,6 +763,95 @@ public static function getProductLocations(int $productid): ?array { } + /** Gets the main location of a product and amount of stock of a product */ + public static function getProductLocationMain(int $productId): GrocyLocation { + + $url = API_STOCK . "/" . $productId . "/locations?include_sub_products=true&query%5B%5D=product_id%3D" . $productId; + + $result = null; + $curl = new CurlGenerator($url); + try { + $result = $curl->execute(true); + } catch (Exception $e) { + return new GrocyLocation(); + } + return GrocyLocation::parseProductLocationObject($result); + } + + /** Get location by location id */ + public static function getLocation(int $locationId): GrocyLocation { + + $url = API_LOCATIONS . "/" . $locationId; + + $result = null; + $curl = new CurlGenerator($url); + try { + $result = $curl->execute(true); + } catch (Exception $e) { + return new GrocyLocation(); + } + return GrocyLocation::parseLocationObject($result); + } + + /** + * Get all locations + * + * @return GrocyLocation[] + */ + public static function getLocations(): array { + + $url = API_OBJECTS . "/locations"; + + $rtn = []; + $result = null; + $curl = new CurlGenerator($url); + try { + $result = $curl->execute(true); + } catch (Exception $e) { + return $rtn; + } + + foreach ($result as $location) { + $rtn[] = GrocyLocation::parseLocationObject($location); + } + return $rtn; + } + + /** + * Transfers a product from one location to another. + * + * @throws TransferException when the product could not be transferred + * + * @param int $productId + * @param int $sourceId + * @param int $destId + * @return void + */ + public static function transferProduct(int $productId, int $sourceId, int $destId, int $amount = 1): void + { + if ($amount <= 0) + return; + + $data = json_encode(array( + 'amount' => $amount, + 'location_id_from' => $sourceId, + 'location_id_to' => $destId + )); + + $url = API_STOCK . "/" . $productId . "/transfer"; + + $curl = new CurlGenerator($url, METHOD_POST, $data); + try { + $curl->execute(); + } catch (Exception $e) { + if ($e instanceof InvalidJsonResponseException && (str_contains($e->getMessage(), "ould not transfer") || str_contains($e->getMessage(), "mount to be transferred cannot"))) { + throw new TransferException($e->getMessage()); + } + + self::processError($e, "Could not transfer product"); + } + } + /** * Getting info of a Grocy chore * @param int $choreId Chore ID. @@ -765,6 +947,8 @@ public static function processError(Exception $e, string $errorMessage): void { case 'ApiInternalErrorException': self::logError("Could not process API call: " . $errorMessage); break; + default: + self::logError("Unknown error: " . $errorMessage); } } diff --git a/incl/configProcessing.inc.php b/incl/configProcessing.inc.php index ee6152ba..88c98611 100644 --- a/incl/configProcessing.inc.php +++ b/incl/configProcessing.inc.php @@ -17,8 +17,8 @@ */ -const BB_VERSION = "1818"; -const BB_VERSION_READABLE = "1.8.1.8"; +const BB_VERSION = "1819"; +const BB_VERSION_READABLE = "1.8.1.9"; const CONFIG_PATH = __DIR__ . '/../data/config.php'; const AUTHDB_PATH = __DIR__ . '/../data/users.db'; @@ -77,7 +77,9 @@ function checkForMissingConstants(): void { "TRUSTED_PROXIES" => array(), "SEARCH_ENGINE" => "https://google.com/search?q=", "BASEURL" => "/", - "DEFAULT_LOOKUP_LANGUAGE" => "en" + "DEFAULT_LOOKUP_LANGUAGE" => "en", + "LOG_LEVEL" => "info", + "LOG_COMBINED" => true, ); foreach ($defaultValues as $key => $value) { if (!defined($key)) @@ -107,6 +109,9 @@ class GlobalConfig { public $SEARCH_ENGINE = SEARCH_ENGINE; public $BASEURL = BASEURL; public $DEFAULT_LOOKUP_LANGUAGE = DEFAULT_LOOKUP_LANGUAGE; + public $LOG_LEVEL = LOG_LEVEL; + public $LOG_COMBINED = LOG_COMBINED; + function __construct() { $this->loadConfig(); diff --git a/incl/curl.inc.php b/incl/curl.inc.php index 4bc8976f..6b0b4d94 100644 --- a/incl/curl.inc.php +++ b/incl/curl.inc.php @@ -16,6 +16,7 @@ * @since File available since Release 1.6 */ +require_once __DIR__ . '/logging.inc.php'; class InvalidServerResponseException extends Exception { } @@ -51,6 +52,7 @@ class CurlGenerator { private $method; private $urlApi; private $ignoredResultCodes = array(400); + private \Monolog\Logger $logger; const IGNORED_API_ERRORS_REGEX = array( '/No product with barcode .+ found/' @@ -69,12 +71,13 @@ class CurlGenerator { * @param array|null $headers * @throws DbConnectionDuringEstablishException */ - function __construct(string $url, string $method = METHOD_GET, + public function __construct(string $url, string $method = METHOD_GET, string $jasonData = null, array $loginOverride = null, bool $noApiCall = false, array $ignoredResultCodes = null, array $formData = null, string $userAgent = null, array $headers = null) { global $CONFIG; + $this->logger = bb_logger('curl'); $config = BBConfig::getInstance(); @@ -146,7 +149,9 @@ function __construct(string $url, string $method = METHOD_GET, * @throws NotFoundException * @throws UnauthorizedException */ - function execute(bool $decode = false) { + public function execute(bool $decode = false) { + $this->logger->debug("Executing curl for " . $this->urlApi); + if (DISPLAY_DEBUG) { $startTime = microtime(true); DatabaseConnection::getInstance()->saveLog("Executing API call: " . $this->urlApi . "", false, false, true); @@ -160,6 +165,7 @@ function execute(bool $decode = false) { if (DISPLAY_DEBUG) { DatabaseConnection::getInstance()->saveLog($curlResult); } + $this->logger->error("API call failed with error: ".$jsonDecoded->response->errormessage); throw new InvalidJsonResponseException($jsonDecoded->response->errormessage); } @@ -173,6 +179,7 @@ function execute(bool $decode = false) { if (DISPLAY_DEBUG) { DatabaseConnection::getInstance()->saveLog($curlResult); } + $this->logger->error("API call failed with error: ".$jsonDecoded["error_message"]); throw new InvalidJsonResponseException($jsonDecoded["error_message"]); } } @@ -180,10 +187,11 @@ function execute(bool $decode = false) { $totalTimeMs = round((microtime(true) - $startTime) * 1000); DatabaseConnection::getInstance()->saveLog("Executing took " . $totalTimeMs . "ms", false, false, true); } - if ($decode) + if ($decode) { return $jsonDecoded; - else + } else { return $curlResult; + } } @@ -202,6 +210,7 @@ function execute(bool $decode = false) { private function checkForErrorsAndThrow($curlResult): void { $curlError = curl_errno($this->ch); $responseCode = curl_getinfo($this->ch, CURLINFO_RESPONSE_CODE); + $this->logger->debug("Response code: ".$responseCode." Error: ".$curlError); if (in_array($responseCode, $this->ignoredResultCodes)) return; diff --git a/incl/db.inc.php b/incl/db.inc.php index c8500cb1..9f20593c 100755 --- a/incl/db.inc.php +++ b/incl/db.inc.php @@ -27,6 +27,7 @@ require_once __DIR__ . "/modules/quantityManager.php"; require_once __DIR__ . "/modules/barcodeFederation.php"; require_once __DIR__ . "/modules/dbUpgrade.php"; +require_once __DIR__ . "/logging.inc.php"; //States to tell the script what to do with the barcodes that were scanned @@ -37,6 +38,8 @@ const STATE_GETSTOCK = 4; const STATE_ADD_SL = 5; const STATE_CONSUME_ALL = 6; +const STATE_TXFR = 7; + const SECTION_KNOWN_BARCODES = "known"; const SECTION_UNKNOWN_BARCODES = "unknown"; @@ -62,12 +65,17 @@ */ const DEFAULT_USE_REDIS = "0"; + /** * Thrown when a database connection is already being setup and a new connection is requested * This happens most likely when calling getInstance() during the database upgrade */ class DbConnectionDuringEstablishException extends Exception { +} +class TransferDestination { + public ?int $id = null; + public ?string $name = null; } /** @@ -85,6 +93,7 @@ class DatabaseConnection { "BARCODE_GS" => "BBUDDY-I", "BARCODE_Q" => "BBUDDY-Q-", "BARCODE_AS" => "BBUDDY-AS", + "BARCODE_TXFR" => "BBUDDY-TXFR-", "REVERT_TIME" => "10", "REVERT_SINGLE" => "1", "MORE_VERBOSE" => "1", @@ -138,8 +147,10 @@ class DatabaseConnection { private $db; private static $_ConnectionInstance = null; private static $_StartingConnection = false; + private \Monolog\Logger $logger; private function __construct() { + $this->logger = bb_logger('db'); $this->initDb(); } @@ -178,6 +189,8 @@ static function getInstance(): DatabaseConnection { private function initDb(): void { global $CONFIG; + $this->logger->debug("Initializing database"); + self::checkPermissions(); $this->db = new SQLite3($CONFIG->DATABASE_PATH); $this->db->busyTimeout(5000); @@ -190,6 +203,7 @@ private function initDb(): void { $this->db->exec("CREATE TABLE IF NOT EXISTS Quantities(id INTEGER PRIMARY KEY, barcode TEXT NOT NULL UNIQUE, quantity INTEGER NOT NULL, product TEXT)"); $this->db->exec("CREATE TABLE IF NOT EXISTS ApiKeys(id INTEGER PRIMARY KEY, key TEXT NOT NULL UNIQUE, lastused INTEGER NOT NULL)"); $this->db->exec("CREATE TABLE IF NOT EXISTS LookupProviderData(providerType INTEGER PRIMARY KEY, data TEXT NOT NULL)"); + $this->db->exec("CREATE TABLE IF NOT EXISTS Transfer(id INTEGER PRIMARY KEY, dest_id INTEGER NULL, dest_name TEXT NULL)"); $this->insertDefaultValues(); $previousVersion = intval(BBConfig::getInstance($this)["version"]); if ($previousVersion < BB_VERSION) { @@ -233,13 +247,15 @@ private function checkPermissions(): void { global $CONFIG; if (file_exists($CONFIG->DATABASE_PATH)) { if (!is_writable($CONFIG->DATABASE_PATH)) { + $this->logger->error("DB Error: Existing DB not writable"); showErrorNotWritable("DB Error: DB_Not_Writable"); } } else { DbUpgrade::createDbDirectory(); DbUpgrade::checkAndMoveIfOldDbLocation(); if (!is_writable(dirname($CONFIG->DATABASE_PATH))) { - showErrorNotWritable("DB Error Not_Writable"); + $this->logger->error("DB Error: New/Moved DB not writable"); + showErrorNotWritable("DB Error: Not_Writable"); } } } @@ -260,6 +276,7 @@ public function getTransactionState(): int { else return $state; } else { + $this->logger->error("DB Error: No state found"); die("DB Error"); } } @@ -563,6 +580,7 @@ public function getLogs(): array { public function saveError(string $errorMessage, bool $isFatal = true): void { + $this->logger->error($errorMessage, ['isFatal' => $isFatal]); $verboseError = '' . sanitizeString($errorMessage) . ' Please check your URL and API key in the settings menu!'; $this->saveLog($verboseError, false, true); if ($isFatal) { @@ -582,6 +600,8 @@ public function saveError(string $errorMessage, bool $isFatal = true): void { * @throws DbConnectionDuringEstablishException */ public function saveLog(string $log, bool $isVerbose = false, bool $isError = false, bool $isDebug = false): void { + $this->logger->debug($log); + if ($isVerbose == false || BBConfig::getInstance()["MORE_VERBOSE"] == true) { $date = date('Y-m-d H:i:s'); if ($isError || $isDebug) { @@ -653,6 +673,37 @@ public function updateConfig(string $key, ?string $value): void { BBConfig::getInstance()[$key] = $value; } + public function getTransferDestination(): TransferDestination + { + $res = $this->db->query("SELECT dest_id, dest_name FROM Transfer WHERE id=1"); + if ($res !== false && ($row = $res->fetchArray(SQLITE3_ASSOC))) { + $dest = new TransferDestination(); + $dest->id = $row['dest_id'] !== null ? (int) $row['dest_id'] : null; + $dest->name = $row['dest_name'] ?? null; + + return $dest; + } + + return new TransferDestination(); + } + + + public function setTransferDestination(?int $destId, ?string $destName = null): void + { + if (null === $destName) { + $this->db->exec("INSERT INTO Transfer(id, dest_id, dest_name) VALUES(1, $destId, NULL) ON CONFLICT(id) DO UPDATE SET dest_id=$destId, dest_name=NULL"); + + return; + } + + $this->db->exec("INSERT INTO Transfer(id, dest_id, dest_name) VALUES(1, $destId, '$destName') ON CONFLICT(id) DO UPDATE SET dest_id=$destId, dest_name='$destName'"); + } + + public function setTransferDestinationName(?string $destName): void + { + $this->db->exec("INSERT INTO Transfer(id, dest_name) VALUES(1, '$destName') ON CONFLICT(id) DO UPDATE SET dest_name='$destName'"); + } + public function getDatabaseReference(): SQLite3 { return $this->db; } diff --git a/incl/js/JsBarcode.all.min.js b/incl/js/JsBarcode.all.min.js new file mode 100644 index 00000000..80d1092b --- /dev/null +++ b/incl/js/JsBarcode.all.min.js @@ -0,0 +1,2 @@ +/*! JsBarcode v3.12.3 | (c) Johan Lindell | MIT license */ +!function(t){var e={};function n(r){if(e[r])return e[r].exports;var o=e[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=t,n.c=e,n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:r})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var o in t)n.d(r,o,function(e){return t[e]}.bind(null,o));return r},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=16)}([function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});e.default=function t(e,n){!function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),this.data=e,this.text=n.text||e,this.options=n}},function(t,e,n){"use strict";var r;function o(t,e,n){return e in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}Object.defineProperty(e,"__esModule",{value:!0});var i=e.SET_A=0,a=e.SET_B=1,u=e.SET_C=2,f=(e.SHIFT=98,e.START_A=103),c=e.START_B=104,s=e.START_C=105;e.MODULO=103,e.STOP=106,e.FNC1=207,e.SET_BY_CODE=(o(r={},f,i),o(r,c,a),o(r,s,u),r),e.SWAP={101:i,100:a,99:u},e.A_START_CHAR=String.fromCharCode(208),e.B_START_CHAR=String.fromCharCode(209),e.C_START_CHAR=String.fromCharCode(210),e.A_CHARS="[\0-_È-Ï]",e.B_CHARS="[ -È-Ï]",e.C_CHARS="(Ï*[0-9]{2}Ï*)",e.BARS=[11011001100,11001101100,11001100110,10010011e3,10010001100,10001001100,10011001e3,10011000100,10001100100,11001001e3,11001000100,11000100100,10110011100,10011011100,10011001110,10111001100,10011101100,10011100110,11001110010,11001011100,11001001110,11011100100,11001110100,11101101110,11101001100,11100101100,11100100110,11101100100,11100110100,11100110010,11011011e3,11011000110,11000110110,10100011e3,10001011e3,10001000110,10110001e3,10001101e3,10001100010,11010001e3,11000101e3,11000100010,10110111e3,10110001110,10001101110,10111011e3,10111000110,10001110110,11101110110,11010001110,11000101110,11011101e3,11011100010,11011101110,11101011e3,11101000110,11100010110,11101101e3,11101100010,11100011010,11101111010,11001000010,11110001010,1010011e4,10100001100,1001011e4,10010000110,10000101100,10000100110,1011001e4,10110000100,1001101e4,10011000010,10000110100,10000110010,11000010010,1100101e4,11110111010,11000010100,10001111010,10100111100,10010111100,10010011110,10111100100,10011110100,10011110010,11110100100,11110010100,11110010010,11011011110,11011110110,11110110110,10101111e3,10100011110,10001011110,10111101e3,10111100010,11110101e3,11110100010,10111011110,10111101110,11101011110,11110101110,11010000100,1101001e4,11010011100,1100011101011]},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});e.SIDE_BIN="101",e.MIDDLE_BIN="01010",e.BINARIES={L:["0001101","0011001","0010011","0111101","0100011","0110001","0101111","0111011","0110111","0001011"],G:["0100111","0110011","0011011","0100001","0011101","0111001","0000101","0010001","0001001","0010111"],R:["1110010","1100110","1101100","1000010","1011100","1001110","1010000","1000100","1001000","1110100"],O:["0001101","0011001","0010011","0111101","0100011","0110001","0101111","0111011","0110111","0001011"],E:["0100111","0110011","0011011","0100001","0011101","0111001","0000101","0010001","0001001","0010111"]},e.EAN2_STRUCTURE=["LL","LG","GL","GG"],e.EAN5_STRUCTURE=["GGLLL","GLGLL","GLLGL","GLLLG","LGGLL","LLGGL","LLLGG","LGLGL","LGLLG","LLGLG"],e.EAN13_STRUCTURE=["LLLLLL","LLGLGG","LLGGLG","LLGGGL","LGLLGG","LGGLLG","LGGGLL","LGLGLG","LGLGGL","LGGLGL"]},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n(2);e.default=function(t,e,n){var o=t.split("").map((function(t,n){return r.BINARIES[e[n]]})).map((function(e,n){return e?e[t[n]]:""}));if(n){var i=t.length-1;o=o.map((function(t,e){return e=200){i=t.shift()-105;var a=u.SWAP[i];void 0!==a?o=e.next(t,n+1,a):(r!==u.SET_A&&r!==u.SET_B||i!==u.SHIFT||(t[0]=r===u.SET_A?t[0]>95?t[0]-96:t[0]:t[0]<32?t[0]+96:t[0]),o=e.next(t,n+1,r))}else i=e.correctIndex(t,r),o=e.next(t,n+1,r);var f=i*n;return{result:e.getBar(i)+o.result,checksum:f+o.checksum}}}]),e}(a.default);e.default=f},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.mod10=function(t){for(var e=0,n=0;n10*n.width?10*n.width:n.fontSize,r.guardHeight=n.height+r.fontSize/2+n.textMargin,r}return function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}(e,t),r(e,[{key:"encode",value:function(){return this.options.flat?this.encodeFlat():this.encodeGuarded()}},{key:"leftText",value:function(t,e){return this.text.substr(t,e)}},{key:"leftEncode",value:function(t,e){return(0,i.default)(t,e)}},{key:"rightText",value:function(t,e){return this.text.substr(t,e)}},{key:"rightEncode",value:function(t,e){return(0,i.default)(t,e)}},{key:"encodeGuarded",value:function(){var t={fontSize:this.fontSize},e={height:this.guardHeight};return[{data:o.SIDE_BIN,options:e},{data:this.leftEncode(),text:this.leftText(),options:t},{data:o.MIDDLE_BIN,options:e},{data:this.rightEncode(),text:this.rightText(),options:t},{data:o.SIDE_BIN,options:e}]}},{key:"encodeFlat",value:function(){return{data:[o.SIDE_BIN,this.leftEncode(),o.MIDDLE_BIN,this.rightEncode(),o.SIDE_BIN].join(""),text:this.text}}}]),e}(a(n(0)).default);e.default=u},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=function(){function t(t,e){for(var n=0;n10*n.width?r.fontSize=10*n.width:r.fontSize=n.fontSize,r.guardHeight=n.height+r.fontSize/2+n.textMargin,r}return function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}(e,t),r(e,[{key:"valid",value:function(){return-1!==this.data.search(/^[0-9]{12}$/)&&this.data[11]==u(this.data)}},{key:"encode",value:function(){return this.options.flat?this.flatEncoding():this.guardedEncoding()}},{key:"flatEncoding",value:function(){var t="";return t+="101",t+=(0,o.default)(this.data.substr(0,6),"LLLLLL"),t+="01010",t+=(0,o.default)(this.data.substr(6,6),"RRRRRR"),{data:t+="101",text:this.text}}},{key:"guardedEncoding",value:function(){var t=[];return this.displayValue&&t.push({data:"00000000",text:this.text.substr(0,1),options:{textAlign:"left",fontSize:this.fontSize}}),t.push({data:"101"+(0,o.default)(this.data[0],"L"),options:{height:this.guardHeight}}),t.push({data:(0,o.default)(this.data.substr(1,5),"LLLLL"),text:this.text.substr(1,5),options:{fontSize:this.fontSize}}),t.push({data:"01010",options:{height:this.guardHeight}}),t.push({data:(0,o.default)(this.data.substr(6,5),"RRRRR"),text:this.text.substr(6,5),options:{fontSize:this.fontSize}}),t.push({data:(0,o.default)(this.data[11],"R")+"101",options:{height:this.guardHeight}}),this.displayValue&&t.push({data:"00000000",text:this.text.substr(11,1),options:{textAlign:"right",fontSize:this.fontSize}}),t}}]),e}(i(n(0)).default);function u(t){var e,n=0;for(e=1;e<11;e+=2)n+=parseInt(t[e]);for(e=0;e<11;e+=2)n+=3*parseInt(t[e]);return(10-n%10)%10}e.default=a},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r,o=function(){function t(t,e){for(var n=0;n0?e.fontSize+e.textMargin:0)+e.marginTop+e.marginBottom}function u(t,e,n){if(n.displayValue&&ee&&(e=t[n].height);return e},e.getEncodingHeight=a,e.getBarcodePadding=u,e.calculateEncodingAttributes=function(t,e,n){for(var r=0;r=i(t);return e+String.fromCharCode(r?206:205)+u(t,r)}e.default=function(t){var e=void 0;if(a(t).length>=2)e=r.C_START_CHAR+f(t);else{var n=o(t)>i(t);e=(n?r.A_START_CHAR:r.B_START_CHAR)+u(t,n)}return e.replace(/[\xCD\xCE]([^])[\xCD\xCE]/,(function(t,e){return String.fromCharCode(203)+e}))}},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r,o=function(){function t(t,e){for(var n=0;n10*n.width?r.fontSize=10*n.width:r.fontSize=n.fontSize,r.guardHeight=n.height+r.fontSize/2+n.textMargin,r}return function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}(e,t),r(e,[{key:"valid",value:function(){return this.isValid}},{key:"encode",value:function(){return this.options.flat?this.flatEncoding():this.guardedEncoding()}},{key:"flatEncoding",value:function(){var t="";return t+="101",t+=this.encodeMiddleDigits(),{data:t+="010101",text:this.text}}},{key:"guardedEncoding",value:function(){var t=[];return this.displayValue&&t.push({data:"00000000",text:this.text[0],options:{textAlign:"left",fontSize:this.fontSize}}),t.push({data:"101",options:{height:this.guardHeight}}),t.push({data:this.encodeMiddleDigits(),text:this.text.substring(1,7),options:{fontSize:this.fontSize}}),t.push({data:"010101",options:{height:this.guardHeight}}),this.displayValue&&t.push({data:"00000000",text:this.text[7],options:{textAlign:"right",fontSize:this.fontSize}}),t}},{key:"encodeMiddleDigits",value:function(){var t=this.upcA[0],e=this.upcA[this.upcA.length-1],n=s[parseInt(e)][parseInt(t)];return(0,o.default)(this.middleDigits,n)}}]),e}(i.default);function p(t,e){for(var n=parseInt(t[t.length-1]),r=c[n],o="",i=0,u=0;u=3&&this.number<=131070}}]),e}(((r=i)&&r.__esModule?r:{default:r}).default);e.pharmacode=a},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.codabar=void 0;var r,o=function(){function t(t,e){for(var n=0;n":["(%)","I"],"?":["(%)","J"],"@":["(%)","V"],"[":["(%)","K"],"\\":["(%)","L"],"]":["(%)","M"],"^":["(%)","N"],_:["(%)","O"],"`":["(%)","W"],a:["(+)","A"],b:["(+)","B"],c:["(+)","C"],d:["(+)","D"],e:["(+)","E"],f:["(+)","F"],g:["(+)","G"],h:["(+)","H"],i:["(+)","I"],j:["(+)","J"],k:["(+)","K"],l:["(+)","L"],m:["(+)","M"],n:["(+)","N"],o:["(+)","O"],p:["(+)","P"],q:["(+)","Q"],r:["(+)","R"],s:["(+)","S"],t:["(+)","T"],u:["(+)","U"],v:["(+)","V"],w:["(+)","W"],x:["(+)","X"],y:["(+)","Y"],z:["(+)","Z"],"{":["(%)","P"],"|":["(%)","Q"],"}":["(%)","R"],"~":["(%)","S"],"":["(%)","T"]}},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r,o=function(){function t(t,e){for(var n=0;n0?(n=0,o.textAlign="left"):"right"==t.textAlign?(n=e.width-1,o.textAlign="right"):(n=e.width/2,o.textAlign="center"),o.fillText(e.text,n,r))}},{key:"moveCanvasDrawing",value:function(t){this.canvas.getContext("2d").translate(t.width,0)}},{key:"restoreCanvas",value:function(){this.canvas.getContext("2d").restore()}}]),t}();e.default=f},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r,o=function(){function t(t,e){for(var n=0;n0&&(this.drawRect(a-e.width*i,r,e.width*i,e.height,t),i=0);i>0&&this.drawRect(a-e.width*(i-1),r,e.width*i,e.height,t)}},{key:"drawSVGText",value:function(t,e,n){var r,o,i=this.document.createElementNS(f,"text");e.displayValue&&(i.setAttribute("font-family",e.font),i.setAttribute("font-size",e.fontSize),e.fontOptions.includes("bold")&&i.setAttribute("font-weight","bold"),e.fontOptions.includes("italic")&&i.setAttribute("font-style","italic"),o="top"==e.textPosition?e.fontSize-e.textMargin:e.height+e.textMargin+e.fontSize,"left"==e.textAlign||n.barcodePadding>0?(r=0,i.setAttribute("text-anchor","start")):"right"==e.textAlign?(r=n.width-1,i.setAttribute("text-anchor","end")):(r=n.width/2,i.setAttribute("text-anchor","middle")),i.setAttribute("x",r),i.setAttribute("y",o),i.appendChild(this.document.createTextNode(n.text)),t.appendChild(i))}},{key:"setSvgAttributes",value:function(t,e){var n=this.svg;n.setAttribute("width",t+"px"),n.setAttribute("height",e+"px"),n.setAttribute("x","0px"),n.setAttribute("y","0px"),n.setAttribute("viewBox","0 0 "+t+" "+e),n.setAttribute("xmlns",f),n.setAttribute("version","1.1")}},{key:"createGroup",value:function(t,e,n){var r=this.document.createElementNS(f,"g");return r.setAttribute("transform","translate("+t+", "+e+")"),n.appendChild(r),r}},{key:"setGroupOptions",value:function(t,e){t.setAttribute("fill",e.lineColor)}},{key:"drawRect",value:function(t,e,n,r,o){var i=this.document.createElementNS(f,"rect");return i.setAttribute("x",t),i.setAttribute("y",e),i.setAttribute("width",n),i.setAttribute("height",r),o.appendChild(i),i}}]),t}();e.default=c},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=function(){function t(t,e){for(var n=0;n= $maxBytes + * - keeps $maxFiles rotated logs (.1 .. .$maxFiles) + * + * Uses flock() on a per-logfile lock to avoid rotation races across processes. + */ +final class SizeRotatingFileHandler extends StreamHandler +{ + private string $filename; + private int $maxBytes; + private int $maxFiles; + + public function __construct(string $filename, $level = Logger::DEBUG, bool $bubble = true, int $maxBytes = 5242880, int $maxFiles = 5) + { + $this->filename = $filename; + $this->maxBytes = $maxBytes; + $this->maxFiles = $maxFiles; + + parent::__construct($filename, $level, $bubble); + } + + protected function write(array $record): void + { + $this->maybeRotate(); + parent::write($record); + } + + private function maybeRotate(): void + { + if (!is_file($this->filename)) { + return; + } + + clearstatcache(true, $this->filename); + $size = filesize($this->filename); + if ($size === false || $size < $this->maxBytes) { + return; + } + + $lockHandle = $this->acquireRotationLock(); + if ($lockHandle === null) { + // If we can't lock, skip rotation; next write will try again. + return; + } + + try { + // Another process may have rotated while we waited; re-check. + clearstatcache(true, $this->filename); + $sizeAfterLock = is_file($this->filename) ? filesize($this->filename) : false; + if ($sizeAfterLock === false || $sizeAfterLock < $this->maxBytes) { + return; + } + + // Rotate: file.log.4 -> file.log.5, ..., file.log -> file.log.1 + for ($i = $this->maxFiles - 1; $i >= 1; $i--) { + $src = $this->filename . '.' . $i; + $dst = $this->filename . '.' . ($i + 1); + if (is_file($src)) { + @rename($src, $dst); + } + } + + @rename($this->filename, $this->filename . '.1'); + } finally { + $this->releaseRotationLock($lockHandle); + } + } + + /** + * @return resource|null + */ + private function acquireRotationLock() + { + $lockFile = $this->filename . '.lock'; + $h = @fopen($lockFile, 'c'); + if ($h === false) { + return null; + } + + // Block until we can rotate safely (Linux-friendly). + if (!@flock($h, LOCK_EX)) { + @fclose($h); + return null; + } + + return $h; + } + + /** + * @param resource $lockHandle + */ + private function releaseRotationLock($lockHandle): void + { + @flock($lockHandle, LOCK_UN); + @fclose($lockHandle); + } +} + +final class BBLog +{ + /** @var array */ + private static array $loggers = array(); + + public static function get(string $channel): Logger + { + $channel = $channel !== '' ? $channel : 'app'; + + if (isset(self::$loggers[$channel])) { + return self::$loggers[$channel]; + } + + // Config is loaded very early in this app; use it if available. + $levelName = 'info'; + $combined = true; + + if (isset($GLOBALS['CONFIG'])) { + if (isset($GLOBALS['CONFIG']->LOG_LEVEL)) { + $levelName = (string)$GLOBALS['CONFIG']->LOG_LEVEL; + } + if (isset($GLOBALS['CONFIG']->LOG_COMBINED)) { + $combined = (bool)$GLOBALS['CONFIG']->LOG_COMBINED; + } + } else { + if (defined('LOG_LEVEL')) { + $levelName = (string)LOG_LEVEL; + } + if (defined('LOG_COMBINED')) { + $combined = (bool)LOG_COMBINED; + } + } + + $level = Logger::toMonologLevel($levelName); + + $logDir = __DIR__ . '/../data/logs'; + if (!is_dir($logDir)) { + @mkdir($logDir, 0775, true); + } + + $fileBaseName = $combined ? 'bbuddy' : $channel; + $file = $logDir . '/' . $fileBaseName . '.log'; + + $logger = new Logger($channel); + + $handler = new SizeRotatingFileHandler( + $file, + $level, + true, + 5 * 1024 * 1024, // 5 MB + 5 // keep last 5 rotated logs + ); + + // Nice single-line logs, includes channel already via %channel% + $format = "[%datetime%] %level_name% %channel%: %message% %context% %extra%\n"; + $handler->setFormatter(new LineFormatter($format, null, true, true)); + + $logger->pushHandler($handler); + + self::$loggers[$channel] = $logger; + return $logger; + } +} + +/** Convenience function */ +function bb_logger(string $channel): Logger +{ + return BBLog::get($channel); +} diff --git a/incl/modules/dbUpgrade.php b/incl/modules/dbUpgrade.php index ccb3cca6..75f89fa0 100644 --- a/incl/modules/dbUpgrade.php +++ b/incl/modules/dbUpgrade.php @@ -15,6 +15,9 @@ */ require_once __DIR__ . "/../db.inc.php"; +require_once __DIR__ . "/../logging.inc.php"; + +$loggerDbUpgrade = bb_logger('db-upgrade'); class DbUpgrade { @@ -22,8 +25,12 @@ class DbUpgrade { private $db; private $databaseConnection; + private \Monolog\Logger $logger; public function __construct(DatabaseConnection $databaseConnection) { + global $loggerDbUpgrade; + + $this->logger = $loggerDbUpgrade; $this->db = $databaseConnection->getDatabaseReference(); $this->databaseConnection = $databaseConnection; } @@ -35,12 +42,13 @@ public function __construct(DatabaseConnection $databaseConnection) { * @return void */ public static function checkAndMoveIfOldDbLocation(): void { - global $CONFIG; + global $CONFIG, $loggerDbUpgrade; //If only old db exists, create directory and move file if (file_exists(self::LEGACY_DATABASE_PATH) && !file_exists($CONFIG->DATABASE_PATH)) { self::createDbDirectory(); $couldMove = rename(self::LEGACY_DATABASE_PATH, $CONFIG->DATABASE_PATH); if (!$couldMove) { + $loggerDbUpgrade->error("Could not move database to new location"); showErrorNotWritable("DB Error Could_Not_Move"); } } @@ -48,11 +56,12 @@ public static function checkAndMoveIfOldDbLocation(): void { public static function createDbDirectory(): void { - global $CONFIG; + global $CONFIG, $loggerDbUpgrade; $dirName = dirname($CONFIG->DATABASE_PATH); if (!file_exists($dirName)) { $couldCreateDir = mkdir($dirName, 0700, true); if (!$couldCreateDir) { + $loggerDbUpgrade->error("Could not create database directory"); showErrorNotWritable("DB Error Could_Not_Create_Dir"); } } @@ -74,6 +83,7 @@ public function upgradeBarcodeBuddy(int $previousVersion): void { $this->db->exec("UPDATE BBConfig SET value='" . BB_VERSION . "' WHERE data='version'"); //Place for future update protocols if ($previousVersion < 1211) { + $this->logger->info("Upgrading database to version 1211"); $config = BBConfig::getInstance(); $this->databaseConnection->updateConfig("BARCODE_C", strtoupper($config["BARCODE_C"])); $this->databaseConnection->updateConfig("BARCODE_O", strtoupper($config["BARCODE_O"])); @@ -84,27 +94,32 @@ public function upgradeBarcodeBuddy(int $previousVersion): void { $this->isSupportedGrocyVersionOrDie(); } if ($previousVersion < 1501) { + $this->logger->info("Upgrading database to version 1501"); $this->db->exec("ALTER TABLE Barcodes ADD COLUMN requireWeight INTEGER"); } if ($previousVersion < 1504) { + $this->logger->info("Upgrading database to version 1504"); $this->db->exec("ALTER TABLE Barcodes ADD COLUMN bestBeforeInDays INTEGER"); $this->db->exec("ALTER TABLE Barcodes ADD COLUMN price TEXT"); $this->isSupportedGrocyVersionOrDie(); } if ($previousVersion < 1511) { //Only sqlite 3.25+ supports renaming columns, therefore creating new table instead + $this->logger->info("Upgrading database to version 1511"); $this->db->exec("ALTER TABLE Quantities RENAME TO Quantities_temp;"); $this->db->exec("CREATE TABLE Quantities(id INTEGER PRIMARY KEY, barcode TEXT NOT NULL UNIQUE, quantity INTEGER NOT NULL, product TEXT)"); $this->db->exec("INSERT INTO Quantities(id, barcode, quantity, product) SELECT id, barcode, quantitiy, product FROM Quantities_temp;"); $this->db->exec("DROP TABLE Quantities_temp;"); } if ($previousVersion < 1653) { + $this->logger->info("Upgrading database to version 1653"); $config = BBConfig::getInstance(); if ($config["LOOKUP_ORDER"] != DatabaseConnection::DEFAULT_VALUES["LOOKUP_ORDER"]) { $this->databaseConnection->updateConfig("LOOKUP_ORDER", $config["LOOKUP_ORDER"] . ",6"); } } if ($previousVersion < 1660) { + $this->logger->info("Upgrading database to version 1660"); $quantities = $this->getQuantitiesForUpgrade(); foreach ($quantities as $quantity) { if ($quantity->product != null) { @@ -120,6 +135,7 @@ public function upgradeBarcodeBuddy(int $previousVersion): void { } } if ($previousVersion < 1800) { + $this->logger->info("Upgrading database to version 1800"); $config = BBConfig::getInstance(); if ($config["LOOKUP_ORDER"] != DatabaseConnection::DEFAULT_VALUES["LOOKUP_ORDER"]) { $this->databaseConnection->updateConfig("LOOKUP_ORDER", $config["LOOKUP_ORDER"] . ",7"); @@ -129,25 +145,35 @@ public function upgradeBarcodeBuddy(int $previousVersion): void { } if ($previousVersion < 1802) { //In v1800 the db was not initialised properly for new installations. This has to be done again + $this->logger->info("Upgrading database to version 1802"); $columnInfo = $this->db->querySingle("SELECT COUNT(*) AS CNTREC FROM pragma_table_info('Barcodes') WHERE name='bbServerAltNames'"); if ($columnInfo == 0) $this->db->exec("ALTER TABLE Barcodes ADD COLUMN bbServerAltNames TEXT"); } if ($previousVersion < 1803) { + $this->logger->info("Upgrading database to version 1803"); $config = BBConfig::getInstance(); if ($config["LOOKUP_ORDER"] != DatabaseConnection::DEFAULT_VALUES["LOOKUP_ORDER"]) { $this->databaseConnection->updateConfig("LOOKUP_ORDER", $config["LOOKUP_ORDER"] . ",8"); } } if ($previousVersion < 1804) { + $this->logger->info("Upgrading database to version 1804"); $this->databaseConnection->setTransactionState(0); } if ($previousVersion < 1818) { + $this->logger->info("Upgrading database to version 1818"); $config = BBConfig::getInstance(); if ($config["LOOKUP_ORDER"] != DatabaseConnection::DEFAULT_VALUES["LOOKUP_ORDER"]) { $this->databaseConnection->updateConfig("LOOKUP_ORDER", $config["LOOKUP_ORDER"] . ",9"); } } + if ($previousVersion < 1819) { + $this->logger->info("Upgrading database to version 1819"); + $config = BBConfig::getInstance(); + $this->databaseConnection->updateConfig("BARCODE_TXFR", strtoupper($config["BARCODE_TXFR"])); + $this->db->exec("CREATE TABLE IF NOT EXISTS Transfer(id INTEGER PRIMARY KEY, dest_id INTEGER NULL, dest_name TEXT NULL)"); + } RedisConnection::updateCache(); } @@ -162,6 +188,8 @@ private function isSupportedGrocyVersionOrDie(): void { $ERROR_MESSAGE = "Grocy " . MIN_GROCY_VERSION . " or newer required. You are running $version, please upgrade your Grocy instance."; } if ($ERROR_MESSAGE != null) { + $this->logger->error($ERROR_MESSAGE); + $ERROR_MESSAGE .= " Click here to re-enter your credentials."; $this->databaseConnection->updateConfig("GROCY_API_KEY", null); include __DIR__ . "/../../error.php"; diff --git a/incl/processing.inc.php b/incl/processing.inc.php index 393e1747..f4903552 100755 --- a/incl/processing.inc.php +++ b/incl/processing.inc.php @@ -21,6 +21,9 @@ require_once __DIR__ . "/config.inc.php"; require_once __DIR__ . "/lookupProviders/BarcodeLookup.class.php"; require_once __DIR__ . "/modules/choreManager.php"; +require_once __DIR__ . "/logging.inc.php"; + +$procLog = bb_logger('processing'); /** * @@ -32,6 +35,8 @@ * @throws DbConnectionDuringEstablishException */ function processNewBarcode(string $barcodeInput, ?string $bestBeforeInDays = null, ?string $price = null): string { + global $procLog; + $db = DatabaseConnection::getInstance(); $config = BBConfig::getInstance(); @@ -64,6 +69,33 @@ function processNewBarcode(string $barcodeInput, ?string $bestBeforeInDays = nul $db->setTransactionState(STATE_ADD_SL); return createLogModeChange(STATE_ADD_SL); } + if (stringStartsWith($barcode, $config["BARCODE_TXFR"])) { + $destId = intval(str_replace($config["BARCODE_TXFR"], "", $barcode)); + $procLog->debug("Scan set destination to ".$destId); + $txfrDest = $db->getTransferDestination(); + $procLog->debug("Current destination is ".$txfrDest->id); + + if ($txfrDest->id !== $destId) { + $location = API::getLocation($destId); + $procLog->debug("Api location = ".print_r($location, true)); + if (null !== $location->id) { + $txfrDest->id = $location->id; + $txfrDest->name = $location->name; + $db->setTransferDestination($destId, $location->name); + } else { + $procLog->warning("Invalid destination location: ".$destId); + $log = new LogOutput("Invalid destination location: ".$destId, EVENT_TYPE_ERROR, null, true); + return $log + ->setVerbose() + ->setWebsocketResultCode(WS_RESULT_TXFR_INVALID) + ->createLog() + ; + } + } + + $db->setTransactionState(STATE_TXFR); + return createLogModeChange(STATE_TXFR, " to " . $txfrDest->name); + } if (stringStartsWith($barcode, $config["BARCODE_Q"])) { $quantity = str_replace($config["BARCODE_Q"], "", $barcode); $quantity = checkIfFloat($quantity); @@ -79,11 +111,13 @@ function processNewBarcode(string $barcodeInput, ?string $bestBeforeInDays = nul if (trim($barcode) == "") { $log = new LogOutput("Invalid barcode found", EVENT_TYPE_ERROR); + $procLog->warning("Invalid barcode found: ".$barcode); return $log->setVerbose()->setWebsocketResultCode(WS_RESULT_PRODUCT_UNKNOWN)->createLog(); } if (ChoreManager::isChoreBarcode($barcode)) { $choreText = processChoreBarcode($barcode); + $procLog->debug("Executed chore: ".$choreText); $log = new LogOutput("Executed chore: " . $choreText, EVENT_TYPE_EXEC_CHORE); return $log->setVerbose()->createLog(); } @@ -102,7 +136,9 @@ function processNewBarcode(string $barcodeInput, ?string $bestBeforeInDays = nul } -function createLogModeChange(int $state): string { +function createLogModeChange(int $state, ?string $extra = null): string { + global $procLog; + $text = "Set state to "; switch ($state) { case STATE_CONSUME: @@ -126,9 +162,18 @@ function createLogModeChange(int $state): string { case STATE_CONSUME_ALL: $text .= "Consume all"; break; + case STATE_TXFR: + $text .= "Transfer"; + break; default: + $procLog->warning("Invalid state: ".$state); throw new Exception("Invalid state"); } + if ($extra != null) { + $text .= $extra; + } + + $procLog->debug($text); $log = new LogOutput($text, EVENT_TYPE_MODE_CHANGE); return $log->setVerbose()->createLog(); } @@ -156,12 +201,14 @@ function createLogModeChange(int $state): string { const EVENT_TYPE_ACTION_REQUIRED = 17; const EVENT_TYPE_CONSUME_ALL_PRODUCT = 18; const EVENT_TYPE_NO_STOCK = 19; +const EVENT_TYPE_TRANSFER_PRODUCT = 20; const WS_RESULT_PRODUCT_FOUND = 0; const WS_RESULT_PRODUCT_LOOKED_UP = 1; const WS_RESULT_PRODUCT_UNKNOWN = 2; const WS_RESULT_MODE_CHANGE = 4; +const WS_RESULT_TXFR_INVALID = 8; const WS_RESULT_ERROR = 'E'; @@ -195,10 +242,20 @@ function processChoreBarcode(string $barcode) { function processUnknownBarcode(string $barcode, bool $websocketEnabled, LockGenerator &$fileLock, ?string $bestBeforeInDays, ?string $price): string { $db = DatabaseConnection::getInstance(); $amount = 1; + if ($db->getTransactionState() == STATE_PURCHASE) { $amount = QuantityManager::getStoredQuantityForBarcode($barcode); } - if ($db->isUnknownBarcodeAlreadyStored($barcode)) { + + if ($db->getTransactionState() == STATE_TXFR) { + $log = new LogOutput("Cannot transfer unknown barcode, ignoring.", EVENT_TYPE_TRANSFER_PRODUCT, $barcode); + $output = $log + ->insertBarcodeInWebsocketText() + ->setSendWebsocket($websocketEnabled) + ->setWebsocketResultCode(WS_RESULT_PRODUCT_UNKNOWN) + ->createLog(); + } + elseif ($db->isUnknownBarcodeAlreadyStored($barcode)) { //Unknown barcode already in local database $db->addQuantityToUnknownBarcode($barcode, $amount); $log = new LogOutput("Unknown product already scanned. Increasing quantity.", EVENT_TYPE_ADD_NEW_BARCODE, $barcode); @@ -207,18 +264,19 @@ function processUnknownBarcode(string $barcode, bool $websocketEnabled, LockGene ->setSendWebsocket($websocketEnabled) ->setWebsocketResultCode(WS_RESULT_PRODUCT_LOOKED_UP) ->createLog(); - } else { - $productname = null; + } + else { + $productName = null; if (is_numeric($barcode)) { - $productname = BarcodeLookup::lookup($barcode); + $productName = BarcodeLookup::lookup($barcode); } - if ($productname != null) { - $db->insertUnrecognizedBarcode($barcode, $amount, $bestBeforeInDays, $price, $productname); - $log = new LogOutput("Unknown barcode looked up, found name: " . $productname["name"], EVENT_TYPE_ADD_NEW_BARCODE, $barcode); + if ($productName != null) { + $db->insertUnrecognizedBarcode($barcode, $amount, $bestBeforeInDays, $price, $productName); + $log = new LogOutput("Unknown barcode looked up, found name: " . $productName["name"], EVENT_TYPE_ADD_NEW_BARCODE, $barcode); $output = $log ->insertBarcodeInWebsocketText() ->setSendWebsocket($websocketEnabled) - ->setCustomWebsocketText($productname["name"]) + ->setCustomWebsocketText($productName["name"]) ->setWebsocketResultCode(WS_RESULT_PRODUCT_LOOKED_UP) ->createLog(); } else { @@ -250,7 +308,8 @@ function stateToString(int $state): string { STATE_PURCHASE => "Purchase", STATE_OPEN => "Open", STATE_GETSTOCK => "Inventory", - STATE_ADD_SL => "Add to shoppinglist" + STATE_ADD_SL => "Add to shoppinglist", + STATE_TXFR => "Transfer", ); return $allowedModes[$state]; } @@ -311,6 +370,9 @@ function processModeChangeGetParameter(string $modeParameter): void { case "shoppinglist": $db->setTransactionState(STATE_ADD_SL); break; + case "transfer": + $db->setTransactionState(STATE_TXFR); + break; } } @@ -344,6 +406,8 @@ function processRefreshedBarcode(string $barcode): void { * @throws DbConnectionDuringEstablishException */ function processKnownBarcode(GrocyProduct $productInfo, string $barcode, bool $websocketEnabled, LockGenerator &$fileLock, ?string $bestBeforeInDays, ?string $price): string { + global $procLog; + $config = BBConfig::getInstance(); $db = DatabaseConnection::getInstance(); @@ -485,6 +549,48 @@ function processKnownBarcode(GrocyProduct $productInfo, string $barcode, bool $w $log = "Added to shopping list: " . $amount . " " . $productInfo->unit . " of " . $productInfo->name; API::addToShoppinglist($productInfo->id, 1); return (new LogOutput($log, EVENT_TYPE_ADD_TO_SHOPPINGLIST))->createLog(); + case STATE_TXFR: + + // Get transfer destination and update if necessary + $transferDest = $db->getTransferDestination(); + if (empty($transferDest->name)) { + $location = API::getLocation($transferDest->id); + if (null !== $location->id) { + $transferDest->name = $location->name; + $db->setTransferDestinationName($transferDest->name); + } + } + + // Validate transfer destination + if ($transferDest->id == $productInfo->location->id) { + return (new LogOutput("Transfer destination is same as current location, skipping", EVENT_TYPE_TRANSFER_PRODUCT)) + ->setWebsocketResultCode(WS_RESULT_PRODUCT_UNKNOWN) + ->createLog() + ; + } + + // Transfer product + try { + API::transferProduct($productInfo->id, $productInfo->location->id, $transferDest->id); + } catch (\Throwable $e) { + $procLog->error("Transfer failed: " . $e->getMessage()); + return (new LogOutput("Transfer failed: " . $e->getMessage(), EVENT_TYPE_TRANSFER_PRODUCT)) + ->setWebsocketResultCode(WS_RESULT_TXFR_INVALID) + ->setVerbose() + ->createLog(); + } + + $output = (new LogOutput( + "Transferred product: " . $productInfo->name . " from ".$productInfo->location->toString()." to ".$transferDest->name, + EVENT_TYPE_TRANSFER_PRODUCT + ))->setWebsocketResultCode(WS_RESULT_PRODUCT_FOUND)->createLog(); + + $fileLock->removeLock(); + if ($config["REVERT_SINGLE"]) { + $db->saveLog("Reverting back to Consume", true); + $db->setTransactionState(STATE_CONSUME); + } + return $output; default: throw new Exception("Unknown state"); } @@ -493,16 +599,16 @@ function processKnownBarcode(GrocyProduct $productInfo, string $barcode, bool $w /** * Function for generating the