Merge branch 'Part-DB:master' into master

This commit is contained in:
Marc 2025-02-08 19:04:35 +01:00 committed by GitHub
commit 964df4b2df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
160 changed files with 7655 additions and 5815 deletions

3
.env
View file

@ -143,7 +143,8 @@ PROVIDER_TME_CURRENCY=EUR
PROVIDER_TME_LANGUAGE=en PROVIDER_TME_LANGUAGE=en
# The country to get results for # The country to get results for
PROVIDER_TME_COUNTRY=DE PROVIDER_TME_COUNTRY=DE
# Set this to 1 to get gross prices (including VAT) instead of net prices # [DEPRECATED] Set this to 1 to get gross prices (including VAT) instead of net prices
# With private API keys, this option cannot be used anymore is ignored by Part-DB. The VAT inclusion depends on your TME account settings.
PROVIDER_TME_GET_GROSS_PRICES=1 PROVIDER_TME_GET_GROSS_PRICES=1
# Octopart / Nexar Provider: # Octopart / Nexar Provider:

0
.env.dev Normal file
View file

View file

@ -18,7 +18,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
php-versions: [ '8.1', '8.2', '8.3' ] php-versions: [ '8.1', '8.2', '8.3', '8.4' ]
db-type: [ 'mysql', 'sqlite', 'postgres' ] db-type: [ 'mysql', 'sqlite', 'postgres' ]
env: env:
@ -126,7 +126,7 @@ jobs:
run: ./bin/phpunit --coverage-clover=coverage.xml run: ./bin/phpunit --coverage-clover=coverage.xml
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v4 uses: codecov/codecov-action@v5
with: with:
env_vars: PHP_VERSION,DB_TYPE env_vars: PHP_VERSION,DB_TYPE
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}

View file

@ -1,5 +1,5 @@
ARG BASE_IMAGE=debian:bookworm-slim ARG BASE_IMAGE=debian:bookworm-slim
ARG PHP_VERSION=8.2 ARG PHP_VERSION=8.3
FROM ${BASE_IMAGE} AS base FROM ${BASE_IMAGE} AS base
ARG PHP_VERSION ARG PHP_VERSION
@ -125,6 +125,7 @@ upload_max_filesize=256M
post_max_size=300M post_max_size=300M
opcache.preload_user=www-data opcache.preload_user=www-data
opcache.preload=/var/www/html/config/preload.php opcache.preload=/var/www/html/config/preload.php
log_limit=8096
EOF EOF
COPY ./.docker/symfony.conf /etc/apache2/sites-available/symfony.conf COPY ./.docker/symfony.conf /etc/apache2/sites-available/symfony.conf

View file

@ -1 +1 @@
1.14.3 1.15.2

View file

@ -53,6 +53,7 @@ export default class extends Controller {
const config = { const config = {
language: language, language: language,
licenseKey: "GPL",
} }
const watchdog = new EditorWatchdog(); const watchdog = new EditorWatchdog();

View file

@ -20,7 +20,7 @@
import {Controller} from "@hotwired/stimulus"; import {Controller} from "@hotwired/stimulus";
//import * as ZXing from "@zxing/library"; //import * as ZXing from "@zxing/library";
import {Html5QrcodeScanner, Html5Qrcode} from "html5-qrcode"; import {Html5QrcodeScanner, Html5Qrcode} from "@part-db/html5-qrcode";
/* stimulusFetch: 'lazy' */ /* stimulusFetch: 'lazy' */
export default class extends Controller { export default class extends Controller {
@ -50,7 +50,7 @@ export default class extends Controller {
}); });
this._scanner = new Html5QrcodeScanner(this.element.id, { this._scanner = new Html5QrcodeScanner(this.element.id, {
fps: 2, fps: 10,
qrbox: qrboxFunction, qrbox: qrboxFunction,
experimentalFeatures: { experimentalFeatures: {
//This option improves reading quality on android chrome //This option improves reading quality on android chrome
@ -61,6 +61,11 @@ export default class extends Controller {
this._scanner.render(this.onScanSuccess.bind(this)); this._scanner.render(this.onScanSuccess.bind(this));
} }
disconnect() {
this._scanner.pause();
this._scanner.clear();
}
onScanSuccess(decodedText, decodedResult) { onScanSuccess(decodedText, decodedResult) {
//Put our decoded Text into the input box //Put our decoded Text into the input box
document.getElementById('scan_dialog_input').value = decodedText; document.getElementById('scan_dialog_input').value = decodedText;

View file

@ -24,9 +24,8 @@
/** Should be the same settings, as in label_style.css */ /** Should be the same settings, as in label_style.css */
.ck-html-label .ck-content { .ck-html-label .ck-content {
font-family: "DejaVu Sans Mono", monospace; font-family: "DejaVu Sans Mono", monospace;
font-size: 12px; font-size: 12pt;
line-height: 1.0; line-height: 1.0;
font-size-adjust: 1.5;
} }
.ck-html-label .ck-content p { .ck-html-label .ck-content p {

View file

@ -44,4 +44,18 @@ import "./register_events";
import "./tristate_checkboxes"; import "./tristate_checkboxes";
//Define jquery globally //Define jquery globally
window.$ = window.jQuery = require("jquery") window.$ = window.jQuery = require("jquery");
//Use the local WASM file for the ZXing library
import {
setZXingModuleOverrides,
} from "barcode-detector/pure";
import wasmFile from "../../node_modules/zxing-wasm/dist/reader/zxing_reader.wasm";
setZXingModuleOverrides({
locateFile: (path, prefix) => {
if (path.endsWith(".wasm")) {
return wasmFile;
}
return prefix + path;
},
});

View file

@ -4,6 +4,10 @@
use App\Kernel; use App\Kernel;
use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\Console\Application;
if (!is_dir(dirname(__DIR__).'/vendor')) {
throw new LogicException('Dependencies are missing. Try running "composer install".');
}
//Increase xdebug.max_nesting_level to 1000 if required (see issue #411) //Increase xdebug.max_nesting_level to 1000 if required (see issue #411)
//Check if xdebug extension is active, and xdebug.max_nesting_level is set to 256 or lower //Check if xdebug extension is active, and xdebug.max_nesting_level is set to 256 or lower
if (extension_loaded('xdebug') && ((int) ini_get('xdebug.max_nesting_level')) <= 256) { if (extension_loaded('xdebug') && ((int) ini_get('xdebug.max_nesting_level')) <= 256) {

View file

@ -1,4 +1,5 @@
{ {
"name": "part-db/part-db-server",
"type": "project", "type": "project",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"require": { "require": {
@ -16,7 +17,7 @@
"brick/math": "0.12.1 as 0.11.0", "brick/math": "0.12.1 as 0.11.0",
"composer/ca-bundle": "^1.3", "composer/ca-bundle": "^1.3",
"composer/package-versions-deprecated": "^1.11.99.5", "composer/package-versions-deprecated": "^1.11.99.5",
"doctrine/data-fixtures": "^1.6.6", "doctrine/data-fixtures": "^2.0.0",
"doctrine/dbal": "^4.0.0", "doctrine/dbal": "^4.0.0",
"doctrine/doctrine-bundle": "^2.0", "doctrine/doctrine-bundle": "^2.0",
"doctrine/doctrine-migrations-bundle": "^3.0", "doctrine/doctrine-migrations-bundle": "^3.0",
@ -39,12 +40,9 @@
"nelmio/cors-bundle": "^2.3", "nelmio/cors-bundle": "^2.3",
"nelmio/security-bundle": "^3.0", "nelmio/security-bundle": "^3.0",
"nyholm/psr7": "^1.1", "nyholm/psr7": "^1.1",
"ocramius/proxy-manager": "2.2.*", "omines/datatables-bundle": "^0.9.1",
"omines/datatables-bundle": "^0.8.0",
"paragonie/sodium_compat": "^1.21", "paragonie/sodium_compat": "^1.21",
"part-db/label-fonts": "^1.0", "part-db/label-fonts": "^1.0",
"phpdocumentor/reflection-docblock": "^5.2",
"phpstan/phpdoc-parser": "^1.23",
"runtime/frankenphp-symfony": "^0.2.0", "runtime/frankenphp-symfony": "^0.2.0",
"s9e/text-formatter": "^2.1", "s9e/text-formatter": "^2.1",
"scheb/2fa-backup-code": "^6.8.0", "scheb/2fa-backup-code": "^6.8.0",
@ -69,7 +67,6 @@
"symfony/process": "6.4.*", "symfony/process": "6.4.*",
"symfony/property-access": "6.4.*", "symfony/property-access": "6.4.*",
"symfony/property-info": "6.4.*", "symfony/property-info": "6.4.*",
"symfony/proxy-manager-bridge": "6.4.*",
"symfony/rate-limiter": "6.4.*", "symfony/rate-limiter": "6.4.*",
"symfony/runtime": "6.4.*", "symfony/runtime": "6.4.*",
"symfony/security-bundle": "6.4.*", "symfony/security-bundle": "6.4.*",
@ -91,21 +88,20 @@
"twig/intl-extra": "^3.8", "twig/intl-extra": "^3.8",
"twig/markdown-extra": "^3.8", "twig/markdown-extra": "^3.8",
"twig/string-extra": "^3.8", "twig/string-extra": "^3.8",
"web-auth/webauthn-symfony-bundle": "^4.0.0", "web-auth/webauthn-symfony-bundle": "^4.0.0"
"webmozart/assert": "^1.4"
}, },
"require-dev": { "require-dev": {
"dama/doctrine-test-bundle": "^v8.0.0", "dama/doctrine-test-bundle": "^v8.0.0",
"doctrine/doctrine-fixtures-bundle": "^3.2", "doctrine/doctrine-fixtures-bundle": "^4.0.0",
"ekino/phpstan-banned-code": "^v1.0.0", "ekino/phpstan-banned-code": "^v3.0.0",
"jbtronics/translation-editor-bundle": "^1.0", "jbtronics/translation-editor-bundle": "^1.0",
"phpstan/extension-installer": "^1.0", "phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "^1.4.7", "phpstan/phpstan": "^2.0.4",
"phpstan/phpstan-doctrine": "^1.2.11", "phpstan/phpstan-doctrine": "^2.0.1",
"phpstan/phpstan-strict-rules": "^1.5", "phpstan/phpstan-strict-rules": "^2.0.1",
"phpstan/phpstan-symfony": "^1.1.7", "phpstan/phpstan-symfony": "^2.0.0",
"phpunit/phpunit": "^9.5", "phpunit/phpunit": "^9.5",
"rector/rector": "^1.1.1", "rector/rector": "^2.0.4",
"roave/security-advisories": "dev-latest", "roave/security-advisories": "dev-latest",
"symfony/browser-kit": "6.4.*", "symfony/browser-kit": "6.4.*",
"symfony/css-selector": "6.4.*", "symfony/css-selector": "6.4.*",

2159
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -8,7 +8,7 @@ datatables:
# Set options, as documented at https://datatables.net/reference/option/ # Set options, as documented at https://datatables.net/reference/option/
options: options:
lengthMenu : [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]] lengthMenu : [[10, 25, 50, 100], [10, 25, 50, 100]] # We add the "All" option, when part tables are generated
pageLength: '%partdb.table.default_page_size%' # Set to -1 to disable pagination (i.e. show all rows) by default pageLength: '%partdb.table.default_page_size%' # Set to -1 to disable pagination (i.e. show all rows) by default
dom: " <'row' <'col mb-2 input-group flex-nowrap' B l > <'col-auto mb-2' < p >>> dom: " <'row' <'col mb-2 input-group flex-nowrap' B l > <'col-auto mb-2' < p >>>
<'card' <'card'

View file

@ -57,6 +57,7 @@ doctrine:
field2: App\Doctrine\Functions\Field2 field2: App\Doctrine\Functions\Field2
natsort: App\Doctrine\Functions\Natsort natsort: App\Doctrine\Functions\Natsort
array_position: App\Doctrine\Functions\ArrayPosition array_position: App\Doctrine\Functions\ArrayPosition
ilike: App\Doctrine\Functions\ILike
when@test: when@test:
doctrine: doctrine:

View file

@ -50,7 +50,6 @@ when@prod:
type: stream type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log" path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug level: debug
formatter: monolog.formatter.json
console: console:
type: console type: console
process_psr_3_messages: false process_psr_3_messages: false
@ -74,7 +73,6 @@ when@docker:
type: stream type: stream
path: "php://stderr" path: "php://stderr"
level: debug level: debug
formatter: monolog.formatter.json
console: console:
type: console type: console
process_psr_3_messages: false process_psr_3_messages: false

View file

@ -51,12 +51,16 @@ nelmio_security:
img-src: img-src:
- '*' - '*'
- 'data:' - 'data:'
# Required for be able to load pictures in the QR code scanner
- 'blob:'
style-src: style-src:
- 'self' - 'self'
- 'unsafe-inline' - 'unsafe-inline'
- 'data:' - 'data:'
script-src: script-src:
- 'self' - 'self'
# Required for loading the Wasm for the barcode scanner:
- 'wasm-unsafe-eval'
object-src: object-src:
- 'self' - 'self'
- 'data:' - 'data:'

View file

@ -23,7 +23,7 @@ each other so that it does not matter which one of your 1000 things of Part you
A part entity has many fields, which can be used to describe it better. Most of the fields are optional: A part entity has many fields, which can be used to describe it better. Most of the fields are optional:
* **Name** (Required): The name of the part or how you want to call it. This could be a manufacturer-provided name, or a * **Name** (Required): The name of the part or how you want to call it. This could be a manufacturer-provided name, or a
name you thought of yourself. The name have to be unique in a single category. name you thought of yourself. Each name needs to be unique and must exist in a single category.
* **Description**: A short (single-line) description of what this part is/does. For longer information, you should use * **Description**: A short (single-line) description of what this part is/does. For longer information, you should use
the comment field or the specifications the comment field or the specifications
* **Category** (Required): The category (see there) to which this part belongs to. * **Category** (Required): The category (see there) to which this part belongs to.
@ -239,4 +239,4 @@ replaced with data for the actual thing.
You do not have to define a label profile to generate labels (you can just set the settings on the fly in the label You do not have to define a label profile to generate labels (you can just set the settings on the fly in the label
dialog), however, if you want to generate many labels, it is recommended to save the settings as a label profile, to save dialog), however, if you want to generate many labels, it is recommended to save the settings as a label profile, to save
it for later usage. This ensures that all generated labels look the same. it for later usage. This ensures that all generated labels look the same.

View file

@ -150,9 +150,9 @@ In the `serverVersion` parameter you can specify the version of the PostgreSQL s
The `charset` parameter specify the character set of the database. It should be set to `utf8` to ensure that all characters are stored correctly. The `charset` parameter specify the character set of the database. It should be set to `utf8` to ensure that all characters are stored correctly.
If you want to use a unix socket for the connection instead of a TCP connnection, you can specify the socket path in the `unix_socket` parameter. If you want to use a unix socket for the connection instead of a TCP connnection, you can specify the socket path in the `host` parameter.
```shell ```shell
DATABASE_URL="postgresql://db_user:db_password@localhost/db_name?serverVersion=12.19&charset=utf8&unix_socket=/var/run/postgresql/.s.PGSQL.5432" DATABASE_URL="postgresql://db_user@localhost/db_name?serverVersion=16.6&charset=utf8&host=/var/run/postgresql"
``` ```

View file

@ -6,4 +6,6 @@ has_children: true
--- ---
# Installation # Installation
Below you can find some guides to install Part-DB. Below you can find some guides to install Part-DB.
For the hobbyists without much experience, we recommend the docker installation or direct installation on debian.

View file

@ -0,0 +1,42 @@
---
title: Kubernetes / Helm
layout: default
parent: Installation
nav_order: 5
---
# Kubernetes / Helm Charts
If you are using Kubernetes, you can use the [helm charts](https://helm.sh/) provided in this [repository](https://github.com/Part-DB/helm-charts).
## Usage
[Helm](https://helm.sh) must be installed to use the charts. Please refer to
Helm's [documentation](https://helm.sh/docs) to get started.
Once Helm has been set up correctly, add the repo as follows:
`helm repo add part-db https://part-db.github.io/helm-charts`
If you had already added this repo earlier, run `helm repo update` to retrieve
the latest versions of the packages. You can then run `helm search repo
part-db` to see the charts.
To install the part-db chart:
helm install my-part-db part-db/part-db
To uninstall the chart:
helm delete my-part-db
This repository is also available at [ArtifactHUB](https://artifacthub.io/packages/search?repo=part-db).
## Configuration
See the README in the [chart directory](https://github.com/Part-DB/helm-charts/tree/main/charts/part-db) for more
information on the available configuration options.
## Bugreports
If you find issues related to the helm charts, please open an issue in the [helm-charts repository](https://github.com/Part-DB/helm-charts).

View file

@ -0,0 +1,31 @@
---
title: Proxmox VE LXC
layout: default
parent: Installation
nav_order: 6
---
# Proxmox VE LXC
{: .warning }
> The proxmox VE LXC script for Part-DB is developed and maintained by [Proxmox VE Helper-Scripts](https://community-scripts.github.io/ProxmoxVE/)
> and not by the Part-DB developers. Keep in mind that the script is not officially supported by the Part-DB developers.
If you are using Proxmox VE you can use the scripts provided by [Proxmox VE Helper-Scripts community](https://community-scripts.github.io/ProxmoxVE/scripts?id=part-db)
to easily install Part-DB in a LXC container.
## Usage
To create a new LXC container with Part-DB, you can use the following command in the Proxmox VE shell:
```bash
bash -c "$(wget -qLO - https://github.com/community-scripts/ProxmoxVE/raw/main/ct/part-db.sh)"
```
The same command can be used to update an existing Part-DB container.
See the [helper script website](https://community-scripts.github.io/ProxmoxVE/scripts?id=part-db) for more information.
## Bugreports
If you find issues related to the proxmox VE LXC script, please open an issue in the [Proxmox VE Helper-Scripts repository](https://github.com/community-scripts/ProxmoxVE).

View file

@ -107,7 +107,7 @@ The following env configuration options are available:
default: `EUR`). If an offer is only available in a certain currency, default: `EUR`). If an offer is only available in a certain currency,
Part-DB will save the prices in their native currency, and you can use Part-DB currency conversion feature to convert Part-DB will save the prices in their native currency, and you can use Part-DB currency conversion feature to convert
it to your preferred currency. it to your preferred currency.
* `PROVIDER_OCOTPART_COUNTRY`: The country you want to get prices in if available (optional, 2 letter ISO-code, * `PROVIDER_OCTOPART_COUNTRY`: The country you want to get prices in if available (optional, 2 letter ISO-code,
default: `DE`). To get the correct prices, you have to set this and the currency setting to the correct value. default: `DE`). To get the correct prices, you have to set this and the currency setting to the correct value.
* `PROVIDER_OCTOPART_SEARCH_LIMIT`: The maximum number of results to return per search (optional, default: `10`). This * `PROVIDER_OCTOPART_SEARCH_LIMIT`: The maximum number of results to return per search (optional, default: `10`). This
affects how quickly your monthly limit is used up. affects how quickly your monthly limit is used up.
@ -127,6 +127,9 @@ You must create an organization there and create a "Production app". Most settin
grant access to the "Product Information" API. grant access to the "Product Information" API.
You will get a Client ID and a Client Secret, which you have to put in the Part-DB env configuration (see below). You will get a Client ID and a Client Secret, which you have to put in the Part-DB env configuration (see below).
**Attention**: Currently only the "Product Information V3 (Deprecated)" is supported by Part-DB.
Using "Product Information V4" will not work.
The following env configuration options are available: The following env configuration options are available:
* `PROVIDER_DIGIKEY_CLIENT_ID`: The client ID you got from Digi-Key (mandatory) * `PROVIDER_DIGIKEY_CLIENT_ID`: The client ID you got from Digi-Key (mandatory)

View file

@ -117,6 +117,6 @@ For a German keyboard layout, replace `[` with `0`, and `]` with `´`.
| Key | Character | | Key | Character |
|--------------------------------|--------------------| |--------------------------------|--------------------|
| **Alt + [** (code 219) | © (Copyright char) | | **Alt + [** (code 219) | © (Copyright char) |
| **Alt + Shift + [** (code 219) | (Registered char) | | **Alt + Shift + [** (code 219) | ® (Registered char) |
| **Alt + ]** (code 221) | ™ (Trademark char) | | **Alt + ]** (code 221) | ™ (Trademark char) |
| **Alt + Shift + ]** (code 221) | (Degree char) | | **Alt + Shift + ]** (code 221) | ° (Degree char) |

View file

@ -9,7 +9,7 @@
"@symfony/stimulus-bridge": "^3.2.0", "@symfony/stimulus-bridge": "^3.2.0",
"@symfony/ux-translator": "file:vendor/symfony/ux-translator/assets", "@symfony/ux-translator": "file:vendor/symfony/ux-translator/assets",
"@symfony/ux-turbo": "file:vendor/symfony/ux-turbo/assets", "@symfony/ux-turbo": "file:vendor/symfony/ux-turbo/assets",
"@symfony/webpack-encore": "^4.1.0", "@symfony/webpack-encore": "^5.0.0",
"bootstrap": "^5.1.3", "bootstrap": "^5.1.3",
"core-js": "^3.23.0", "core-js": "^3.23.0",
"intl-messageformat": "^10.2.5", "intl-messageformat": "^10.2.5",
@ -18,7 +18,7 @@
"regenerator-runtime": "^0.13.9", "regenerator-runtime": "^0.13.9",
"webpack": "^5.74.0", "webpack": "^5.74.0",
"webpack-bundle-analyzer": "^4.3.0", "webpack-bundle-analyzer": "^4.3.0",
"webpack-cli": "^4.10.0", "webpack-cli": "^5.1.0",
"webpack-notifier": "^1.15.0" "webpack-notifier": "^1.15.0"
}, },
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
@ -33,50 +33,52 @@
"@algolia/autocomplete-js": "^1.17.0", "@algolia/autocomplete-js": "^1.17.0",
"@algolia/autocomplete-plugin-recent-searches": "^1.17.0", "@algolia/autocomplete-plugin-recent-searches": "^1.17.0",
"@algolia/autocomplete-theme-classic": "^1.17.0", "@algolia/autocomplete-theme-classic": "^1.17.0",
"@ckeditor/ckeditor5-alignment": "^41.0.0", "@ckeditor/ckeditor5-alignment": "^44.0.0",
"@ckeditor/ckeditor5-autoformat": "^41.0.0", "@ckeditor/ckeditor5-autoformat": "^44.0.0",
"@ckeditor/ckeditor5-basic-styles": "^41.0.0", "@ckeditor/ckeditor5-basic-styles": "^44.0.0",
"@ckeditor/ckeditor5-block-quote": "^41.0.0", "@ckeditor/ckeditor5-block-quote": "^44.0.0",
"@ckeditor/ckeditor5-code-block": "^41.0.0", "@ckeditor/ckeditor5-code-block": "^44.0.0",
"@ckeditor/ckeditor5-dev-translations": "^39.1.0", "@ckeditor/ckeditor5-dev-translations": "^43.0.1",
"@ckeditor/ckeditor5-dev-utils": "^39.1.0", "@ckeditor/ckeditor5-dev-utils": "^43.0.1",
"@ckeditor/ckeditor5-editor-classic": "^41.0.0", "@ckeditor/ckeditor5-editor-classic": "^44.0.0",
"@ckeditor/ckeditor5-essentials": "^41.0.0", "@ckeditor/ckeditor5-essentials": "^44.0.0",
"@ckeditor/ckeditor5-find-and-replace": "^41.0.0", "@ckeditor/ckeditor5-find-and-replace": "^44.0.0",
"@ckeditor/ckeditor5-font": "^41.0.0", "@ckeditor/ckeditor5-font": "^44.0.0",
"@ckeditor/ckeditor5-heading": "^41.0.0", "@ckeditor/ckeditor5-heading": "^44.0.0",
"@ckeditor/ckeditor5-highlight": "^41.0.0", "@ckeditor/ckeditor5-highlight": "^44.0.0",
"@ckeditor/ckeditor5-horizontal-line": "^41.0.0", "@ckeditor/ckeditor5-horizontal-line": "^44.0.0",
"@ckeditor/ckeditor5-html-embed": "^41.0.0", "@ckeditor/ckeditor5-html-embed": "^44.0.0",
"@ckeditor/ckeditor5-html-support": "^41.0.0", "@ckeditor/ckeditor5-html-support": "^44.0.0",
"@ckeditor/ckeditor5-image": "^41.0.0", "@ckeditor/ckeditor5-image": "^44.0.0",
"@ckeditor/ckeditor5-indent": "^41.0.0", "@ckeditor/ckeditor5-indent": "^44.0.0",
"@ckeditor/ckeditor5-link": "^41.0.0", "@ckeditor/ckeditor5-link": "^44.0.0",
"@ckeditor/ckeditor5-list": "^41.0.0", "@ckeditor/ckeditor5-list": "^44.0.0",
"@ckeditor/ckeditor5-markdown-gfm": "^41.0.0", "@ckeditor/ckeditor5-markdown-gfm": "^44.0.0",
"@ckeditor/ckeditor5-media-embed": "^41.0.0", "@ckeditor/ckeditor5-media-embed": "^44.0.0",
"@ckeditor/ckeditor5-paragraph": "^41.0.0", "@ckeditor/ckeditor5-paragraph": "^44.0.0",
"@ckeditor/ckeditor5-paste-from-office": "^41.0.0", "@ckeditor/ckeditor5-paste-from-office": "^44.0.0",
"@ckeditor/ckeditor5-remove-format": "^41.0.0", "@ckeditor/ckeditor5-remove-format": "^44.0.0",
"@ckeditor/ckeditor5-source-editing": "^41.0.0", "@ckeditor/ckeditor5-source-editing": "^44.0.0",
"@ckeditor/ckeditor5-special-characters": "^41.0.0", "@ckeditor/ckeditor5-special-characters": "^44.0.0",
"@ckeditor/ckeditor5-table": "^41.0.0", "@ckeditor/ckeditor5-table": "^44.0.0",
"@ckeditor/ckeditor5-theme-lark": "^41.0.0", "@ckeditor/ckeditor5-theme-lark": "^44.0.0",
"@ckeditor/ckeditor5-upload": "^41.0.0", "@ckeditor/ckeditor5-upload": "^44.0.0",
"@ckeditor/ckeditor5-watchdog": "^41.0.0", "@ckeditor/ckeditor5-watchdog": "^44.0.0",
"@ckeditor/ckeditor5-word-count": "^41.0.0", "@ckeditor/ckeditor5-word-count": "^44.0.0",
"@jbtronics/bs-treeview": "^1.0.1", "@jbtronics/bs-treeview": "^1.0.1",
"@part-db/html5-qrcode": "^3.1.0",
"@zxcvbn-ts/core": "^3.0.2", "@zxcvbn-ts/core": "^3.0.2",
"@zxcvbn-ts/language-common": "^3.0.3", "@zxcvbn-ts/language-common": "^3.0.3",
"@zxcvbn-ts/language-de": "^3.0.1", "@zxcvbn-ts/language-de": "^3.0.1",
"@zxcvbn-ts/language-en": "^3.0.1", "@zxcvbn-ts/language-en": "^3.0.1",
"@zxcvbn-ts/language-fr": "^3.0.1", "@zxcvbn-ts/language-fr": "^3.0.1",
"@zxcvbn-ts/language-ja": "^3.0.1", "@zxcvbn-ts/language-ja": "^3.0.1",
"barcode-detector": "^2.3.1",
"bootbox": "^6.0.0", "bootbox": "^6.0.0",
"bootswatch": "^5.1.3", "bootswatch": "^5.1.3",
"bs-custom-file-input": "^1.3.4", "bs-custom-file-input": "^1.3.4",
"clipboard": "^2.0.4", "clipboard": "^2.0.4",
"compression-webpack-plugin": "^10.0.0", "compression-webpack-plugin": "^11.1.0",
"datatables.net": "^2.0.0", "datatables.net": "^2.0.0",
"datatables.net-bs5": "^2.0.0", "datatables.net-bs5": "^2.0.0",
"datatables.net-buttons-bs5": "^3.0.0", "datatables.net-buttons-bs5": "^3.0.0",
@ -86,18 +88,17 @@
"datatables.net-select-bs5": "^2.0.0", "datatables.net-select-bs5": "^2.0.0",
"dompurify": "^3.0.3", "dompurify": "^3.0.3",
"emoji.json": "^15.0.0", "emoji.json": "^15.0.0",
"exports-loader": "^3.0.0", "exports-loader": "^5.0.0",
"html5-qrcode": "^2.2.1",
"json-formatter-js": "^2.3.4", "json-formatter-js": "^2.3.4",
"jszip": "^3.2.0", "jszip": "^3.2.0",
"katex": "^0.16.0", "katex": "^0.16.0",
"marked": "^12.0.0", "marked": "^15.0.4",
"marked-gfm-heading-id": "^3.0.4", "marked-gfm-heading-id": "^4.1.1",
"marked-mangle": "^1.0.1", "marked-mangle": "^1.0.1",
"pdfmake": "^0.2.2", "pdfmake": "^0.2.2",
"stimulus-use": "^0.52.0", "stimulus-use": "^0.52.0",
"tom-select": "^2.1.0", "tom-select": "^2.1.0",
"ts-loader": "^9.2.6", "ts-loader": "^9.2.6",
"typescript": "^4.0.2" "typescript": "^5.7.2"
} }
} }

View file

@ -20,7 +20,7 @@ parameters:
treatPhpDocTypesAsCertain: false treatPhpDocTypesAsCertain: false
symfony: symfony:
container_xml_path: '%rootDir%/../../../var/cache/dev/App_KernelDevDebugContainer.xml' containerXmlPath: '%rootDir%/../../../var/cache/dev/App_KernelDevDebugContainer.xml'
doctrine: doctrine:
objectManagerLoader: tests/object-manager.php objectManagerLoader: tests/object-manager.php
@ -30,11 +30,6 @@ parameters:
checkFunctionNameCase: false checkFunctionNameCase: false
checkAlwaysTrueInstanceof: false
checkAlwaysTrueCheckTypeFunctionCall: false
checkAlwaysTrueStrictComparison: false
reportAlwaysTrueInLastCondition: false
reportMaybesInPropertyPhpDocTypes: false reportMaybesInPropertyPhpDocTypes: false
reportMaybesInMethodSignatures: false reportMaybesInMethodSignatures: false
@ -43,14 +38,14 @@ parameters:
booleansInConditions: false booleansInConditions: false
uselessCast: false uselessCast: false
requireParentConstructorCall: true requireParentConstructorCall: true
disallowedConstructs: false
overwriteVariablesWithLoop: false overwriteVariablesWithLoop: false
closureUsesThis: false closureUsesThis: false
matchingInheritedMethodNames: true matchingInheritedMethodNames: true
numericOperandsInArithmeticOperators: true numericOperandsInArithmeticOperators: true
strictCalls: true
switchConditionsMatchingType: false switchConditionsMatchingType: false
noVariableVariables: false noVariableVariables: false
disallowedEmpty: false
disallowedShortTernary: false
ignoreErrors: ignoreErrors:
# Ignore errors caused by complex mapping with AbstractStructuralDBElement # Ignore errors caused by complex mapping with AbstractStructuralDBElement
@ -62,4 +57,7 @@ parameters:
- '#Part::getParameters\(\) should return .*AbstractParameter#' - '#Part::getParameters\(\) should return .*AbstractParameter#'
# Ignore doctrine type mapping mismatch # Ignore doctrine type mapping mismatch
- '#Property .* type mapping mismatch: property can contain .* but database expects .*#' - '#Property .* type mapping mismatch: property can contain .* but database expects .*#'
# Ignore error of unused WithPermPresetsTrait, as it is used in the migrations which are not analyzed by Phpstan
- '#Trait App\\Migration\\WithPermPresetsTrait is used zero times and is not analysed#'

View file

@ -43,9 +43,9 @@ class AddDocumentedAPIPropertiesJSONSchemaFactory implements SchemaFactoryInterf
string $className, string $className,
string $format = 'json', string $format = 'json',
string $type = Schema::TYPE_OUTPUT, string $type = Schema::TYPE_OUTPUT,
Operation $operation = null, ?Operation $operation = null,
Schema $schema = null, ?Schema $schema = null,
array $serializerContext = null, ?array $serializerContext = null,
bool $forceCollection = false bool $forceCollection = false
): Schema { ): Schema {

View file

@ -37,7 +37,7 @@ class EntityFilter extends AbstractFilter
public function __construct( public function __construct(
ManagerRegistry $managerRegistry, ManagerRegistry $managerRegistry,
private readonly EntityFilterHelper $filter_helper, private readonly EntityFilterHelper $filter_helper,
LoggerInterface $logger = null, ?LoggerInterface $logger = null,
?array $properties = null, ?array $properties = null,
?NameConverterInterface $nameConverter = null ?NameConverterInterface $nameConverter = null
) { ) {
@ -50,7 +50,7 @@ class EntityFilter extends AbstractFilter
QueryBuilder $queryBuilder, QueryBuilder $queryBuilder,
QueryNameGeneratorInterface $queryNameGenerator, QueryNameGeneratorInterface $queryNameGenerator,
string $resourceClass, string $resourceClass,
Operation $operation = null, ?Operation $operation = null,
array $context = [] array $context = []
): void { ): void {
if ( if (

View file

@ -38,7 +38,7 @@ final class LikeFilter extends AbstractFilter
QueryBuilder $queryBuilder, QueryBuilder $queryBuilder,
QueryNameGeneratorInterface $queryNameGenerator, QueryNameGeneratorInterface $queryNameGenerator,
string $resourceClass, string $resourceClass,
Operation $operation = null, ?Operation $operation = null,
array $context = [] array $context = []
): void { ): void {
// Otherwise filter is applied to order and page as well // Otherwise filter is applied to order and page as well
@ -50,7 +50,7 @@ final class LikeFilter extends AbstractFilter
} }
$parameterName = $queryNameGenerator->generateParameterName($property); // Generate a unique parameter name to avoid collisions with other filters $parameterName = $queryNameGenerator->generateParameterName($property); // Generate a unique parameter name to avoid collisions with other filters
$queryBuilder $queryBuilder
->andWhere(sprintf('o.%s LIKE :%s', $property, $parameterName)) ->andWhere(sprintf('ILIKE(o.%s, :%s) = TRUE', $property, $parameterName))
->setParameter($parameterName, $value); ->setParameter($parameterName, $value);
} }

View file

@ -38,7 +38,7 @@ class PartStoragelocationFilter extends AbstractFilter
public function __construct( public function __construct(
ManagerRegistry $managerRegistry, ManagerRegistry $managerRegistry,
private readonly EntityFilterHelper $filter_helper, private readonly EntityFilterHelper $filter_helper,
LoggerInterface $logger = null, ?LoggerInterface $logger = null,
?array $properties = null, ?array $properties = null,
?NameConverterInterface $nameConverter = null ?NameConverterInterface $nameConverter = null
) { ) {
@ -51,7 +51,7 @@ class PartStoragelocationFilter extends AbstractFilter
QueryBuilder $queryBuilder, QueryBuilder $queryBuilder,
QueryNameGeneratorInterface $queryNameGenerator, QueryNameGeneratorInterface $queryNameGenerator,
string $resourceClass, string $resourceClass,
Operation $operation = null, ?Operation $operation = null,
array $context = [] array $context = []
): void { ): void {
//Do not check for mapping here, as we are using a virtual property //Do not check for mapping here, as we are using a virtual property

View file

@ -61,10 +61,10 @@ final class TagFilter extends AbstractFilter
$expr = $queryBuilder->expr(); $expr = $queryBuilder->expr();
$tmp = $expr->orX( $tmp = $expr->orX(
$expr->like('o.'.$property, ':' . $tag_identifier_prefix . '_1'), 'ILIKE(o.'.$property.', :' . $tag_identifier_prefix . '_1) = TRUE',
$expr->like('o.'.$property, ':' . $tag_identifier_prefix . '_2'), 'ILIKE(o.'.$property.', :' . $tag_identifier_prefix . '_2) = TRUE',
$expr->like('o.'.$property, ':' . $tag_identifier_prefix . '_3'), 'ILIKE(o.'.$property.', :' . $tag_identifier_prefix . '_3) = TRUE',
$expr->eq('o.'.$property, ':' . $tag_identifier_prefix . '_4'), 'ILIKE(o.'.$property.', :' . $tag_identifier_prefix . '_4) = TRUE',
); );
$queryBuilder->andWhere($tmp); $queryBuilder->andWhere($tmp);

View file

@ -79,7 +79,7 @@ class CheckRequirementsCommand extends Command
//Checking 32-bit system //Checking 32-bit system
if (PHP_INT_SIZE === 4) { if (PHP_INT_SIZE === 4) {
$io->warning('You are using a 32-bit system. You will have problems with working with dates after the year 2038, therefore a 64-bit system is recommended.'); $io->warning('You are using a 32-bit system. You will have problems with working with dates after the year 2038, therefore a 64-bit system is recommended.');
} elseif (PHP_INT_SIZE === 8) { } elseif (PHP_INT_SIZE === 8) { //@phpstan-ignore-line //PHP_INT_SIZE is always 4 or 8
if (!$only_issues) { if (!$only_issues) {
$io->success('You are using a 64-bit system.'); $io->success('You are using a 64-bit system.');
} }

View file

@ -79,6 +79,7 @@ class ConvertBBCodeCommand extends Command
/** /**
* Returns a list which entities and which properties need to be checked. * Returns a list which entities and which properties need to be checked.
* @return array<class-string<AbstractNamedDBElement>, string[]>
*/ */
protected function getTargetsLists(): array protected function getTargetsLists(): array
{ {
@ -109,7 +110,6 @@ class ConvertBBCodeCommand extends Command
$class $class
)); ));
//Determine which entities of this type we need to modify //Determine which entities of this type we need to modify
/** @var EntityRepository $repo */
$repo = $this->em->getRepository($class); $repo = $this->em->getRepository($class);
$qb = $repo->createQueryBuilder('e') $qb = $repo->createQueryBuilder('e')
->select('e'); ->select('e');

View file

@ -35,7 +35,7 @@ use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand('partdb:users:enable|partdb:user:enable', 'Enables/Disable the login of one or more users')] #[AsCommand('partdb:users:enable|partdb:user:enable', 'Enables/Disable the login of one or more users')]
class UserEnableCommand extends Command class UserEnableCommand extends Command
{ {
public function __construct(protected EntityManagerInterface $entityManager, string $name = null) public function __construct(protected EntityManagerInterface $entityManager, ?string $name = null)
{ {
parent::__construct($name); parent::__construct($name);
} }

View file

@ -206,12 +206,15 @@ class UsersPermissionsCommand extends Command
return '<fg=green>Allow</>'; return '<fg=green>Allow</>';
} elseif ($permission_value === false) { } elseif ($permission_value === false) {
return '<fg=red>Disallow</>'; return '<fg=red>Disallow</>';
} elseif ($permission_value === null && !$inherit) { }
// Permission value is null by this point
elseif (!$inherit) {
return '<fg=blue>Inherit</>'; return '<fg=blue>Inherit</>';
} elseif ($permission_value === null && $inherit) { } elseif ($inherit) {
return '<fg=red>Disallow (Inherited)</>'; return '<fg=red>Disallow (Inherited)</>';
} }
//@phpstan-ignore-next-line This line is never reached, but PHPstorm complains otherwise
return '???'; return '???';
} }
} }

View file

@ -221,7 +221,6 @@ abstract class BaseAdminController extends AbstractController
} }
} }
/** @var AbstractPartsContainingRepository $repo */
$repo = $this->entityManager->getRepository($this->entity_class); $repo = $this->entityManager->getRepository($this->entity_class);
return $this->render($this->twig_template, [ return $this->render($this->twig_template, [
@ -397,7 +396,7 @@ abstract class BaseAdminController extends AbstractController
{ {
if ($entity instanceof AbstractPartsContainingDBElement) { if ($entity instanceof AbstractPartsContainingDBElement) {
/** @var AbstractPartsContainingRepository $repo */ /** @var AbstractPartsContainingRepository $repo */
$repo = $this->entityManager->getRepository($this->entity_class); $repo = $this->entityManager->getRepository($this->entity_class); //@phpstan-ignore-line
if ($repo->getPartsCount($entity) > 0) { if ($repo->getPartsCount($entity) > 0) {
$this->addFlash('error', t('entity.delete.must_not_contain_parts', ['%PATH%' => $entity->getFullPath()])); $this->addFlash('error', t('entity.delete.must_not_contain_parts', ['%PATH%' => $entity->getFullPath()]));
@ -468,6 +467,11 @@ abstract class BaseAdminController extends AbstractController
$this->denyAccessUnlessGranted('read', $entity); $this->denyAccessUnlessGranted('read', $entity);
$entities = $em->getRepository($this->entity_class)->findAll(); $entities = $em->getRepository($this->entity_class)->findAll();
if (count($entities) === 0) {
$this->addFlash('error', 'entity.export.flash.error.no_entities');
return $this->redirectToRoute($this->route_base.'_new');
}
return $exporter->exportEntityFromRequest($entities, $request); return $exporter->exportEntityFromRequest($entities, $request);
} }

View file

@ -23,10 +23,13 @@ declare(strict_types=1);
namespace App\Controller; namespace App\Controller;
use App\Entity\Parts\Manufacturer;
use App\Entity\Parts\Part; use App\Entity\Parts\Part;
use App\Form\InfoProviderSystem\PartSearchType; use App\Form\InfoProviderSystem\PartSearchType;
use App\Services\InfoProviderSystem\ExistingPartFinder;
use App\Services\InfoProviderSystem\PartInfoRetriever; use App\Services\InfoProviderSystem\PartInfoRetriever;
use App\Services\InfoProviderSystem\ProviderRegistry; use App\Services\InfoProviderSystem\ProviderRegistry;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Bridge\Doctrine\Attribute\MapEntity; use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@ -42,7 +45,9 @@ class InfoProviderController extends AbstractController
{ {
public function __construct(private readonly ProviderRegistry $providerRegistry, public function __construct(private readonly ProviderRegistry $providerRegistry,
private readonly PartInfoRetriever $infoRetriever) private readonly PartInfoRetriever $infoRetriever,
private readonly ExistingPartFinder $existingPartFinder
)
{ {
} }
@ -72,21 +77,49 @@ class InfoProviderController extends AbstractController
//When we are updating a part, use its name as keyword, to make searching easier //When we are updating a part, use its name as keyword, to make searching easier
//However we can only do this, if the form was not submitted yet //However we can only do this, if the form was not submitted yet
if ($update_target !== null && !$form->isSubmitted()) { if ($update_target !== null && !$form->isSubmitted()) {
$form->get('keyword')->setData($update_target->getName()); //Use the provider reference if available, otherwise use the manufacturer product number
$keyword = $update_target->getProviderReference()->getProviderId() ?? $update_target->getManufacturerProductNumber();
//Or the name if both are not available
if ($keyword === "") {
$keyword = $update_target->getName();
}
$form->get('keyword')->setData($keyword);
//If we are updating a part, which already has a provider, preselect that provider in the form
if ($update_target->getProviderReference()->getProviderKey() !== null) {
try {
$form->get('providers')->setData([$this->providerRegistry->getProviderByKey($update_target->getProviderReference()->getProviderKey())]);
} catch (\InvalidArgumentException $e) {
//If the provider is not found, just ignore it
}
}
} }
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
$keyword = $form->get('keyword')->getData(); $keyword = $form->get('keyword')->getData();
$providers = $form->get('providers')->getData(); $providers = $form->get('providers')->getData();
$dtos = [];
try { try {
$results = $this->infoRetriever->searchByKeyword(keyword: $keyword, providers: $providers); $dtos = $this->infoRetriever->searchByKeyword(keyword: $keyword, providers: $providers);
} catch (ClientException $e) { } catch (ClientException $e) {
$this->addFlash('error', t('info_providers.search.error.client_exception')); $this->addFlash('error', t('info_providers.search.error.client_exception'));
$this->addFlash('error',$e->getMessage()); $this->addFlash('error',$e->getMessage());
//Log the exception //Log the exception
$exceptionLogger->error('Error during info provider search: ' . $e->getMessage(), ['exception' => $e]); $exceptionLogger->error('Error during info provider search: ' . $e->getMessage(), ['exception' => $e]);
} }
// modify the array to an array of arrays that has a field for a matching local Part
// the advantage to use that format even when we don't look for local parts is that we
// always work with the same interface
$results = array_map(function ($result) {return ['dto' => $result, 'localPart' => null];}, $dtos);
if(!$update_target) {
foreach ($results as $index => $result) {
$results[$index]['localPart'] = $this->existingPartFinder->findFirstExisting($result['dto']);
}
}
} }
return $this->render('info_providers/search/part_search.html.twig', [ return $this->render('info_providers/search/part_search.html.twig', [

View file

@ -108,8 +108,31 @@ class LabelController extends AbstractController
$pdf_data = null; $pdf_data = null;
$filename = 'invalid.pdf'; $filename = 'invalid.pdf';
//Generate PDF either when the form is submitted and valid, or the form was not submit yet, and generate is set
if (($form->isSubmitted() && $form->isValid()) || ($generate && !$form->isSubmitted() && $profile instanceof LabelProfile)) { if (($form->isSubmitted() && $form->isValid()) || ($generate && !$form->isSubmitted() && $profile instanceof LabelProfile)) {
//Check if the label should be saved as profile
if ($form->get('save_profile')->isClicked() && $this->isGranted('@labels.create_profiles')) { //@phpstan-ignore-line Phpstan does not recognize the isClicked method
//Retrieve the profile name from the form
$new_name = $form->get('save_profile_name')->getData();
//ensure that the name is not empty
if ($new_name === '' || $new_name === null) {
$form->get('save_profile_name')->addError(new FormError($this->translator->trans('label_generator.profile_name_empty')));
goto render;
}
$profile = new LabelProfile();
$profile->setName($form->get('save_profile_name')->getData());
$profile->setOptions($form_options);
$this->em->persist($profile);
$this->em->flush();
$this->addFlash('success', 'label_generator.profile_saved');
return $this->redirectToRoute('label_dialog_profile', [
'profile' => $profile->getID(),
'target_id' => (string) $form->get('target_id')->getData()
]);
}
$target_id = (string) $form->get('target_id')->getData(); $target_id = (string) $form->get('target_id')->getData();
$targets = $this->findObjects($form_options->getSupportedElement(), $target_id); $targets = $this->findObjects($form_options->getSupportedElement(), $target_id);
if ($targets !== []) { if ($targets !== []) {
@ -132,6 +155,7 @@ class LabelController extends AbstractController
} }
} }
render:
return $this->render('label_system/dialog.html.twig', [ return $this->render('label_system/dialog.html.twig', [
'form' => $form, 'form' => $form,
'pdf_data' => $pdf_data, 'pdf_data' => $pdf_data,
@ -152,7 +176,7 @@ class LabelController extends AbstractController
{ {
$id_array = $this->rangeParser->parse($ids); $id_array = $this->rangeParser->parse($ids);
/** @var DBElementRepository $repo */ /** @var DBElementRepository<AbstractDBElement> $repo */
$repo = $this->em->getRepository($type->getEntityClass()); $repo = $this->em->getRepository($type->getEntityClass());
return $repo->getElementsFromIDArray($id_array); return $repo->getElementsFromIDArray($id_array);

View file

@ -229,6 +229,10 @@ class PartController extends AbstractController
$dto = $infoRetriever->getDetails($providerKey, $providerId); $dto = $infoRetriever->getDetails($providerKey, $providerId);
$new_part = $infoRetriever->dtoToPart($dto); $new_part = $infoRetriever->dtoToPart($dto);
if ($new_part->getCategory() === null || $new_part->getCategory()->getID() === null) {
$this->addFlash('warning', t("part.create_from_info_provider.no_category_yet"));
}
return $this->renderPartForm('new', $request, $new_part, [ return $this->renderPartForm('new', $request, $new_part, [
'info_provider_dto' => $dto, 'info_provider_dto' => $dto,
]); ]);

View file

@ -112,8 +112,9 @@ class PartImportExportController extends AbstractController
$ids = $request->query->get('ids', ''); $ids = $request->query->get('ids', '');
$parts = $this->partsTableActionHandler->idStringToArray($ids); $parts = $this->partsTableActionHandler->idStringToArray($ids);
if ($parts === []) { if (count($parts) === 0) {
throw new \RuntimeException('No parts found!'); $this->addFlash('error', 'entity.export.flash.error.no_entities');
return $this->redirectToRoute('homepage');
} }
//Ensure that we have access to the parts //Ensure that we have access to the parts

View file

@ -60,6 +60,7 @@ class PartListsController extends AbstractController
$ids = $request->request->get('ids'); $ids = $request->request->get('ids');
$action = $request->request->get('action'); $action = $request->request->get('action');
$target = $request->request->get('target'); $target = $request->request->get('target');
$redirectResponse = null;
if (!$this->isCsrfTokenValid('table_action', $request->request->get('_token'))) { if (!$this->isCsrfTokenValid('table_action', $request->request->get('_token'))) {
$this->addFlash('error', 'csfr_invalid'); $this->addFlash('error', 'csfr_invalid');
@ -80,7 +81,7 @@ class PartListsController extends AbstractController
} }
//If the action handler returned a response, we use it, otherwise we redirect back to the previous page. //If the action handler returned a response, we use it, otherwise we redirect back to the previous page.
if (isset($redirectResponse) && $redirectResponse instanceof Response) { if ($redirectResponse !== null) {
return $redirectResponse; return $redirectResponse;
} }
@ -131,7 +132,11 @@ class PartListsController extends AbstractController
$filterForm->handleRequest($formRequest); $filterForm->handleRequest($formRequest);
$table = $this->dataTableFactory->createFromType(PartsDataTable::class, array_merge(['filter' => $filter], $additional_table_vars)) $table = $this->dataTableFactory->createFromType(
PartsDataTable::class,
array_merge(['filter' => $filter], $additional_table_vars),
['lengthMenu' => PartsDataTable::LENGTH_MENU]
)
->handleRequest($request); ->handleRequest($request);
if ($table->isCallback()) { if ($table->isCallback()) {

View file

@ -42,10 +42,10 @@ declare(strict_types=1);
namespace App\Controller; namespace App\Controller;
use App\Form\LabelSystem\ScanDialogType; use App\Form\LabelSystem\ScanDialogType;
use App\Services\LabelSystem\Barcodes\BarcodeScanHelper; use App\Services\LabelSystem\BarcodeScanner\BarcodeRedirector;
use App\Services\LabelSystem\Barcodes\BarcodeRedirector; use App\Services\LabelSystem\BarcodeScanner\BarcodeScanHelper;
use App\Services\LabelSystem\Barcodes\BarcodeScanResult; use App\Services\LabelSystem\BarcodeScanner\BarcodeSourceType;
use App\Services\LabelSystem\Barcodes\BarcodeSourceType; use App\Services\LabelSystem\BarcodeScanner\LocalBarcodeScanResult;
use Doctrine\ORM\EntityNotFoundException; use Doctrine\ORM\EntityNotFoundException;
use InvalidArgumentException; use InvalidArgumentException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@ -77,13 +77,21 @@ class ScanController extends AbstractController
$mode = $form['mode']->getData(); $mode = $form['mode']->getData();
} }
$infoModeData = null;
if ($input !== null) { if ($input !== null) {
try { try {
$scan_result = $this->barcodeNormalizer->scanBarcodeContent($input, $mode ?? null); $scan_result = $this->barcodeNormalizer->scanBarcodeContent($input, $mode ?? null);
try { //Perform a redirect if the info mode is not enabled
return $this->redirect($this->barcodeParser->getRedirectURL($scan_result)); if (!$form['info_mode']->getData()) {
} catch (EntityNotFoundException) { try {
$this->addFlash('success', 'scan.qr_not_found'); return $this->redirect($this->barcodeParser->getRedirectURL($scan_result));
} catch (EntityNotFoundException) {
$this->addFlash('success', 'scan.qr_not_found');
}
} else { //Otherwise retrieve infoModeData
$infoModeData = $scan_result->getDecodedForInfoMode();
} }
} catch (InvalidArgumentException) { } catch (InvalidArgumentException) {
$this->addFlash('error', 'scan.format_unknown'); $this->addFlash('error', 'scan.format_unknown');
@ -92,6 +100,7 @@ class ScanController extends AbstractController
return $this->render('label_system/scanner/scanner.html.twig', [ return $this->render('label_system/scanner/scanner.html.twig', [
'form' => $form, 'form' => $form,
'infoModeData' => $infoModeData,
]); ]);
} }
@ -109,7 +118,7 @@ class ScanController extends AbstractController
throw new InvalidArgumentException('Unknown type: '.$type); throw new InvalidArgumentException('Unknown type: '.$type);
} }
//Construct the scan result manually, as we don't have a barcode here //Construct the scan result manually, as we don't have a barcode here
$scan_result = new BarcodeScanResult( $scan_result = new LocalBarcodeScanResult(
target_type: BarcodeScanHelper::QR_TYPE_MAP[$type], target_type: BarcodeScanHelper::QR_TYPE_MAP[$type],
target_id: $id, target_id: $id,
//The routes are only used on the internal generated QR codes //The routes are only used on the internal generated QR codes

View file

@ -22,6 +22,7 @@ declare(strict_types=1);
namespace App\Controller; namespace App\Controller;
use App\Entity\Parameters\AbstractParameter;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use App\Entity\Attachments\Attachment; use App\Entity\Attachments\Attachment;
use App\Entity\Parts\Category; use App\Entity\Parts\Category;
@ -92,7 +93,7 @@ class TypeaheadController extends AbstractController
/** /**
* This function map the parameter type to the class, so we can access its repository * This function map the parameter type to the class, so we can access its repository
* @return class-string * @return class-string<AbstractParameter>
*/ */
private function typeToParameterClass(string $type): string private function typeToParameterClass(string $type): string
{ {
@ -155,7 +156,7 @@ class TypeaheadController extends AbstractController
//Ensure user has the correct permissions //Ensure user has the correct permissions
$this->denyAccessUnlessGranted('read', $test_obj); $this->denyAccessUnlessGranted('read', $test_obj);
/** @var ParameterRepository $repository */ /** @var ParameterRepository<AbstractParameter> $repository */
$repository = $entityManager->getRepository($class); $repository = $entityManager->getRepository($class);
$data = $repository->autocompleteParamName($query); $data = $repository->autocompleteParamName($query);

View file

@ -240,7 +240,10 @@ class UserSettingsController extends AbstractController
$page_need_reload = true; $page_need_reload = true;
} }
/** @var Form $form We need a form implementation for the next calls */ if (!$form instanceof Form) {
throw new RuntimeException('Form is not an instance of Form, so we cannot retrieve the clicked button!');
}
//Remove the avatar attachment from the user if requested //Remove the avatar attachment from the user if requested
if ($form->getClickedButton() && 'remove_avatar' === $form->getClickedButton()->getName() && $user->getMasterPictureAttachment() instanceof Attachment) { if ($form->getClickedButton() && 'remove_avatar' === $form->getClickedButton()->getName() && $user->getMasterPictureAttachment() instanceof Attachment) {
$em->remove($user->getMasterPictureAttachment()); $em->remove($user->getMasterPictureAttachment());

View file

@ -41,7 +41,7 @@ class APITokenFixtures extends Fixture implements DependentFixtureInterface
public function load(ObjectManager $manager): void public function load(ObjectManager $manager): void
{ {
/** @var User $admin_user */ /** @var User $admin_user */
$admin_user = $this->getReference(UserFixtures::ADMIN); $admin_user = $this->getReference(UserFixtures::ADMIN, User::class);
$read_only_token = new ApiToken(); $read_only_token = new ApiToken();
$read_only_token->setUser($admin_user); $read_only_token->setUser($admin_user);

View file

@ -35,7 +35,7 @@ use Doctrine\Persistence\ObjectManager;
class LogEntryFixtures extends Fixture implements DependentFixtureInterface class LogEntryFixtures extends Fixture implements DependentFixtureInterface
{ {
public function load(ObjectManager $manager) public function load(ObjectManager $manager): void
{ {
$this->createCategoryEntries($manager); $this->createCategoryEntries($manager);
$this->createDeletedCategory($manager); $this->createDeletedCategory($manager);

View file

@ -106,7 +106,7 @@ class PartFixtures extends Fixture implements DependentFixtureInterface
$partLot2->setComment('Test'); $partLot2->setComment('Test');
$partLot2->setNeedsRefill(true); $partLot2->setNeedsRefill(true);
$partLot2->setStorageLocation($manager->find(StorageLocation::class, 3)); $partLot2->setStorageLocation($manager->find(StorageLocation::class, 3));
$partLot2->setVendorBarcode('lot2_vendor_barcode'); $partLot2->setUserBarcode('lot2_vendor_barcode');
$part->addPartLot($partLot2); $part->addPartLot($partLot2);
$orderdetail = new Orderdetail(); $orderdetail = new Orderdetail();

View file

@ -22,6 +22,7 @@ declare(strict_types=1);
namespace App\DataFixtures; namespace App\DataFixtures;
use App\Entity\UserSystem\Group;
use App\Entity\UserSystem\User; use App\Entity\UserSystem\User;
use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\DataFixtures\DependentFixtureInterface; use Doctrine\Common\DataFixtures\DependentFixtureInterface;
@ -41,7 +42,7 @@ class UserFixtures extends Fixture implements DependentFixtureInterface
{ {
$anonymous = new User(); $anonymous = new User();
$anonymous->setName('anonymous'); $anonymous->setName('anonymous');
$anonymous->setGroup($this->getReference(GroupFixtures::READONLY)); $anonymous->setGroup($this->getReference(GroupFixtures::READONLY, Group::class));
$anonymous->setNeedPwChange(false); $anonymous->setNeedPwChange(false);
$anonymous->setPassword($this->encoder->hashPassword($anonymous, 'test')); $anonymous->setPassword($this->encoder->hashPassword($anonymous, 'test'));
$manager->persist($anonymous); $manager->persist($anonymous);
@ -50,7 +51,7 @@ class UserFixtures extends Fixture implements DependentFixtureInterface
$admin->setName('admin'); $admin->setName('admin');
$admin->setPassword($this->encoder->hashPassword($admin, 'test')); $admin->setPassword($this->encoder->hashPassword($admin, 'test'));
$admin->setNeedPwChange(false); $admin->setNeedPwChange(false);
$admin->setGroup($this->getReference(GroupFixtures::ADMINS)); $admin->setGroup($this->getReference(GroupFixtures::ADMINS, Group::class));
$manager->persist($admin); $manager->persist($admin);
$this->addReference(self::ADMIN, $admin); $this->addReference(self::ADMIN, $admin);
@ -60,7 +61,7 @@ class UserFixtures extends Fixture implements DependentFixtureInterface
$user->setEmail('user@invalid.invalid'); $user->setEmail('user@invalid.invalid');
$user->setFirstName('Test')->setLastName('User'); $user->setFirstName('Test')->setLastName('User');
$user->setPassword($this->encoder->hashPassword($user, 'test')); $user->setPassword($this->encoder->hashPassword($user, 'test'));
$user->setGroup($this->getReference(GroupFixtures::USERS)); $user->setGroup($this->getReference(GroupFixtures::USERS, Group::class));
$manager->persist($user); $manager->persist($user);
$noread = new User(); $noread = new User();

View file

@ -54,7 +54,7 @@ class TwoStepORMAdapter extends ORMAdapter
private \Closure|null $query_modifier = null; private \Closure|null $query_modifier = null;
public function __construct(ManagerRegistry $registry = null) public function __construct(?ManagerRegistry $registry = null)
{ {
parent::__construct($registry); parent::__construct($registry);
$this->detailQueryCallable = static function (QueryBuilder $qb, array $ids): never { $this->detailQueryCallable = static function (QueryBuilder $qb, array $ids): never {

View file

@ -45,7 +45,7 @@ abstract class AbstractConstraint implements FilterInterface
* @var string The property where this BooleanConstraint should apply to * @var string The property where this BooleanConstraint should apply to
*/ */
protected string $property, protected string $property,
string $identifier = null) ?string $identifier = null)
{ {
$this->identifier = $identifier ?? $this->generateParameterIdentifier($property); $this->identifier = $identifier ?? $this->generateParameterIdentifier($property);
} }

View file

@ -28,7 +28,7 @@ class BooleanConstraint extends AbstractConstraint
{ {
public function __construct( public function __construct(
string $property, string $property,
string $identifier = null, ?string $identifier = null,
/** @var bool|null The value of our constraint */ /** @var bool|null The value of our constraint */
protected ?bool $value = null protected ?bool $value = null
) )

View file

@ -34,7 +34,7 @@ class DateTimeConstraint extends AbstractConstraint
public function __construct( public function __construct(
string $property, string $property,
string $identifier = null, ?string $identifier = null,
/** /**
* The value1 used for comparison (this is the main one used for all mono-value comparisons) * The value1 used for comparison (this is the main one used for all mono-value comparisons)
*/ */

View file

@ -46,7 +46,7 @@ class EntityConstraint extends AbstractConstraint
public function __construct(protected ?NodesListBuilder $nodesListBuilder, public function __construct(protected ?NodesListBuilder $nodesListBuilder,
protected string $class, protected string $class,
string $property, string $property,
string $identifier = null, ?string $identifier = null,
protected ?AbstractDBElement $value = null, protected ?AbstractDBElement $value = null,
protected ?string $operator = null) protected ?string $operator = null)
{ {

View file

@ -31,7 +31,7 @@ class NumberConstraint extends AbstractConstraint
public function __construct( public function __construct(
string $property, string $property,
string $identifier = null, ?string $identifier = null,
/** /**
* The value1 used for comparison (this is the main one used for all mono-value comparisons) * The value1 used for comparison (this is the main one used for all mono-value comparisons)
*/ */

View file

@ -28,7 +28,7 @@ use Doctrine\ORM\QueryBuilder;
class LessThanDesiredConstraint extends BooleanConstraint class LessThanDesiredConstraint extends BooleanConstraint
{ {
public function __construct(string $property = null, string $identifier = null, ?bool $default_value = null) public function __construct(?string $property = null, ?string $identifier = null, ?bool $default_value = null)
{ {
parent::__construct($property ?? '( parent::__construct($property ?? '(
SELECT COALESCE(SUM(ld_partLot.amount), 0.0) SELECT COALESCE(SUM(ld_partLot.amount), 0.0)

View file

@ -30,7 +30,7 @@ class TagsConstraint extends AbstractConstraint
{ {
final public const ALLOWED_OPERATOR_VALUES = ['ANY', 'ALL', 'NONE']; final public const ALLOWED_OPERATOR_VALUES = ['ANY', 'ALL', 'NONE'];
public function __construct(string $property, string $identifier = null, public function __construct(string $property, ?string $identifier = null,
protected ?string $value = null, protected ?string $value = null,
protected ?string $operator = '') protected ?string $operator = '')
{ {
@ -93,10 +93,10 @@ class TagsConstraint extends AbstractConstraint
$expr = $queryBuilder->expr(); $expr = $queryBuilder->expr();
$tmp = $expr->orX( $tmp = $expr->orX(
$expr->like($this->property, ':' . $tag_identifier_prefix . '_1'), 'ILIKE(' . $this->property . ', :' . $tag_identifier_prefix . '_1) = TRUE',
$expr->like($this->property, ':' . $tag_identifier_prefix . '_2'), 'ILIKE(' . $this->property . ', :' . $tag_identifier_prefix . '_2) = TRUE',
$expr->like($this->property, ':' . $tag_identifier_prefix . '_3'), 'ILIKE(' . $this->property . ', :' . $tag_identifier_prefix . '_3) = TRUE',
$expr->eq($this->property, ':' . $tag_identifier_prefix . '_4'), 'ILIKE(' . $this->property . ', :' . $tag_identifier_prefix . '_4) = TRUE',
); );
//Set the parameters for the LIKE expression, in each variation of the tag (so with a comma, at the end, at the beginning, and on both ends, and equaling the tag) //Set the parameters for the LIKE expression, in each variation of the tag (so with a comma, at the end, at the beginning, and on both ends, and equaling the tag)
@ -133,6 +133,7 @@ class TagsConstraint extends AbstractConstraint
return; return;
} }
//@phpstan-ignore-next-line Keep this check to ensure that everything has the same structure even if we add a new operator
if ($this->operator === 'NONE') { if ($this->operator === 'NONE') {
$queryBuilder->andWhere($queryBuilder->expr()->not($queryBuilder->expr()->orX(...$tagsExpressions))); $queryBuilder->andWhere($queryBuilder->expr()->not($queryBuilder->expr()->orX(...$tagsExpressions)));
return; return;

View file

@ -32,7 +32,7 @@ class TextConstraint extends AbstractConstraint
/** /**
* @param string $value * @param string $value
*/ */
public function __construct(string $property, string $identifier = null, /** public function __construct(string $property, ?string $identifier = null, /**
* @var string|null The value to compare to * @var string|null The value to compare to
*/ */
protected ?string $value = null, /** protected ?string $value = null, /**
@ -107,7 +107,8 @@ class TextConstraint extends AbstractConstraint
} }
if ($like_value !== null) { if ($like_value !== null) {
$this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier, 'LIKE', $like_value); $queryBuilder->andWhere(sprintf('ILIKE(%s, :%s) = TRUE', $this->property, $this->identifier));
$queryBuilder->setParameter($this->identifier, $like_value);
return; return;
} }

View file

@ -21,7 +21,6 @@ declare(strict_types=1);
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
namespace App\DataTables\Filters; namespace App\DataTables\Filters;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
class PartSearchFilter implements FilterInterface class PartSearchFilter implements FilterInterface
@ -132,15 +131,15 @@ class PartSearchFilter implements FilterInterface
return sprintf("REGEXP(%s, :search_query) = TRUE", $field); return sprintf("REGEXP(%s, :search_query) = TRUE", $field);
} }
return sprintf("%s LIKE :search_query", $field); return sprintf("ILIKE(%s, :search_query) = TRUE", $field);
}, $fields_to_search); }, $fields_to_search);
//Add Or concatation of the expressions to our query //Add Or concatenation of the expressions to our query
$queryBuilder->andWhere( $queryBuilder->andWhere(
$queryBuilder->expr()->orX(...$expressions) $queryBuilder->expr()->orX(...$expressions)
); );
//For regex we pass the query as is, for like we add % to the start and end as wildcards //For regex, we pass the query as is, for like we add % to the start and end as wildcards
if ($this->regex) { if ($this->regex) {
$queryBuilder->setParameter('search_query', $this->keyword); $queryBuilder->setParameter('search_query', $this->keyword);
} else { } else {

View file

@ -57,6 +57,8 @@ use Symfony\Contracts\Translation\TranslatorInterface;
final class PartsDataTable implements DataTableTypeInterface final class PartsDataTable implements DataTableTypeInterface
{ {
const LENGTH_MENU = [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]];
public function __construct( public function __construct(
private readonly EntityURLGenerator $urlGenerator, private readonly EntityURLGenerator $urlGenerator,
private readonly TranslatorInterface $translator, private readonly TranslatorInterface $translator,

View file

@ -87,16 +87,14 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
if(!$context->getPart() instanceof Part) { if(!$context->getPart() instanceof Part) {
return htmlspecialchars((string) $context->getName()); return htmlspecialchars((string) $context->getName());
} }
if($context->getPart() instanceof Part) {
$tmp = $this->partDataTableHelper->renderName($context->getPart());
if($context->getName() !== null && $context->getName() !== '') {
$tmp .= '<br><b>'.htmlspecialchars($context->getName()).'</b>';
}
return $tmp;
}
//@phpstan-ignore-next-line //Part exists if we reach this point
throw new \RuntimeException('This should never happen!');
$tmp = $this->partDataTableHelper->renderName($context->getPart());
if($context->getName() !== null && $context->getName() !== '') {
$tmp .= '<br><b>'.htmlspecialchars($context->getName()).'</b>';
}
return $tmp;
}, },
]) ])
->add('ipn', TextColumn::class, [ ->add('ipn', TextColumn::class, [

View file

@ -0,0 +1,71 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2024 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\Doctrine\Functions;
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\DBAL\Platforms\SQLitePlatform;
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\SqlWalker;
use Doctrine\ORM\Query\TokenType;
/**
* A platform invariant version of the case-insensitive LIKE operation.
* On MySQL and SQLite this is the normal LIKE, but on PostgreSQL it is the ILIKE operator.
*/
class ILike extends FunctionNode
{
public $value = null;
public $expr = null;
public function parse(Parser $parser): void
{
$parser->match(TokenType::T_IDENTIFIER);
$parser->match(TokenType::T_OPEN_PARENTHESIS);
$this->value = $parser->StringPrimary();
$parser->match(TokenType::T_COMMA);
$this->expr = $parser->StringExpression();
$parser->match(TokenType::T_CLOSE_PARENTHESIS);
}
public function getSql(SqlWalker $sqlWalker): string
{
$platform = $sqlWalker->getConnection()->getDatabasePlatform();
//
if ($platform instanceof AbstractMySQLPlatform || $platform instanceof SQLitePlatform) {
$operator = 'LIKE';
} elseif ($platform instanceof PostgreSQLPlatform) {
//Use the case-insensitive operator, to have the same behavior as MySQL
$operator = 'ILIKE';
} else {
throw new \RuntimeException('Platform ' . gettype($platform) . ' does not support case insensitive like expressions.');
}
return '(' . $this->value->dispatch($sqlWalker) . ' ' . $operator . ' ' . $this->expr->dispatch($sqlWalker) . ')';
}
}

View file

@ -44,15 +44,13 @@ class SQLiteRegexExtensionMiddlewareDriver extends AbstractDriverMiddleware
$native_connection = $connection->getNativeConnection(); $native_connection = $connection->getNativeConnection();
//Ensure that the function really exists on the connection, as it is marked as experimental according to PHP documentation //Ensure that the function really exists on the connection, as it is marked as experimental according to PHP documentation
if($native_connection instanceof \PDO && method_exists($native_connection, 'sqliteCreateFunction' )) { if($native_connection instanceof \PDO) {
$native_connection->sqliteCreateFunction('REGEXP', self::regexp(...), 2, \PDO::SQLITE_DETERMINISTIC); $native_connection->sqliteCreateFunction('REGEXP', self::regexp(...), 2, \PDO::SQLITE_DETERMINISTIC);
$native_connection->sqliteCreateFunction('FIELD', self::field(...), -1, \PDO::SQLITE_DETERMINISTIC); $native_connection->sqliteCreateFunction('FIELD', self::field(...), -1, \PDO::SQLITE_DETERMINISTIC);
$native_connection->sqliteCreateFunction('FIELD2', self::field2(...), 2, \PDO::SQLITE_DETERMINISTIC); $native_connection->sqliteCreateFunction('FIELD2', self::field2(...), 2, \PDO::SQLITE_DETERMINISTIC);
//Create a new collation for natural sorting //Create a new collation for natural sorting
if (method_exists($native_connection, 'sqliteCreateCollation')) { $native_connection->sqliteCreateCollation('NATURAL_CMP', strnatcmp(...));
$native_connection->sqliteCreateCollation('NATURAL_CMP', strnatcmp(...));
}
} }
} }

View file

@ -44,7 +44,7 @@ class DoNotUsePurgerFactory implements PurgerFactory
throw new \LogicException('Do not use doctrine:fixtures:load directly. Use partdb:fixtures:load instead!'); throw new \LogicException('Do not use doctrine:fixtures:load directly. Use partdb:fixtures:load instead!');
} }
public function setEntityManager(EntityManagerInterface $em) public function setEntityManager(EntityManagerInterface $em): void
{ {
// TODO: Implement setEntityManager() method. // TODO: Implement setEntityManager() method.
} }

View file

@ -531,7 +531,7 @@ abstract class Attachment extends AbstractNamedDBElement
$url = str_replace(' ', '%20', $url); $url = str_replace(' ', '%20', $url);
//Only set if the URL is not empty //Only set if the URL is not empty
if ($url !== null && $url !== '') { if ($url !== '') {
if (str_contains($url, '%BASE%') || str_contains($url, '%MEDIA%')) { if (str_contains($url, '%BASE%') || str_contains($url, '%MEDIA%')) {
throw new InvalidArgumentException('You can not reference internal files via the url field! But nice try!'); throw new InvalidArgumentException('You can not reference internal files via the url field! But nice try!');
} }

View file

@ -33,7 +33,7 @@ use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Serializer\Annotation\Groups;
/** /**
* @template-covariant AT of Attachment * @template AT of Attachment
*/ */
#[ORM\MappedSuperclass(repositoryClass: AttachmentContainingDBElementRepository::class)] #[ORM\MappedSuperclass(repositoryClass: AttachmentContainingDBElementRepository::class)]
abstract class AttachmentContainingDBElement extends AbstractNamedDBElement implements HasMasterAttachmentInterface, HasAttachmentsInterface abstract class AttachmentContainingDBElement extends AbstractNamedDBElement implements HasMasterAttachmentInterface, HasAttachmentsInterface

View file

@ -33,8 +33,8 @@ use Symfony\Component\Validator\Constraints as Assert;
/** /**
* This abstract class is used for companies like suppliers or manufacturers. * This abstract class is used for companies like suppliers or manufacturers.
* *
* @template-covariant AT of Attachment * @template AT of Attachment
* @template-covariant PT of AbstractParameter * @template PT of AbstractParameter
* @extends AbstractPartsContainingDBElement<AT, PT> * @extends AbstractPartsContainingDBElement<AT, PT>
*/ */
#[ORM\MappedSuperclass] #[ORM\MappedSuperclass]
@ -162,7 +162,7 @@ abstract class AbstractCompany extends AbstractPartsContainingDBElement
* *
* @return string the link to the article * @return string the link to the article
*/ */
public function getAutoProductUrl(string $partnr = null): string public function getAutoProductUrl(?string $partnr = null): string
{ {
if (is_string($partnr)) { if (is_string($partnr)) {
return str_replace('%PARTNUMBER%', $partnr, $this->auto_product_url); return str_replace('%PARTNUMBER%', $partnr, $this->auto_product_url);

View file

@ -31,8 +31,8 @@ use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Serializer\Annotation\Groups;
/** /**
* @template-covariant AT of Attachment * @template AT of Attachment
* @template-covariant PT of AbstractParameter * @template PT of AbstractParameter
* @extends AbstractStructuralDBElement<AT, PT> * @extends AbstractStructuralDBElement<AT, PT>
*/ */
#[ORM\MappedSuperclass(repositoryClass: AbstractPartsContainingRepository::class)] #[ORM\MappedSuperclass(repositoryClass: AbstractPartsContainingRepository::class)]

View file

@ -53,8 +53,8 @@ use Symfony\Component\Serializer\Annotation\Groups;
* *
* @see \App\Tests\Entity\Base\AbstractStructuralDBElementTest * @see \App\Tests\Entity\Base\AbstractStructuralDBElementTest
* *
* @template-covariant AT of Attachment * @template AT of Attachment
* @template-covariant PT of AbstractParameter * @template PT of AbstractParameter
* @template-use ParametersTrait<PT> * @template-use ParametersTrait<PT>
* @extends AttachmentContainingDBElement<AT> * @extends AttachmentContainingDBElement<AT>
* @uses ParametersTrait<PT> * @uses ParametersTrait<PT>

View file

@ -22,6 +22,8 @@ declare(strict_types=1);
*/ */
namespace App\Entity\LabelSystem; namespace App\Entity\LabelSystem;
use App\Entity\Base\AbstractDBElement;
use App\Entity\Base\AbstractNamedDBElement;
use App\Entity\Parts\Part; use App\Entity\Parts\Part;
use App\Entity\Parts\PartLot; use App\Entity\Parts\PartLot;
use App\Entity\Parts\StorageLocation; use App\Entity\Parts\StorageLocation;
@ -34,7 +36,7 @@ enum LabelSupportedElement: string
/** /**
* Returns the entity class for the given element type * Returns the entity class for the given element type
* @return string * @return class-string<AbstractDBElement>
*/ */
public function getEntityClass(): string public function getEntityClass(): string
{ {

View file

@ -44,9 +44,9 @@ namespace App\Entity\LogSystem;
use App\Entity\Base\AbstractDBElement; use App\Entity\Base\AbstractDBElement;
use App\Entity\UserSystem\User; use App\Entity\UserSystem\User;
use App\Events\SecurityEvents; use App\Events\SecurityEvents;
use App\Helpers\IPAnonymizer;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use InvalidArgumentException; use InvalidArgumentException;
use Symfony\Component\HttpFoundation\IpUtils;
/** /**
* This log entry is created when something security related to a user happens. * This log entry is created when something security related to a user happens.
@ -134,7 +134,7 @@ class SecurityEventLogEntry extends AbstractLogEntry
public function setIPAddress(string $ip, bool $anonymize = true): self public function setIPAddress(string $ip, bool $anonymize = true): self
{ {
if ($anonymize) { if ($anonymize) {
$ip = IpUtils::anonymize($ip); $ip = IPAnonymizer::anonymize($ip);
} }
$this->extra['i'] = $ip; $this->extra['i'] = $ip;

View file

@ -22,8 +22,9 @@ declare(strict_types=1);
namespace App\Entity\LogSystem; namespace App\Entity\LogSystem;
use App\Helpers\IPAnonymizer;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\IpUtils;
/** /**
* This log entry is created when a user logs in. * This log entry is created when a user logs in.
@ -59,7 +60,7 @@ class UserLoginLogEntry extends AbstractLogEntry
public function setIPAddress(string $ip, bool $anonymize = true): self public function setIPAddress(string $ip, bool $anonymize = true): self
{ {
if ($anonymize) { if ($anonymize) {
$ip = IpUtils::anonymize($ip); $ip = IPAnonymizer::anonymize($ip);
} }
$this->extra['i'] = $ip; $this->extra['i'] = $ip;

View file

@ -22,8 +22,8 @@ declare(strict_types=1);
namespace App\Entity\LogSystem; namespace App\Entity\LogSystem;
use App\Helpers\IPAnonymizer;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\IpUtils;
#[ORM\Entity] #[ORM\Entity]
class UserLogoutLogEntry extends AbstractLogEntry class UserLogoutLogEntry extends AbstractLogEntry
@ -56,7 +56,7 @@ class UserLogoutLogEntry extends AbstractLogEntry
public function setIPAddress(string $ip, bool $anonymize = true): self public function setIPAddress(string $ip, bool $anonymize = true): self
{ {
if ($anonymize) { if ($anonymize) {
$ip = IpUtils::anonymize($ip); $ip = IPAnonymizer::anonymize($ip);
} }
$this->extra['i'] = $ip; $this->extra['i'] = $ip;

View file

@ -54,7 +54,7 @@ class OAuthToken extends AbstractNamedDBElement implements AccessTokenInterface
*/ */
private const DEFAULT_EXPIRATION_TIME = 3600; private const DEFAULT_EXPIRATION_TIME = 3600;
public function __construct(string $name, ?string $refresh_token, ?string $token = null, \DateTimeImmutable $expires_at = null) public function __construct(string $name, ?string $refresh_token, ?string $token = null, ?\DateTimeImmutable $expires_at = null)
{ {
//If token is given, you also have to give the expires_at date //If token is given, you also have to give the expires_at date
if ($token !== null && $expires_at === null) { if ($token !== null && $expires_at === null) {

View file

@ -68,7 +68,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
#[ORM\Index(columns: ['needs_refill'], name: 'part_lots_idx_needs_refill')] #[ORM\Index(columns: ['needs_refill'], name: 'part_lots_idx_needs_refill')]
#[ORM\Index(columns: ['vendor_barcode'], name: 'part_lots_idx_barcode')] #[ORM\Index(columns: ['vendor_barcode'], name: 'part_lots_idx_barcode')]
#[ValidPartLot] #[ValidPartLot]
#[UniqueEntity(['vendor_barcode'], message: 'validator.part_lot.vendor_barcode_must_be_unique')] #[UniqueEntity(['user_barcode'], message: 'validator.part_lot.vendor_barcode_must_be_unique')]
#[ApiResource( #[ApiResource(
operations: [ operations: [
new Get(security: 'is_granted("read", object)'), new Get(security: 'is_granted("read", object)'),
@ -166,10 +166,10 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named
/** /**
* @var string|null The content of the barcode of this part lot (e.g. a barcode on the package put by the vendor) * @var string|null The content of the barcode of this part lot (e.g. a barcode on the package put by the vendor)
*/ */
#[ORM\Column(type: Types::STRING, nullable: true)] #[ORM\Column(name: "vendor_barcode", type: Types::STRING, nullable: true)]
#[Groups(['part_lot:read', 'part_lot:write'])] #[Groups(['part_lot:read', 'part_lot:write'])]
#[Length(max: 255)] #[Length(max: 255)]
protected ?string $vendor_barcode = null; protected ?string $user_barcode = null;
public function __clone() public function __clone()
{ {
@ -185,7 +185,6 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named
* *
* @return bool|null True, if the part lot is expired. Returns null, if no expiration date was set. * @return bool|null True, if the part lot is expired. Returns null, if no expiration date was set.
* *
* @throws Exception If an error with the DateTime occurs
*/ */
public function isExpired(): ?bool public function isExpired(): ?bool
{ {
@ -376,19 +375,19 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named
* null if no barcode is set. * null if no barcode is set.
* @return string|null * @return string|null
*/ */
public function getVendorBarcode(): ?string public function getUserBarcode(): ?string
{ {
return $this->vendor_barcode; return $this->user_barcode;
} }
/** /**
* Set the content of the barcode of this part lot (e.g. a barcode on the package put by the vendor). * Set the content of the barcode of this part lot (e.g. a barcode on the package put by the vendor).
* @param string|null $vendor_barcode * @param string|null $user_barcode
* @return $this * @return $this
*/ */
public function setVendorBarcode(?string $vendor_barcode): PartLot public function setUserBarcode(?string $user_barcode): PartLot
{ {
$this->vendor_barcode = $vendor_barcode; $this->user_barcode = $user_barcode;
return $this; return $this;
} }

View file

@ -103,7 +103,7 @@ class Supplier extends AbstractCompany
protected ?AbstractStructuralDBElement $parent = null; protected ?AbstractStructuralDBElement $parent = null;
/** /**
* @var Collection<int, Orderdetail>|Orderdetail[] * @var Collection<int, Orderdetail>
*/ */
#[ORM\OneToMany(mappedBy: 'supplier', targetEntity: Orderdetail::class)] #[ORM\OneToMany(mappedBy: 'supplier', targetEntity: Orderdetail::class)]
protected Collection $orderdetails; protected Collection $orderdetails;

View file

@ -333,7 +333,6 @@ class Project extends AbstractStructuralDBElement
{ {
//If this project has subprojects, and these have builds part, they must be included in the BOM //If this project has subprojects, and these have builds part, they must be included in the BOM
foreach ($this->getChildren() as $child) { foreach ($this->getChildren() as $child) {
/** @var $child Project */
if (!$child->getBuildPart() instanceof Part) { if (!$child->getBuildPart() instanceof Part) {
continue; continue;
} }

View file

@ -256,7 +256,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
protected ?string $password = null; protected ?string $password = null;
#[Assert\NotBlank] #[Assert\NotBlank]
#[Assert\Regex('/^[\w\.\+\-\$]+$/', message: 'user.invalid_username')] #[Assert\Regex('/^[\w\.\+\-\$]+[\w\.\+\-\$\@]*$/', message: 'user.invalid_username')]
#[Groups(['user:read'])] #[Groups(['user:read'])]
protected string $name = ''; protected string $name = '';
@ -893,8 +893,6 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
* @param string[] $codes An array containing the backup codes * @param string[] $codes An array containing the backup codes
* *
* @return $this * @return $this
*
* @throws Exception If an error with the datetime occurs
*/ */
public function setBackupCodes(array $codes): self public function setBackupCodes(array $codes): self
{ {

View file

@ -1,57 +0,0 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\WebpackEncoreBundle\Event\RenderAssetTagEvent;
/**
* This class fixes the wrong pathes generated by webpack using the auto publicPath mode.
* Basically it replaces the wrong /auto/ part of the path with the correct /build/ in all encore entrypoints.
*/
class WebpackAutoPathSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
RenderAssetTagEvent::class => 'onRenderAssetTag'
];
}
public function onRenderAssetTag(RenderAssetTagEvent $event): void
{
if ($event->isScriptTag()) {
$event->setAttribute('src', $this->resolveAuto($event->getUrl()));
}
if ($event->isLinkTag()) {
$event->setAttribute('href', $this->resolveAuto($event->getUrl()));
}
}
private function resolveAuto(string $path): string
{
//Replace the first occurence of /auto/ with /build/ to get the correct path
return preg_replace('/\/auto\//', '/build/', $path, 1);
}
}

View file

@ -71,6 +71,22 @@ class LabelDialogType extends AbstractType
'label' => false, 'label' => false,
'disabled' => !$this->security->isGranted('@labels.edit_options') || $options['disable_options'], 'disabled' => !$this->security->isGranted('@labels.edit_options') || $options['disable_options'],
]); ]);
$builder->add('save_profile_name', TextType::class, [
'required' => false,
'attr' =>[
'placeholder' => 'label_generator.save_profile_name',
]
]);
$builder->add('save_profile', SubmitType::class, [
'label' => 'label_generator.save_profile',
'disabled' => !$this->security->isGranted('@labels.create_profiles'),
'attr' => [
'class' => 'btn btn-outline-success'
]
]);
$builder->add('update', SubmitType::class, [ $builder->add('update', SubmitType::class, [
'label' => 'label_generator.update', 'label' => 'label_generator.update',
]); ]);

View file

@ -41,8 +41,9 @@ declare(strict_types=1);
namespace App\Form\LabelSystem; namespace App\Form\LabelSystem;
use App\Services\LabelSystem\Barcodes\BarcodeSourceType; use App\Services\LabelSystem\BarcodeScanner\BarcodeSourceType;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\EnumType; use Symfony\Component\Form\Extension\Core\Type\EnumType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
@ -55,6 +56,8 @@ class ScanDialogType extends AbstractType
{ {
$builder->add('input', TextType::class, [ $builder->add('input', TextType::class, [
'label' => 'scan_dialog.input', 'label' => 'scan_dialog.input',
//Do not trim the input, otherwise this damages Format06 barcodes which end with non-printable characters
'trim' => false,
'attr' => [ 'attr' => [
'autofocus' => true, 'autofocus' => true,
'id' => 'scan_dialog_input', 'id' => 'scan_dialog_input',
@ -71,9 +74,14 @@ class ScanDialogType extends AbstractType
null => 'scan_dialog.mode.auto', null => 'scan_dialog.mode.auto',
BarcodeSourceType::INTERNAL => 'scan_dialog.mode.internal', BarcodeSourceType::INTERNAL => 'scan_dialog.mode.internal',
BarcodeSourceType::IPN => 'scan_dialog.mode.ipn', BarcodeSourceType::IPN => 'scan_dialog.mode.ipn',
BarcodeSourceType::VENDOR => 'scan_dialog.mode.vendor', BarcodeSourceType::USER_DEFINED => 'scan_dialog.mode.user',
BarcodeSourceType::EIGP114 => 'scan_dialog.mode.eigp'
}, },
]);
$builder->add('info_mode', CheckboxType::class, [
'label' => 'scan_dialog.info_mode',
'required' => false,
]); ]);
$builder->add('submit', SubmitType::class, [ $builder->add('submit', SubmitType::class, [

View file

@ -101,6 +101,8 @@ class PartBaseType extends AbstractType
'dto_value' => $dto?->category, 'dto_value' => $dto?->category,
'label' => 'part.edit.category', 'label' => 'part.edit.category',
'disable_not_selectable' => true, 'disable_not_selectable' => true,
//Do not require category for new parts, so that the user must select the category by hand and cannot forget it (the requirement is handled by the constraint in the entity)
'required' => !$new_part,
]) ])
->add('footprint', StructuralEntityType::class, [ ->add('footprint', StructuralEntityType::class, [
'class' => Footprint::class, 'class' => Footprint::class,

View file

@ -103,10 +103,12 @@ class PartLotType extends AbstractType
'help' => 'part_lot.owner.help', 'help' => 'part_lot.owner.help',
]); ]);
$builder->add('vendor_barcode', TextType::class, [ $builder->add('user_barcode', TextType::class, [
'label' => 'part_lot.edit.vendor_barcode', 'label' => 'part_lot.edit.user_barcode',
'help' => 'part_lot.edit.vendor_barcode.help', 'help' => 'part_lot.edit.vendor_barcode.help',
'required' => false, 'required' => false,
//Do not remove whitespace chars on the beginning and end of the string
'trim' => false,
]); ]);
} }

View file

@ -39,7 +39,7 @@ class ExponentialNumberType extends AbstractType
return NumberType::class; return NumberType::class;
} }
public function configureOptions(OptionsResolver $resolver) public function configureOptions(OptionsResolver $resolver): void
{ {
$resolver->setDefaults([ $resolver->setDefaults([
//We want to allow the full precision of the number, so disable rounding //We want to allow the full precision of the number, so disable rounding
@ -47,7 +47,7 @@ class ExponentialNumberType extends AbstractType
]); ]);
} }
public function buildForm(FormBuilderInterface $builder, array $options) public function buildForm(FormBuilderInterface $builder, array $options): void
{ {
$builder->resetViewTransformers(); $builder->resetViewTransformers();

View file

@ -43,7 +43,7 @@ class StructuralEntityChoiceHelper
/** /**
* Generates the choice attributes for the given AbstractStructuralDBElement. * Generates the choice attributes for the given AbstractStructuralDBElement.
* @return array|string[] * @return array<string, mixed>
*/ */
public function generateChoiceAttr(AbstractNamedDBElement $choice, Options|array $options): array public function generateChoiceAttr(AbstractNamedDBElement $choice, Options|array $options): array
{ {

View file

@ -24,6 +24,7 @@ declare(strict_types=1);
namespace App\Form\Type\Helper; namespace App\Form\Type\Helper;
use App\Entity\Base\AbstractNamedDBElement; use App\Entity\Base\AbstractNamedDBElement;
use App\Entity\Base\AbstractStructuralDBElement;
use App\Repository\StructuralDBElementRepository; use App\Repository\StructuralDBElementRepository;
use App\Services\Trees\NodesListBuilder; use App\Services\Trees\NodesListBuilder;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
@ -33,6 +34,9 @@ use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\Options;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @template T of AbstractStructuralDBElement
*/
class StructuralEntityChoiceLoader extends AbstractChoiceLoader class StructuralEntityChoiceLoader extends AbstractChoiceLoader
{ {
private ?string $additional_element = null; private ?string $additional_element = null;
@ -90,10 +94,14 @@ class StructuralEntityChoiceLoader extends AbstractChoiceLoader
} }
} }
/** @var class-string<T> $class */
$class = $this->options['class']; $class = $this->options['class'];
/** @var StructuralDBElementRepository $repo */
/** @var StructuralDBElementRepository<T> $repo */
$repo = $this->entityManager->getRepository($class); $repo = $this->entityManager->getRepository($class);
$entities = $repo->getNewEntityFromPath($value, '->'); $entities = $repo->getNewEntityFromPath($value, '->');
$results = []; $results = [];

View file

@ -99,7 +99,6 @@ final class TriStateCheckboxType extends AbstractType implements DataTransformer
* *
* @return mixed The value in the transformed representation * @return mixed The value in the transformed representation
* *
* @throws TransformationFailedException when the transformation fails
*/ */
public function transform(mixed $value) public function transform(mixed $value)
{ {
@ -142,8 +141,6 @@ final class TriStateCheckboxType extends AbstractType implements DataTransformer
* @param mixed $value The value in the transformed representation * @param mixed $value The value in the transformed representation
* *
* @return mixed The value in the original representation * @return mixed The value in the original representation
*
* @throws TransformationFailedException when the transformation fails
*/ */
public function reverseTransform(mixed $value) public function reverseTransform(mixed $value)
{ {

View file

@ -0,0 +1,49 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2024 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\Helpers;
use Symfony\Component\HttpFoundation\IpUtils;
/**
* Utils to assist with IP anonymization.
* The IPUtils::anonymize has a certain edgecase with local-link addresses, which is handled here.
* See: https://github.com/Part-DB/Part-DB-server/issues/782
*/
final class IPAnonymizer
{
public static function anonymize(string $ip): string
{
/**
* If the IP contains a % symbol, then it is a local-link address with scoping according to RFC 4007
* In that case, we only care about the part before the % symbol, as the following functions, can only work with
* the IP address itself. As the scope can leak information (containing interface name), we do not want to
* include it in our anonymized IP data.
*/
if (str_contains($ip, '%')) {
$ip = substr($ip, 0, strpos($ip, '%'));
}
return IpUtils::anonymize($ip);
}
}

View file

@ -62,7 +62,7 @@ trait WithPermPresetsTrait
return json_encode($user->getPermissions()); return json_encode($user->getPermissions());
} }
public function setContainer(ContainerInterface $container = null): void public function setContainer(?ContainerInterface $container = null): void
{ {
if ($container !== null) { if ($container !== null) {
$this->container = $container; $this->container = $container;

View file

@ -58,8 +58,8 @@ class AttachmentRepository extends DBElementRepository
{ {
$qb = $this->createQueryBuilder('attachment'); $qb = $this->createQueryBuilder('attachment');
$qb->select('COUNT(attachment)') $qb->select('COUNT(attachment)')
->where('attachment.path LIKE :like'); ->where('attachment.path LIKE :like ESCAPE \'#\'');
$qb->setParameter('like', '\\%SECURE\\%%'); $qb->setParameter('like', '#%SECURE#%%');
$query = $qb->getQuery(); $query = $qb->getQuery();
return (int) $query->getSingleScalarResult(); return (int) $query->getSingleScalarResult();
@ -75,8 +75,8 @@ class AttachmentRepository extends DBElementRepository
{ {
$qb = $this->createQueryBuilder('attachment'); $qb = $this->createQueryBuilder('attachment');
$qb->select('COUNT(attachment)') $qb->select('COUNT(attachment)')
->where('attachment.path LIKE :http') ->where('ILIKE(attachment.path, :http) = TRUE')
->orWhere('attachment.path LIKE :https'); ->orWhere('ILIKE(attachment.path, :https) = TRUE');
$qb->setParameter('http', 'http://%'); $qb->setParameter('http', 'http://%');
$qb->setParameter('https', 'https://%'); $qb->setParameter('https', 'https://%');
$query = $qb->getQuery(); $query = $qb->getQuery();
@ -94,12 +94,12 @@ class AttachmentRepository extends DBElementRepository
{ {
$qb = $this->createQueryBuilder('attachment'); $qb = $this->createQueryBuilder('attachment');
$qb->select('COUNT(attachment)') $qb->select('COUNT(attachment)')
->where('attachment.path LIKE :base') ->where('attachment.path LIKE :base ESCAPE \'#\'')
->orWhere('attachment.path LIKE :media') ->orWhere('attachment.path LIKE :media ESCAPE \'#\'')
->orWhere('attachment.path LIKE :secure'); ->orWhere('attachment.path LIKE :secure ESCAPE \'#\'');
$qb->setParameter('secure', '\\%SECURE\\%%'); $qb->setParameter('secure', '#%SECURE#%%');
$qb->setParameter('base', '\\%BASE\\%%'); $qb->setParameter('base', '#%BASE#%%');
$qb->setParameter('media', '\\%MEDIA\\%%'); $qb->setParameter('media', '#%MEDIA#%%');
$query = $qb->getQuery(); $query = $qb->getQuery();
return (int) $query->getSingleScalarResult(); return (int) $query->getSingleScalarResult();

View file

@ -160,7 +160,7 @@ class LogEntryRepository extends DBElementRepository
* @param int|null $limit * @param int|null $limit
* @param int|null $offset * @param int|null $offset
*/ */
public function getLogsOrderedByTimestamp(string $order = 'DESC', int $limit = null, int $offset = null): array public function getLogsOrderedByTimestamp(string $order = 'DESC', ?int $limit = null, ?int $offset = null): array
{ {
return $this->findBy([], ['timestamp' => $order], $limit, $offset); return $this->findBy([], ['timestamp' => $order], $limit, $offset);
} }

View file

@ -44,7 +44,7 @@ class ParameterRepository extends DBElementRepository
->select('parameter.name') ->select('parameter.name')
->addSelect('parameter.symbol') ->addSelect('parameter.symbol')
->addSelect('parameter.unit') ->addSelect('parameter.unit')
->where('parameter.name LIKE :name'); ->where('ILIKE(parameter.name, :name) = TRUE');
if ($exact) { if ($exact) {
$qb->setParameter('name', $name); $qb->setParameter('name', $name);
} else { } else {

View file

@ -81,10 +81,10 @@ class PartRepository extends NamedDBElementRepository
->leftJoin('part.category', 'category') ->leftJoin('part.category', 'category')
->leftJoin('part.footprint', 'footprint') ->leftJoin('part.footprint', 'footprint')
->where('part.name LIKE :query') ->where('ILIKE(part.name, :query) = TRUE')
->orWhere('part.description LIKE :query') ->orWhere('ILIKE(part.description, :query) = TRUE')
->orWhere('category.name LIKE :query') ->orWhere('ILIKE(category.name, :query) = TRUE')
->orWhere('footprint.name LIKE :query') ->orWhere('ILIKE(footprint.name, :query) = TRUE')
; ;
$qb->setParameter('query', '%'.$query.'%'); $qb->setParameter('query', '%'.$query.'%');

View file

@ -151,7 +151,7 @@ class StructuralDBElementRepository extends AttachmentContainingDBElementReposit
} }
if (null === $entity) { if (null === $entity) {
$class = $this->getClassName(); $class = $this->getClassName();
/** @var AbstractStructuralDBElement $entity */ /** @var TEntityClass $entity */
$entity = new $class; $entity = new $class;
$entity->setName($name); $entity->setName($name);
$entity->setParent($parent); $entity->setParent($parent);
@ -265,7 +265,7 @@ class StructuralDBElementRepository extends AttachmentContainingDBElementReposit
} }
$class = $this->getClassName(); $class = $this->getClassName();
/** @var AbstractStructuralDBElement $entity */ /** @var TEntityClass $entity */
$entity = new $class; $entity = new $class;
$entity->setName($name); $entity->setName($name);

View file

@ -131,7 +131,7 @@ class ApiTokenAuthenticator implements AuthenticatorInterface
/** /**
* @see https://datatracker.ietf.org/doc/html/rfc6750#section-3 * @see https://datatracker.ietf.org/doc/html/rfc6750#section-3
*/ */
private function getAuthenticateHeader(string $errorDescription = null): string private function getAuthenticateHeader(?string $errorDescription = null): string
{ {
$data = [ $data = [
'realm' => $this->realm, 'realm' => $this->realm,

View file

@ -47,7 +47,7 @@ class AuthenticationEntryPoint implements AuthenticationEntryPointInterface
) { ) {
} }
public function start(Request $request, AuthenticationException $authException = null): Response public function start(Request $request, ?AuthenticationException $authException = null): Response
{ {
//Check if the request is an API request //Check if the request is an API request
if ($this->isJSONRequest($request)) { if ($this->isJSONRequest($request)) {

View file

@ -25,6 +25,7 @@ namespace App\Security;
use App\Entity\UserSystem\User; use App\Entity\UserSystem\User;
use Nbgrp\OneloginSamlBundle\Security\Http\Authenticator\Token\SamlToken; use Nbgrp\OneloginSamlBundle\Security\Http\Authenticator\Token\SamlToken;
use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent; use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAccountStatusException; use Symfony\Component\Security\Core\Exception\CustomUserMessageAccountStatusException;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
@ -50,13 +51,20 @@ class EnsureSAMLUserForSAMLLoginChecker implements EventSubscriberInterface
$token = $event->getAuthenticationToken(); $token = $event->getAuthenticationToken();
$user = $token->getUser(); $user = $token->getUser();
//If we are using SAML, we need to check that the user is a SAML user. //Do not check for anonymous users
if ($token instanceof SamlToken) { if (!$user instanceof User) {
if ($user instanceof User && !$user->isSamlUser()) { return;
throw new CustomUserMessageAccountStatusException($this->translator->trans('saml.error.cannot_login_local_user_per_saml', [], 'security')); }
}
} elseif ($user instanceof User && $user->isSamlUser()) { //Do not allow SAML users to login as local user
//Ensure that you can not login locally with a SAML user (even if this should not happen, as the password is not set) if ($token instanceof SamlToken && !$user->isSamlUser()) {
throw new CustomUserMessageAccountStatusException($this->translator->trans('saml.error.cannot_login_local_user_per_saml',
[], 'security'));
}
//Do not allow local users to login as SAML user via local username and password
if ($token instanceof UsernamePasswordToken && $user->isSamlUser()) {
//Ensure that you can not login locally with a SAML user (even though this should not happen, as the password is not set)
throw new CustomUserMessageAccountStatusException($this->translator->trans('saml.error.cannot_login_saml_user_locally', [], 'security')); throw new CustomUserMessageAccountStatusException($this->translator->trans('saml.error.cannot_login_saml_user_locally', [], 'security'));
} }
} }

View file

@ -116,10 +116,10 @@ class SamlUserFactory implements SamlUserFactoryInterface, EventSubscriberInterf
* Maps a list of SAML roles to a local group ID. * Maps a list of SAML roles to a local group ID.
* The first available mapping will be used (so the order of the $map is important, first match wins). * The first available mapping will be used (so the order of the $map is important, first match wins).
* @param array $roles The list of SAML roles * @param array $roles The list of SAML roles
* @param array $map|null The mapping from SAML roles. If null, the global mapping will be used. * @param array|null $map The mapping from SAML roles. If null, the global mapping will be used.
* @return int|null The ID of the local group or null if no mapping was found. * @return int|null The ID of the local group or null if no mapping was found.
*/ */
public function mapSAMLRolesToLocalGroupID(array $roles, array $map = null): ?int public function mapSAMLRolesToLocalGroupID(array $roles, ?array $map = null): ?int
{ {
$map ??= $this->saml_role_mapping; $map ??= $this->saml_role_mapping;

View file

@ -40,12 +40,10 @@ final class UserChecker implements UserCheckerInterface
/** /**
* Checks the user account before authentication. * Checks the user account before authentication.
*
* @throws AccountStatusException
*/ */
public function checkPreAuth(UserInterface $user): void public function checkPreAuth(UserInterface $user): void
{ {
// TODO: Implement checkPreAuth() method. //We don't need to check the user before authentication, just implemented to fulfill the interface
} }
/** /**

View file

@ -42,7 +42,7 @@ class AttachmentNormalizer implements NormalizerInterface, NormalizerAwareInterf
{ {
} }
public function normalize(mixed $object, string $format = null, array $context = []): array|null public function normalize(mixed $object, ?string $format = null, array $context = []): array|null
{ {
if (!$object instanceof Attachment) { if (!$object instanceof Attachment) {
throw new \InvalidArgumentException('This normalizer only supports Attachment objects!'); throw new \InvalidArgumentException('This normalizer only supports Attachment objects!');
@ -60,7 +60,7 @@ class AttachmentNormalizer implements NormalizerInterface, NormalizerAwareInterf
return $data; return $data;
} }
public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
{ {
// avoid recursion: only call once per object // avoid recursion: only call once per object
if (isset($context[self::ALREADY_CALLED])) { if (isset($context[self::ALREADY_CALLED])) {

View file

@ -33,12 +33,12 @@ use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class BigNumberNormalizer implements NormalizerInterface, DenormalizerInterface class BigNumberNormalizer implements NormalizerInterface, DenormalizerInterface
{ {
public function supportsNormalization($data, string $format = null, array $context = []): bool public function supportsNormalization($data, ?string $format = null, array $context = []): bool
{ {
return $data instanceof BigNumber; return $data instanceof BigNumber;
} }
public function normalize($object, string $format = null, array $context = []): string public function normalize($object, ?string $format = null, array $context = []): string
{ {
if (!$object instanceof BigNumber) { if (!$object instanceof BigNumber) {
throw new \InvalidArgumentException('This normalizer only supports BigNumber objects!'); throw new \InvalidArgumentException('This normalizer only supports BigNumber objects!');
@ -58,7 +58,7 @@ class BigNumberNormalizer implements NormalizerInterface, DenormalizerInterface
]; ];
} }
public function denormalize(mixed $data, string $type, string $format = null, array $context = []): BigNumber|null public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): BigNumber|null
{ {
if (!is_a($type, BigNumber::class, true)) { if (!is_a($type, BigNumber::class, true)) {
throw new \InvalidArgumentException('This normalizer only supports BigNumber objects!'); throw new \InvalidArgumentException('This normalizer only supports BigNumber objects!');
@ -67,7 +67,7 @@ class BigNumberNormalizer implements NormalizerInterface, DenormalizerInterface
return $type::of($data); return $type::of($data);
} }
public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool
{ {
//data must be a string or a number (int, float, etc.) and the type must be BigNumber or BigDecimal //data must be a string or a number (int, float, etc.) and the type must be BigNumber or BigDecimal
return (is_string($data) || is_numeric($data)) && (is_subclass_of($type, BigNumber::class)); return (is_string($data) || is_numeric($data)) && (is_subclass_of($type, BigNumber::class));

View file

@ -63,13 +63,13 @@ class PartNormalizer implements NormalizerInterface, DenormalizerInterface, Norm
{ {
} }
public function supportsNormalization($data, string $format = null, array $context = []): bool public function supportsNormalization($data, ?string $format = null, array $context = []): bool
{ {
//We only remove the type field for CSV export //We only remove the type field for CSV export
return !isset($context[self::ALREADY_CALLED]) && $format === 'csv' && $data instanceof Part ; return !isset($context[self::ALREADY_CALLED]) && $format === 'csv' && $data instanceof Part ;
} }
public function normalize($object, string $format = null, array $context = []): array public function normalize($object, ?string $format = null, array $context = []): array
{ {
if (!$object instanceof Part) { if (!$object instanceof Part) {
throw new \InvalidArgumentException('This normalizer only supports Part objects!'); throw new \InvalidArgumentException('This normalizer only supports Part objects!');
@ -94,7 +94,14 @@ class PartNormalizer implements NormalizerInterface, DenormalizerInterface, Norm
public function supportsDenormalization($data, string $type, string $format = null, array $context = []): bool public function supportsDenormalization($data, string $type, string $format = null, array $context = []): bool
{ {
return !isset($context[self::ALREADY_CALLED]) && is_array($data) && is_a($type, Part::class, true); //Only denormalize if we are doing a file import operation
if (!($context['partdb_import'] ?? false)) {
return false;
}
//Only make the denormalizer available on import operations
return !isset($context[self::ALREADY_CALLED])
&& is_array($data) && is_a($type, Part::class, true);
} }
private function normalizeKeys(array &$data): array private function normalizeKeys(array &$data): array
@ -110,7 +117,7 @@ class PartNormalizer implements NormalizerInterface, DenormalizerInterface, Norm
return $data; return $data;
} }
public function denormalize($data, string $type, string $format = null, array $context = []): ?Part public function denormalize($data, string $type, ?string $format = null, array $context = []): ?Part
{ {
$this->normalizeKeys($data); $this->normalizeKeys($data);

Some files were not shown because too many files have changed in this diff Show more