Merge tag 'v2.12.3' into order-details

# Conflicts:
#	src/DataTables/ProjectBomEntriesDataTable.php
This commit is contained in:
Fabian Wunsch 2026-06-23 08:43:08 +02:00
commit 472eca8c9d
47 changed files with 1862 additions and 1411 deletions

View file

@ -51,5 +51,18 @@
# Disable Topics tracking if not enabled explicitly: https://github.com/jkarlin/topics
header ?Permissions-Policy "browsing-topics=()"
# Set a strict CSP and nosniff for all static assets not handled by PHP.
# ? means "set only if not already present", so PHP responses carrying a Nelmio CSP are left untouched.
header ?Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; sandbox;"
header ?X-Content-Type-Options "nosniff"
# SVG files get a slightly different CSP because they can embed resources and must not be framed.
@svg path *.svg *.svg.gz *.svg.br
header @svg Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'; sandbox;"
# Prevent PHP execution in the media upload directory
@php_in_media path_regexp (?i)^/media/.*\.(php[3-8]?|phar|phtml|pht|phps)$
respond @php_in_media 403
php_server
}

View file

@ -15,6 +15,14 @@
AllowOverride All
</Directory>
# Prevent PHP execution in the media upload directory (server-level, not .htaccess,
# because public/media is a Docker volume and .htaccess there may not be present)
<Directory /var/www/html/public/media>
<FilesMatch "(?i)\.(php[3-8]?|phar|phtml|pht|phps)$">
Require all denied
</FilesMatch>
</Directory>
# Available loglevels: trace8, ..., trace1, debug, info, notice, warn,
# error, crit, alert, emerg.
# It is also possible to configure the loglevel for particular

15
.env
View file

@ -1,6 +1,10 @@
#### Part-DB Configuration
# See https://docs.part-db.de/configuration.html for documentation of available options
# Change this to a random value to secure your installation! You can generate a random string with "openssl rand -hex 16"
# Share that value with nobody and keep it secret
APP_SECRET=a03498528f5a5fc089273ec9ae5b2849
###################################################################################
# Database settings
###################################################################################
@ -145,6 +149,16 @@ DISABLE_YEAR2038_BUG_CHECK=0
#TRUSTED_PROXIES=127.0.0.0/8,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
#TRUSTED_HOSTS='^(localhost|example\.com)$'
###################################################################################
# Logging settings
###################################################################################
# The minimum level a deprecation notice must have to be written to the var/log/<env>_deprecations.log file.
# Deprecation notices are logged with level "info", so this disables the deprecation log by default.
# Set to debug to log all deprecation notices
DEPRECATION_LOG_LEVEL=emergency
###> symfony/lock ###
# Choose one of the stores below
@ -158,7 +172,6 @@ CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
###> symfony/framework-bundle ###
APP_ENV=prod
APP_SECRET=a03498528f5a5fc089273ec9ae5b2849
APP_SHARE_DIR=var/share
###< symfony/framework-bundle ###

View file

@ -129,7 +129,7 @@ jobs:
run: ./bin/phpunit --coverage-clover=coverage.xml
- name: Upload coverage
uses: codecov/codecov-action@v6
uses: codecov/codecov-action@v7
with:
env_vars: PHP_VERSION,DB_TYPE
token: ${{ secrets.CODECOV_TOKEN }}

View file

@ -193,7 +193,7 @@ RUN a2dissite 000-default.conf && \
a2enmod proxy_fcgi setenvif && \
a2enconf php${PHP_VERSION}-fpm && \
a2enconf docker-php && \
a2enmod rewrite
a2enmod rewrite headers
# Install composer and yarn dependencies for Part-DB
USER www-data

View file

@ -1 +1 @@
2.12.0
2.12.3

View file

@ -51,7 +51,7 @@ export default class extends Controller {
//Make the state persistent over reloads
if(localStorage.getItem(STORAGE_KEY) === 'true') {
sidebarHide();
this.hideSidebar();
}
}

View file

@ -70,6 +70,7 @@
"symfony/flex": "^v2.3.1",
"symfony/form": "7.4.*",
"symfony/framework-bundle": "7.4.*",
"symfony/html-sanitizer": "7.4.*",
"symfony/http-client": "7.4.*",
"symfony/http-kernel": "7.4.*",
"symfony/mailer": "7.4.*",

1150
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -51,6 +51,7 @@ when@prod:
type: stream
channels: [deprecation]
path: "%kernel.logs_dir%/%kernel.environment%_deprecations.log"
level: "%env(DEPRECATION_LOG_LEVEL)%"
when@docker:
monolog:
@ -75,3 +76,4 @@ when@docker:
type: stream
channels: [deprecation]
path: "%kernel.logs_dir%/%kernel.environment%_deprecations.log"
level: "%env(DEPRECATION_LOG_LEVEL)%"

View file

@ -653,7 +653,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* time_based_uuid_node?: scalar|Param|null,
* },
* html_sanitizer?: bool|array{ // HtmlSanitizer configuration
* enabled?: bool|Param, // Default: false
* enabled?: bool|Param, // Default: true
* sanitizers?: array<string, array{ // Default: []
* allow_safe_elements?: bool|Param, // Allows "safe" elements and attributes. // Default: false
* allow_static_elements?: bool|Param, // Allows all static elements and attributes from the W3C Sanitizer API standard. // Default: false
@ -718,7 +718,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* servicename?: scalar|Param|null, // Overrules dbname parameter if given and used as SERVICE_NAME or SID connection parameter for Oracle depending on the service parameter.
* sessionMode?: scalar|Param|null, // The session mode to use for the oci8 driver
* server?: scalar|Param|null, // The name of a running database server to connect to for SQL Anywhere.
* default_dbname?: scalar|Param|null, // Override the default database (postgres) to connect to for PostgreSQL connexion.
* default_dbname?: scalar|Param|null, // Override the default database (postgres) to connect to for PostgreSQL connection.
* sslmode?: scalar|Param|null, // Determines whether or with what priority a SSL TCP/IP connection will be negotiated with the server for PostgreSQL.
* sslrootcert?: scalar|Param|null, // The name of a file containing SSL certificate authority (CA) certificate(s). If the file exists, the server's certificate will be verified to be signed by one of these authorities.
* sslcert?: scalar|Param|null, // The path to the SSL client certificate file for PostgreSQL.
@ -769,7 +769,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* servicename?: scalar|Param|null, // Overrules dbname parameter if given and used as SERVICE_NAME or SID connection parameter for Oracle depending on the service parameter.
* sessionMode?: scalar|Param|null, // The session mode to use for the oci8 driver
* server?: scalar|Param|null, // The name of a running database server to connect to for SQL Anywhere.
* default_dbname?: scalar|Param|null, // Override the default database (postgres) to connect to for PostgreSQL connexion.
* default_dbname?: scalar|Param|null, // Override the default database (postgres) to connect to for PostgreSQL connection.
* sslmode?: scalar|Param|null, // Determines whether or with what priority a SSL TCP/IP connection will be negotiated with the server for PostgreSQL.
* sslrootcert?: scalar|Param|null, // The name of a file containing SSL certificate authority (CA) certificate(s). If the file exists, the server's certificate will be verified to be signed by one of these authorities.
* sslcert?: scalar|Param|null, // The path to the SSL client certificate file for PostgreSQL.
@ -801,7 +801,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* servicename?: scalar|Param|null, // Overrules dbname parameter if given and used as SERVICE_NAME or SID connection parameter for Oracle depending on the service parameter.
* sessionMode?: scalar|Param|null, // The session mode to use for the oci8 driver
* server?: scalar|Param|null, // The name of a running database server to connect to for SQL Anywhere.
* default_dbname?: scalar|Param|null, // Override the default database (postgres) to connect to for PostgreSQL connexion.
* default_dbname?: scalar|Param|null, // Override the default database (postgres) to connect to for PostgreSQL connection.
* sslmode?: scalar|Param|null, // Determines whether or with what priority a SSL TCP/IP connection will be negotiated with the server for PostgreSQL.
* sslrootcert?: scalar|Param|null, // The name of a file containing SSL certificate authority (CA) certificate(s). If the file exists, the server's certificate will be verified to be signed by one of these authorities.
* sslcert?: scalar|Param|null, // The path to the SSL client certificate file for PostgreSQL.
@ -2436,6 +2436,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* },
* jsonapi?: array{
* use_iri_as_id?: bool|Param, // Set to false to use entity identifiers instead of IRIs as the "id" field in JSON:API responses. // Default: true
* allow_client_generated_id?: bool|Param, // Allow client-generated IDs on JSON:API POST per https://jsonapi.org/format/#crud-creating-client-ids. Off by default to prevent id spoofing on public endpoints. // Default: false
* },
* eager_loading?: bool|array{
* enabled?: bool|Param, // Default: true

View file

@ -114,10 +114,21 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept
* `datastructure_create`: Creation of a new data structure (e.g. category, manufacturer, ...)
* `CHECK_FOR_UPDATES` (default `1`): Set this to 0 if you do not want Part-DB to connect to GitHub to check for new
versions, or if your server cannot connect to the internet.
* `APP_SECRET` (env only): This variable is a configuration parameter used for various security-related purposes,
particularly for securing and protecting various aspects of your application. It's a secret key that is used for
cryptographic operations and security measures (session management, CSRF protection, etc..). Therefore this
value should be handled as confidential data and not shared publicly.
* `APP_SECRET` (env only): A secret key used by Symfony for cryptographic operations — signing cookies, generating
CSRF tokens, and other security-sensitive tasks. **You must change this from the default value before exposing
Part-DB to any network.** The default value shipped with Part-DB is publicly known; leaving it in place would allow
an attacker to forge signed cookies and bypass CSRF protection.
Generate a secure value and add it to `.env.local`:
```bash
echo "APP_SECRET=$(openssl rand -hex 32)" >> .env.local
```
For Docker, pass it in the `environment` section of your `docker-compose.yaml`:
```yaml
environment:
- APP_SECRET=<output of: openssl rand -hex 32>
```
Part-DB displays a warning on the homepage (visible to administrators only) as long as the default value is in use.
* `SHOW_PART_IMAGE_OVERLAY`: Set to 0 to disable the part image overlay, which appears if you hover over an image in the
part image gallery
* `IPN_SUGGEST_REGEX`: A global regular expression, that part IPNs have to fulfill. Enforce your own format for your users.
@ -268,9 +279,13 @@ See the [information providers]({% link usage/information_provider_system.md %})
* `BANNER`: You can configure the text that should be shown as the banner on the homepage. Useful especially for docker
containers. In all other applications you can just change the `config/banner.md` file.
* `DISABLE_YEAR2038_BUG_CHECK` (env only): If set to `1`, the year 2038 bug check is disabled on 32-bit systems, and dates after
2038 are no longer forbidden. However this will lead to 500 error messages when rendering dates after 2038 as all current
2038 are no longer forbidden. However, this will lead to 500 error messages when rendering dates after 2038 as all current
32-bit PHP versions can not format these dates correctly. This setting is for the case that future PHP versions will
handle this correctly on 32-bit systems. 64-bit systems are not affected by this bug, and the check is always disabled.
* `DEPRECATION_LOG_LEVEL` (default `emergency`) (env only): In the `prod` and `docker` environments, PHP/Symfony
deprecation notices are written to their own `var/log/<env>_deprecations.log` file. This option sets the minimum log
level a deprecation notice must have to be written there. Since deprecation notices are logged with level `info`,
the default value of `emergency` effectively disables this dedicated deprecation log. Set it to `debug` to enable it.
## Banner

View file

@ -47,7 +47,10 @@ services:
- DATABASE_URL=sqlite:///%kernel.project_dir%/var/db/app.db
# In docker env logs will be redirected to stderr
- APP_ENV=docker
# Secret key used to sign cookies and CSRF tokens. MUST be changed to a unique random value before going live!
# Generate one with: openssl rand -hex 32
- APP_SECRET=CHANGE_ME
# Uncomment this, if you want to use the automatic database migration feature. With this you have you do not have to
# run the doctrine:migrations:migrate commands on installation or upgrade. A database backup is written to the uploads/
# folder (under .automigration-backup), so you can restore it, if the migration fails.
@ -89,7 +92,11 @@ services:
4. Customize the settings by changing the environment variables (or adding new ones). See [Configuration]({% link
configuration.md %}) for more information.
5. Inside the folder, run
5. Make sure to change the `APP_SECRET` variable to a unique random value (32 characters). You can generate one with the following command:
```bash
openssl rand -hex 32
```
6. Inside the folder, run
```bash
docker-compose up -d
@ -100,7 +107,7 @@ services:
> Otherwise Part-DB console might use the wrong configuration to execute commands.
6. Create the initial database with
7. Create the initial database with
```bash
docker exec --user=www-data partdb php bin/console doctrine:migrations:migrate
@ -108,7 +115,7 @@ docker exec --user=www-data partdb php bin/console doctrine:migrations:migrate
and watch for the password output
6. Part-DB is available under `http://localhost:8080` and you can log in with the username `admin` and the password shown
8. Part-DB is available under `http://localhost:8080` and you can log in with the username `admin` and the password shown
before
The docker image uses a SQLite database and all data (database, uploads, and other media) is put into folders relative to
@ -121,6 +128,7 @@ If you want to use MySQL as a database, you can use the following docker-compose
{: .warning }
> You have to replace the values for MYSQL_ROOT_PASSWORD and MYSQL_PASSWORD with your own passwords!!
> You have to change MYSQL_PASSWORD in the database section and for the DATABASE_URL in the partdb section.
> Generate a random string for APP_SECRET.
```yaml
version: '3.3'
@ -142,14 +150,19 @@ services:
environment:
# Replace SECRET_USER_PASSWORD with the value of MYSQL_PASSWORD from below
- DATABASE_URL=mysql://partdb:SECRET_USER_PASSWORD@database:3306/partdb
# Secret key used to sign cookies. MUST be changed to a unique random value before going live!
# Generate one with: openssl rand -hex 32
- APP_SECRET=CHANGE_ME
# In docker env logs will be redirected to stderr
- APP_ENV=docker
# Uncomment this, if you want to use the automatic database migration feature. With this you do not have to
# run the doctrine:migrations:migrate commands on installation or upgrade. A database backup is written to the uploads/
# folder (under .automigration-backup), so you can restore it, if the migration fails.
# This feature is currently experimental, so use it at your own risk!
# - DB_AUTOMIGRATE=true
# Uncomment this, if you want to use the automatic database migration feature. With this you do not have to
# run the doctrine:migrations:migrate commands on installation or upgrade. A database backup is written to the uploads/
# folder (under .automigration-backup), so you can restore it, if the migration fails.
# This feature is currently experimental, so use it at your own risk!
# - DB_AUTOMIGRATE=true
# You can configure Part-DB using the webUI or environment variables
# However you can add any other environment configuration you want here

View file

@ -136,6 +136,15 @@ cp .env .env.local
In your `.env.local` you can configure Part-DB according to your wishes and overwrite web interface settings.
A full list of configuration options can be found [here](../configuration.md).
{: .important }
> **Change `APP_SECRET` before going live.** The default value shipped with Part-DB is publicly known and must not be
> used in production — it would allow an attacker to forge signed cookies and bypass CSRF protection.
> Generate a new value and add it to your `.env.local`:
> ```bash
> echo "APP_SECRET=$(openssl rand -hex 32)" >> .env.local
> ```
> or edit the file with a text editor and add a new value for `APP_SECRET` (you can generate a random value with `openssl rand -hex 32`).
Please check that the configured base currency matches your mainly used currency, as
this can not be changed after creating price information.
@ -223,7 +232,7 @@ sudo ln -s /etc/apache2/sites-available/partdb.conf /etc/apache2/sites-enabled/p
Configure apache to show pretty URL paths for Part-DB (`/label/dialog` instead of `/index.php/label/dialog`):
```bash
sudo a2enmod rewrite
sudo a2enmod rewrite headers
```
If you want to access Part-DB via the IP-Address of the server, instead of the domain name, you have to remove the

View file

@ -36,6 +36,10 @@ server {
root /var/www/partdb/public;
location / {
# Headers are set here for static assets. PHP responses are served via the index.php location
# below and inherit neither of these headers, so Nelmio's PHP-side CSP is unaffected.
add_header Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; sandbox;" always;
add_header X-Content-Type-Options "nosniff" always;
try_files $uri /index.php$is_args$args;
}
@ -52,10 +56,17 @@ server {
location ~ \.php$ {
return 404;
}
# Set Content-Security-Policy for svg files, to block embedded javascript in there
# Prevent PHP execution in the media upload directory
location ~* ^/media/.*\.(php[3-8]?|phar|phtml|pht|phps)$ {
return 403;
}
# SVG files get a slightly different CSP because they can embed resources and must not be framed.
# This regex location takes precedence over location /, so headers must be repeated here.
location ~* \.svg$ {
add_header Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none';";
add_header Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'; sandbox;" always;
add_header X-Content-Type-Options "nosniff" always;
}
error_log /var/log/nginx/parts.error.log;

View file

@ -119,9 +119,14 @@ DirectoryIndex index.php
</IfModule>
</IfModule>
# Set Content-Security-Policy for svg files (and compressed variants), to block embedded javascript in there
<IfModule mod_headers.c>
# Set a strict CSP for all static assets not handled by PHP.
# PHP responses already carry their own CSP via NelmioSecurityBundle, so setifempty leaves those untouched.
Header always setifempty Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; sandbox;"
Header always setifempty X-Content-Type-Options "nosniff"
# SVG files get a slightly different CSP because they can embed resources and must not be framed.
<FilesMatch "\.(svg|svg\.gz|svg\.br)$">
Header set Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none';"
Header always set Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'; sandbox;"
</FilesMatch>
</IfModule>
</IfModule>

View file

@ -1,4 +1,4 @@
# Generated on Mon May 25 06:41:46 UTC 2026
# Generated on Mon Jun 15 07:28:00 UTC 2026
# This file contains all footprints available in the offical KiCAD library
Audio_Module:Reverb_BTDR-1H
Audio_Module:Reverb_BTDR-1V
@ -12012,7 +12012,6 @@ Package_DFN_QFN:WDFN-8-1EP_4x3mm_P0.65mm_EP2.4x1.8mm_ThermalVias
Package_DFN_QFN:WDFN-8-1EP_6x5mm_P1.27mm_EP3.4x4mm
Package_DFN_QFN:WDFN-8-1EP_8x6mm_P1.27mm_EP6x4.8mm
Package_DFN_QFN:WDFN-8-1EP_8x6mm_P1.27mm_EP6x4.8mm_ThermalVias
Package_DFN_QFN:WDFN-8_2x2mm_P0.5mm
Package_DFN_QFN:WFDFPN-8-1EP_3x2mm_P0.5mm_EP1.25x1.35mm
Package_DFN_QFN:WQFN-14-1EP_2.5x2.5mm_P0.5mm_EP1.45x1.45mm
Package_DFN_QFN:WQFN-14-1EP_2.5x2.5mm_P0.5mm_EP1.45x1.45mm_ThermalVias
@ -12358,7 +12357,6 @@ Package_DirectFET:DirectFET_SQ
Package_DirectFET:DirectFET_ST
Package_LCC:Analog_LCC-8_5x5mm_P1.27mm
Package_LCC:MO047AD_PLCC-52_19.1x19.1mm_P1.27mm
Package_LCC:PLCC-20
Package_LCC:PLCC-20_9.0x9.0mm_P1.27mm
Package_LCC:PLCC-20_SMD-Socket
Package_LCC:PLCC-20_THT-Socket

View file

@ -1,4 +1,4 @@
# Generated on Mon May 25 06:42:27 UTC 2026
# Generated on Mon Jun 15 07:28:38 UTC 2026
# This file contains all symbols available in the offical KiCAD library
4xxx:14528
4xxx:14529
@ -2255,11 +2255,11 @@ Battery_Management:BQ76200PW
Battery_Management:BQ76920PW
Battery_Management:BQ76930DBT
Battery_Management:BQ76940DBT
Battery_Management:BQ7695201PFBR
Battery_Management:BQ7695202PFBR
Battery_Management:BQ7695203PFBR
Battery_Management:BQ7695204PFBR
Battery_Management:BQ76952PFBR
Battery_Management:BQ7695201PFB
Battery_Management:BQ7695202PFB
Battery_Management:BQ7695203PFB
Battery_Management:BQ7695204PFB
Battery_Management:BQ76952PFB
Battery_Management:BQ78350DBT
Battery_Management:BQ78350DBT-R1
Battery_Management:CN3063
@ -5300,21 +5300,39 @@ Converter_DCDC:TMR-4812
Converter_DCDC:TMR-4821
Converter_DCDC:TMR-4822
Converter_DCDC:TMR-4823
Converter_DCDC:TMR10-1211WI
Converter_DCDC:TMR10-1212WI
Converter_DCDC:TMR10-1213WI
Converter_DCDC:TMR10-1215WI
Converter_DCDC:TMR10-1222WI
Converter_DCDC:TMR10-1223WI
Converter_DCDC:TMR10-2410WIR
Converter_DCDC:TMR10-2411WI
Converter_DCDC:TMR10-2411WIR
Converter_DCDC:TMR10-2412WI
Converter_DCDC:TMR10-2412WIR
Converter_DCDC:TMR10-2413WI
Converter_DCDC:TMR10-2413WIR
Converter_DCDC:TMR10-2415WI
Converter_DCDC:TMR10-2415WIR
Converter_DCDC:TMR10-2421WIR
Converter_DCDC:TMR10-2422WI
Converter_DCDC:TMR10-2422WIR
Converter_DCDC:TMR10-2423WI
Converter_DCDC:TMR10-2423WIR
Converter_DCDC:TMR10-4810WIR
Converter_DCDC:TMR10-4811WI
Converter_DCDC:TMR10-4811WIR
Converter_DCDC:TMR10-4812WI
Converter_DCDC:TMR10-4812WIR
Converter_DCDC:TMR10-4813WI
Converter_DCDC:TMR10-4813WIR
Converter_DCDC:TMR10-4815WI
Converter_DCDC:TMR10-4815WIR
Converter_DCDC:TMR10-4821WIR
Converter_DCDC:TMR10-4822WI
Converter_DCDC:TMR10-4822WIR
Converter_DCDC:TMR10-4823WI
Converter_DCDC:TMR10-4823WIR
Converter_DCDC:TMR10-7210WIR
Converter_DCDC:TMR10-7211WIR
@ -14593,6 +14611,8 @@ MCU_Texas:LM4F111C4QR
MCU_Texas:LM4F111E5QR
MCU_Texas:LM4F111H5QR
MCU_Texas:MSP432E401Y
MCU_Texas:MSPM0C110xSDDF
MCU_Texas:MSPM0C110xSDSG
MCU_Texas:TM4C1230C3PM
MCU_Texas:TM4C1230D5PM
MCU_Texas:TM4C1230E6PM
@ -15115,6 +15135,7 @@ Memory_Flash:AM29F400Bx-xxEx
Memory_Flash:AM29F400Bx-xxSx
Memory_Flash:AM29PDL128G
Memory_Flash:AT25DF041x-UxN-x
Memory_Flash:AT25SF041B-SSHD-X
Memory_Flash:AT25SF081-SSHD-X
Memory_Flash:AT25SF081-SSHF-X
Memory_Flash:AT25SF081-XMHD-X
@ -15670,6 +15691,7 @@ Power_Management:LM5069MM-1
Power_Management:LM5069MM-2
Power_Management:LM66100DCK
Power_Management:LM74700
Power_Management:LM74701-Q1
Power_Management:LMG3410
Power_Management:LMG5200
Power_Management:LT1641-1
@ -15771,6 +15793,7 @@ Power_Management:TPS22810DBV
Power_Management:TPS22810DRV
Power_Management:TPS22917DBV
Power_Management:TPS22917LDBV
Power_Management:TPS22919DCK
Power_Management:TPS22929D
Power_Management:TPS22993
Power_Management:TPS2412D
@ -16037,6 +16060,7 @@ Power_Supervisor:TPS3831
Power_Supervisor:TPS3839DBZ
Power_Supervisor:TPS3839DQN
RF:0900PC15J0013
RF:AD8302xRU
RF:ADC-10-1R
RF:ADCH-80
RF:ADCH-80A
@ -16479,6 +16503,7 @@ RF_Module:DWM3000
RF_Module:E18-MS1-PCB
RF_Module:E73-2G4M04S-52810
RF_Module:E73-2G4M04S-52832
RF_Module:ESP-01
RF_Module:ESP-07
RF_Module:ESP-12E
RF_Module:ESP-12F
@ -16680,6 +16705,19 @@ Reference_Voltage:LM4040LP-4.1
Reference_Voltage:LM4040LP-5
Reference_Voltage:LM4040LP-8.2
Reference_Voltage:LM4041LP-ADJ
Reference_Voltage:LM4050xEM3-2.1
Reference_Voltage:LM4050xEM3-2.5
Reference_Voltage:LM4050xEM3-3.0
Reference_Voltage:LM4050xEM3-3.3
Reference_Voltage:LM4050xEM3-4.1
Reference_Voltage:LM4050xEM3-5.0
Reference_Voltage:LM4050xEX3-2.1
Reference_Voltage:LM4050xEX3-2.5
Reference_Voltage:LM4050xEX3-3.3
Reference_Voltage:LM4050xEX3-4.1
Reference_Voltage:LM4050xEX3-5.0
Reference_Voltage:LM4051xEM3-1.2
Reference_Voltage:LM4051xEX3-1.2
Reference_Voltage:LM4125AIM5-2.5
Reference_Voltage:LM4125IM5-2.0
Reference_Voltage:LM4125IM5-2.5
@ -16827,7 +16865,6 @@ Reference_Voltage:MCP1501-25xCH
Reference_Voltage:MCP1501-25xRW
Reference_Voltage:MCP1501-25xSN
Reference_Voltage:MCP1501-30xCH
Reference_Voltage:MCP1501-30xRW
Reference_Voltage:MCP1501-30xSN
Reference_Voltage:MCP1501-33xCH
Reference_Voltage:MCP1501-33xRW
@ -20988,6 +21025,7 @@ Sensor_Temperature:MCP9501
Sensor_Temperature:MCP9502
Sensor_Temperature:MCP9503
Sensor_Temperature:MCP9504
Sensor_Temperature:MCP96xx01x-x-MX
Sensor_Temperature:MCP9700Ax-ELT
Sensor_Temperature:MCP9700Ax-ETT
Sensor_Temperature:MCP9700Ax-HLT

View file

@ -1,3 +1,4 @@
# Ignore everything except this .gitignore
*
!.gitignore
!.gitignore
!.htaccess

10
public/media/.htaccess Normal file
View file

@ -0,0 +1,10 @@
# Deny access to PHP and PHP-like files to prevent remote code execution
<FilesMatch "(?i)\.(php[3-8]?|phar|phtml|pht|phps)$">
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
Order deny,allow
Deny from all
</IfModule>
</FilesMatch>

View file

@ -22,6 +22,7 @@ declare(strict_types=1);
*/
namespace App\Command;
use App\Services\System\AppSecretChecker;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
@ -33,7 +34,9 @@ use Symfony\Component\DependencyInjection\ParameterBag\ContainerBagInterface;
#[AsCommand('partdb:check-requirements', 'Checks if the requirements Part-DB needs or recommends are fulfilled.')]
class CheckRequirementsCommand extends Command
{
public function __construct(protected ContainerBagInterface $params)
public function __construct(protected ContainerBagInterface $params,
private readonly AppSecretChecker $appSecretChecker
)
{
parent::__construct();
}
@ -121,6 +124,16 @@ class CheckRequirementsCommand extends Command
$io->success('Debug mode disabled.');
}
//Check if APP_SECRET has been changed from the default
if ($io->isVerbose()) {
$io->comment('Checking APP_SECRET...');
}
if ($this->appSecretChecker->isInsecureSecret()) {
$io->warning('APP_SECRET is set to the default value shipped with Part-DB. This is a security risk! Generate a new secret (e.g. using "openssl rand -hex 32") and set it as APP_SECRET in your .env.local file.');
} elseif (!$only_issues) {
$io->success('APP_SECRET has been changed from the default value.');
}
}
protected function checkPHPExtensions(SymfonyStyle $io, bool $only_issues = false): void

View file

@ -93,6 +93,8 @@ class AttachmentFileController extends AbstractController
//Set header content disposition, so that the file will be downloaded
$response->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $attachment->getFilename());
$this->setAttachmentCSPHeaders($response);
return $response;
}
@ -112,6 +114,16 @@ class AttachmentFileController extends AbstractController
//Set header content disposition, so that the file will be downloaded
$response->setContentDisposition(ResponseHeaderBag::DISPOSITION_INLINE, $attachment->getFilename());
$this->setAttachmentCSPHeaders($response);
return $response;
}
private function setAttachmentCSPHeaders(Response $response): Response
{
//Set an CSP that disallow to run any scripts, styles or images from the attachment render page, as it is not used anywhere else for now and can be a security risk if used without proper precautions, so it should be opt-in
$response->headers->set('Content-Security-Policy', "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; sandbox;");
return $response;
}

View file

@ -24,6 +24,7 @@ namespace App\Controller;
use App\DataTables\LogDataTable;
use App\Entity\Parts\Part;
use App\Services\System\AppSecretChecker;
use App\Services\System\BannerHelper;
use App\Services\System\GitVersionInfoProvider;
use App\Services\System\UpdateAvailableFacade;
@ -36,8 +37,11 @@ use Symfony\Component\Routing\Attribute\Route;
class HomepageController extends AbstractController
{
public function __construct(private readonly DataTableFactory $dataTable, private readonly BannerHelper $bannerHelper)
{
public function __construct(
private readonly DataTableFactory $dataTable,
private readonly BannerHelper $bannerHelper,
private readonly AppSecretChecker $appSecretChecker,
) {
}
@ -84,6 +88,8 @@ class HomepageController extends AbstractController
'new_version_available' => $updateAvailableManager->isUpdateAvailable(),
'new_version' => $updateAvailableManager->getLatestVersionString(),
'new_version_url' => $updateAvailableManager->getLatestVersionUrl(),
'insecure_app_secret' => $this->appSecretChecker->isInsecureSecret(),
'suggested_app_secret' => $this->appSecretChecker->isInsecureSecret() ? $this->appSecretChecker->generateSecret() : null,
]);
}
}

View file

@ -90,7 +90,7 @@ class PartImportExportController extends AbstractController
goto ret;
}
if (!isset($errors) || $errors) {
if (!isset($errors) || $errors) { //@phpstan-ignore-line
$this->addFlash('error', 'parts.import.flash.error');
} else {
$this->addFlash('success', 'parts.import.flash.success');

View file

@ -22,6 +22,7 @@ declare(strict_types=1);
namespace App\Controller;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Component\HttpFoundation\Response;
use App\Entity\ProjectSystem\Project;
use App\Entity\Parts\Category;
@ -55,7 +56,7 @@ class TreeController extends AbstractController
#[Route(path: '/category/{id}', name: 'tree_category')]
#[Route(path: '/categories', name: 'tree_category_root')]
public function categoryTree(?Category $category = null): JsonResponse
public function categoryTree(#[MapEntity(id: 'id')] ?Category $category = null): JsonResponse
{
if ($this->isGranted('@parts.read') && $this->isGranted('@categories.read')) {
$tree = $this->treeGenerator->getTreeView(Category::class, $category, 'list_parts_root');
@ -68,7 +69,7 @@ class TreeController extends AbstractController
#[Route(path: '/footprint/{id}', name: 'tree_footprint')]
#[Route(path: '/footprints', name: 'tree_footprint_root')]
public function footprintTree(?Footprint $footprint = null): JsonResponse
public function footprintTree(#[MapEntity(id: 'id')] ?Footprint $footprint = null): JsonResponse
{
if ($this->isGranted('@parts.read') && $this->isGranted('@footprints.read')) {
$tree = $this->treeGenerator->getTreeView(Footprint::class, $footprint, 'list_parts_root');
@ -80,7 +81,7 @@ class TreeController extends AbstractController
#[Route(path: '/location/{id}', name: 'tree_location')]
#[Route(path: '/locations', name: 'tree_location_root')]
public function locationTree(?StorageLocation $location = null): JsonResponse
public function locationTree(#[MapEntity(id: 'id')] ?StorageLocation $location = null): JsonResponse
{
if ($this->isGranted('@parts.read') && $this->isGranted('@storelocations.read')) {
$tree = $this->treeGenerator->getTreeView(StorageLocation::class, $location, 'list_parts_root');
@ -93,7 +94,7 @@ class TreeController extends AbstractController
#[Route(path: '/manufacturer/{id}', name: 'tree_manufacturer')]
#[Route(path: '/manufacturers', name: 'tree_manufacturer_root')]
public function manufacturerTree(?Manufacturer $manufacturer = null): JsonResponse
public function manufacturerTree(#[MapEntity(id: 'id')] ?Manufacturer $manufacturer = null): JsonResponse
{
if ($this->isGranted('@parts.read') && $this->isGranted('@manufacturers.read')) {
$tree = $this->treeGenerator->getTreeView(Manufacturer::class, $manufacturer, 'list_parts_root');
@ -106,7 +107,7 @@ class TreeController extends AbstractController
#[Route(path: '/supplier/{id}', name: 'tree_supplier')]
#[Route(path: '/suppliers', name: 'tree_supplier_root')]
public function supplierTree(?Supplier $supplier = null): JsonResponse
public function supplierTree(#[MapEntity(id: 'id')] ?Supplier $supplier = null): JsonResponse
{
if ($this->isGranted('@parts.read') && $this->isGranted('@suppliers.read')) {
$tree = $this->treeGenerator->getTreeView(Supplier::class, $supplier, 'list_parts_root');
@ -119,7 +120,7 @@ class TreeController extends AbstractController
#[Route(path: '/device/{id}', name: 'tree_device')]
#[Route(path: '/devices', name: 'tree_device_root')]
public function deviceTree(?Project $device = null): JsonResponse
public function deviceTree(#[MapEntity(id: 'id')] ?Project $device = null): JsonResponse
{
if ($this->isGranted('@projects.read')) {
$tree = $this->treeGenerator->getTreeView(Project::class, $device, 'devices');

View file

@ -22,6 +22,7 @@ declare(strict_types=1);
namespace App\DataTables;
use App\DataTables\Column\HTMLColumn;
use App\DataTables\Column\LocaleDateTimeColumn;
use App\DataTables\Column\PrettyBoolColumn;
use App\DataTables\Column\RowClassColumn;
@ -40,14 +41,19 @@ use Omines\DataTablesBundle\DataTable;
use Omines\DataTablesBundle\DataTableTypeInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
final class AttachmentDataTable implements DataTableTypeInterface
final readonly class AttachmentDataTable implements DataTableTypeInterface
{
public function __construct(private readonly TranslatorInterface $translator, private readonly EntityURLGenerator $entityURLGenerator, private readonly AttachmentManager $attachmentHelper, private readonly AttachmentURLGenerator $attachmentURLGenerator, private readonly ElementTypeNameGenerator $elementTypeNameGenerator)
public function __construct(private TranslatorInterface $translator, private EntityURLGenerator $entityURLGenerator, private AttachmentManager $attachmentHelper, private AttachmentURLGenerator $attachmentURLGenerator, private ElementTypeNameGenerator $elementTypeNameGenerator)
{
}
public function configure(DataTable $dataTable, array $options): void
{
/*************************************************************************************************************
* Avoid using render, as it has no escaping, and is a potential security risk. Use data on TextColumn or the
* HTMLColumn, if necessary
************************************************************************************************************/
$dataTable->add('dont_matter', RowClassColumn::class, [
'render' => function ($value, Attachment $context): string {
//Mark attachments yellow which have an internal file linked that doesn't exist
@ -59,10 +65,10 @@ final class AttachmentDataTable implements DataTableTypeInterface
},
]);
$dataTable->add('picture', TextColumn::class, [
$dataTable->add('picture', HTMLColumn::class, [
'label' => '',
'className' => 'no-colvis',
'render' => function ($value, Attachment $context): string {
'data' => function (Attachment $context): string {
if ($context->isPicture()
&& $this->attachmentHelper->isInternalFileExisting($context)) {
@ -95,65 +101,65 @@ final class AttachmentDataTable implements DataTableTypeInterface
'orderField' => 'NATSORT(attachment.name)',
]);
$dataTable->add('attachment_type', TextColumn::class, [
$dataTable->add('attachment_type', HTMLColumn::class, [
'label' => 'attachment.table.type',
'field' => 'attachment_type.name',
'orderField' => 'NATSORT(attachment_type.name)',
'render' => fn($value, Attachment $context): string => sprintf(
'data' => fn(Attachment $context, $value): string => sprintf(
'<a href="%s">%s</a>',
$this->entityURLGenerator->editURL($context->getAttachmentType()),
htmlspecialchars((string) $value)
),
]);
$dataTable->add('element', TextColumn::class, [
$dataTable->add('element', HTMLColumn::class, [
'label' => 'attachment.table.element',
//'propertyPath' => 'element.name',
'render' => fn($value, Attachment $context): string => sprintf(
'data' => fn(Attachment $context): string => sprintf(
'<a href="%s">%s</a>',
$this->entityURLGenerator->infoURL($context->getElement()),
$this->elementTypeNameGenerator->getTypeNameCombination($context->getElement(), true)
),
]);
$dataTable->add('internal_link', TextColumn::class, [
$dataTable->add('internal_link', HTMLColumn::class, [
'label' => 'attachment.table.internal_file',
'propertyPath' => 'filename',
'orderField' => 'NATSORT(attachment.original_filename)',
'render' => function ($value, Attachment $context) {
'data' => function (Attachment $context, $value) {
if ($this->attachmentHelper->isInternalFileExisting($context)) {
return sprintf(
'<a href="%s" target="_blank" data-no-ajax>%s</a>',
$this->entityURLGenerator->viewURL($context),
htmlspecialchars($value)
htmlspecialchars((string) $value)
);
}
return $value;
}
return htmlspecialchars((string) $value);
},
]);
$dataTable->add('external_link', TextColumn::class, [
$dataTable->add('external_link', HTMLColumn::class, [
'label' => 'attachment.table.external_link',
'propertyPath' => 'host',
'orderField' => 'attachment.external_path',
'render' => function ($value, Attachment $context) {
'data' => function (Attachment $context, $value) {
if ($context->hasExternal()) {
return sprintf(
'<a href="%s" class="link-external" title="%s" target="_blank" rel="noopener">%s</a>',
htmlspecialchars((string) $context->getExternalPath()),
htmlspecialchars((string) $context->getExternalPath()),
htmlspecialchars($value),
htmlspecialchars((string) $value),
);
}
return $value;
}
return htmlspecialchars((string) $value);
},
]);
$dataTable->add('filesize', TextColumn::class, [
$dataTable->add('filesize', HTMLColumn::class, [
'label' => $this->translator->trans('attachment.table.filesize'),
'render' => function ($value, Attachment $context) {
'data' => function (Attachment $context) {
if (!$context->hasInternal()) {
return sprintf(
'<span class="badge bg-primary">
@ -168,7 +174,7 @@ final class AttachmentDataTable implements DataTableTypeInterface
'<span class="badge bg-secondary">
<i class="fas fa-hdd fa-fw"></i> %s
</span>',
$this->attachmentHelper->getHumanFileSize($context)
htmlspecialchars($this->attachmentHelper->getHumanFileSize($context))
);
}

View file

@ -78,7 +78,7 @@ class EntityColumn extends AbstractColumn
);
}
return sprintf('<i>%s</i>', $value);
return sprintf('<i>%s</i>', htmlspecialchars($value));
}
return '';

View file

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2026 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/>.
*/
namespace App\DataTables\Column;
use Omines\DataTablesBundle\Column\TextColumn;
/**
* A TextColumn whose value is always treated as raw HTML and therefore never passed through htmlspecialchars().
* The value returned by the 'data' option must already contain properly escaped/sanitized HTML, as it is output as-is.
*/
class HTMLColumn extends TextColumn
{
public function isRaw(): bool
{
return true;
}
}

View file

@ -87,9 +87,9 @@ class IconLinkColumn extends AbstractColumn
return sprintf(
'<a class="btn btn-primary btn-sm %s" href="%s" title="%s"><i class="%s"></i></a>',
$disabled ? 'disabled' : '',
$href,
$title,
$icon
htmlspecialchars($href),
htmlspecialchars($title ?? ''),
htmlspecialchars($icon ?? '')
);
}

View file

@ -22,9 +22,9 @@ declare(strict_types=1);
*/
namespace App\DataTables;
use App\DataTables\Column\HTMLColumn;
use App\DataTables\Column\RowClassColumn;
use Omines\DataTablesBundle\Adapter\ArrayAdapter;
use Omines\DataTablesBundle\Column\TextColumn;
use Omines\DataTablesBundle\DataTable;
use Omines\DataTablesBundle\DataTableFactory;
use Omines\DataTablesBundle\DataTableTypeInterface;
@ -32,7 +32,7 @@ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ErrorDataTable implements DataTableTypeInterface
final readonly class ErrorDataTable implements DataTableTypeInterface
{
public function configureOptions(OptionsResolver $optionsResolver): void
{
@ -49,6 +49,11 @@ class ErrorDataTable implements DataTableTypeInterface
public function configure(DataTable $dataTable, array $options): void
{
/*************************************************************************************************************
* Avoid using render, as it has no escaping, and is a potential security risk. Use data on TextColumn or the
* HTMLColumn, if necessary
************************************************************************************************************/
$optionsResolver = new OptionsResolver();
$this->configureOptions($optionsResolver);
$options = $optionsResolver->resolve($options);
@ -58,9 +63,9 @@ class ErrorDataTable implements DataTableTypeInterface
'render' => fn($value, $context): string => 'table-warning',
])
->add('error', TextColumn::class, [
->add('error', HTMLColumn::class, [
'label' => 'error_table.error',
'render' => fn($value, $context): string => '<i class="fa-solid fa-triangle-exclamation fa-fw"></i> ' . $value,
'data' => fn($context, $value): string => '<i class="fa-solid fa-triangle-exclamation fa-fw"></i> ' . htmlspecialchars((string) $value),
])
;

View file

@ -62,7 +62,7 @@ class PartDataTableHelper
}
if ($context->getBuiltProject() instanceof Project) {
$icon = sprintf('<i class="fa-solid fa-box-archive fa-fw me-1" title="%s"></i>',
$this->translator->trans('part.info.projectBuildPart.hint').': '.$context->getBuiltProject()->getName());
$this->translator->trans('part.info.projectBuildPart.hint').': '.htmlspecialchars($context->getBuiltProject()->getName()));
}

View file

@ -25,6 +25,7 @@ namespace App\DataTables;
use App\DataTables\Column\EnumColumn;
use App\Entity\LogSystem\LogTargetType;
use Symfony\Bundle\SecurityBundle\Security;
use App\DataTables\Column\HTMLColumn;
use App\DataTables\Column\IconLinkColumn;
use App\DataTables\Column\LocaleDateTimeColumn;
use App\DataTables\Column\LogEntryExtraColumn;
@ -59,7 +60,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class LogDataTable implements DataTableTypeInterface
final readonly class LogDataTable implements DataTableTypeInterface
{
protected LogEntryRepository $logRepo;
@ -95,6 +96,11 @@ class LogDataTable implements DataTableTypeInterface
public function configure(DataTable $dataTable, array $options): void
{
/*************************************************************************************************************
* Avoid using render, as it has no escaping, and is a potential security risk. Use data on TextColumn or the
* HTMLColumn, if necessary
************************************************************************************************************/
$resolver = new OptionsResolver();
$this->configureOptions($resolver);
$options = $resolver->resolve($options);
@ -104,10 +110,10 @@ class LogDataTable implements DataTableTypeInterface
'render' => fn($value, AbstractLogEntry $context) => $this->logLevelHelper->logLevelToTableColorClass($context->getLevelString()),
]);
$dataTable->add('symbol', TextColumn::class, [
$dataTable->add('symbol', HTMLColumn::class, [
'label' => '',
'className' => 'no-colvis',
'render' => fn($value, AbstractLogEntry $context): string => sprintf(
'data' => fn(AbstractLogEntry $context): string => sprintf(
'<i class="fas fa-fw %s" title="%s"></i>',
$this->logLevelHelper->logLevelToIconClass($context->getLevelString()),
$context->getLevelString()
@ -128,10 +134,10 @@ class LogDataTable implements DataTableTypeInterface
)
]);
$dataTable->add('type', TextColumn::class, [
$dataTable->add('type', HTMLColumn::class, [
'label' => 'log.type',
'propertyPath' => 'type',
'render' => function (string $value, AbstractLogEntry $context) {
'data' => function (AbstractLogEntry $context, string $value) {
$text = $this->translator->trans('log.type.'.$value);
if ($context instanceof PartStockChangedLogEntry) {
@ -149,20 +155,20 @@ class LogDataTable implements DataTableTypeInterface
'label' => 'log.level',
'visible' => 'system_log' === $options['mode'],
'propertyPath' => 'levelString',
'render' => fn(string $value, AbstractLogEntry $context) => $this->translator->trans('log.level.'.$value),
'data' => fn(AbstractLogEntry $context, string $value) => $this->translator->trans('log.level.'.$value),
]);
$dataTable->add('user', TextColumn::class, [
$dataTable->add('user', HTMLColumn::class, [
'label' => 'log.user',
'orderField' => 'NATSORT(user.name)',
'render' => function ($value, AbstractLogEntry $context): string {
'data' => function (AbstractLogEntry $context): string {
$user = $context->getUser();
//If user was deleted, show the info from the username field
if (!$user instanceof User) {
if ($context->isCLIEntry()) {
return sprintf('%s [%s]',
htmlentities((string) $context->getCLIUsername()),
htmlspecialchars((string) $context->getCLIUsername()),
$this->translator->trans('log.cli_user')
);
}
@ -170,7 +176,7 @@ class LogDataTable implements DataTableTypeInterface
//Else we just deal with a deleted user
return sprintf(
'@%s [%s]',
htmlentities($context->getUsername()),
htmlspecialchars($context->getUsername()),
$this->translator->trans('log.target_deleted'),
);
}
@ -182,7 +188,7 @@ class LogDataTable implements DataTableTypeInterface
$img_url,
$this->userAvatarHelper->getAvatarMdURL($user),
$this->urlGenerator->generate('user_info', ['id' => $user->getID()]),
htmlentities($user->getFullName(true))
htmlspecialchars($user->getFullName(true))
);
},
]);
@ -194,7 +200,7 @@ class LogDataTable implements DataTableTypeInterface
'render' => function (LogTargetType $value, AbstractLogEntry $context) {
$class = $value->toClass();
if (null !== $class) {
return $this->elementTypeNameGenerator->getLocalizedTypeLabel($class);
return $this->elementTypeNameGenerator->typeLabel($class);
}
return '';
@ -216,9 +222,9 @@ class LogDataTable implements DataTableTypeInterface
'icon' => 'fas fa-fw fa-eye',
'href' => function ($value, AbstractLogEntry $context) {
if (
$context instanceof CollectionElementDeleted ||
($context instanceof TimeTravelInterface
&& $context->hasOldDataInformation())
|| $context instanceof CollectionElementDeleted
) {
try {
$target = $this->logRepo->getTargetElement($context);

View file

@ -25,6 +25,7 @@ namespace App\DataTables;
use App\DataTables\Adapters\TwoStepORMAdapter;
use App\DataTables\Column\EntityColumn;
use App\DataTables\Column\EnumColumn;
use App\DataTables\Column\HTMLColumn;
use App\DataTables\Column\IconLinkColumn;
use App\DataTables\Column\LocaleDateTimeColumn;
use App\DataTables\Column\MarkdownColumn;
@ -58,7 +59,7 @@ use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface;
final class PartsDataTable implements DataTableTypeInterface
final readonly class PartsDataTable implements DataTableTypeInterface
{
public const LENGTH_MENU = [[10, 25, 50, 100, 250, 500, -1], [10, 25, 50, 100, 250, 500, "All"]];
@ -94,6 +95,11 @@ final class PartsDataTable implements DataTableTypeInterface
* When adding columns here, add them also to PartTableColumns enum, to make them configurable in the settings!
*************************************************************************************************************/
/*************************************************************************************************************
* Avoid using render, as it has no escaping, and is a potential security risk. Use data on TextColumn or the
* HTMLColumn, if necessary
************************************************************************************************************/
$this->csh
//Color the table rows depending on the review and favorite status
->add('row_color', RowClassColumn::class, [
@ -109,23 +115,23 @@ final class PartsDataTable implements DataTableTypeInterface
},
], visibility_configurable: false)
->add('select', SelectColumn::class, visibility_configurable: false)
->add('picture', TextColumn::class, [
->add('picture', HTMLColumn::class, [
'label' => '',
'className' => 'no-colvis',
'render' => fn($value, Part $context) => $this->partDataTableHelper->renderPicture($context),
'data' => fn(Part $context) => $this->partDataTableHelper->renderPicture($context),
], visibility_configurable: false)
->add('name', TextColumn::class, [
->add('name', HTMLColumn::class, [
'label' => $this->translator->trans('part.table.name'),
'render' => fn($value, Part $context) => $this->partDataTableHelper->renderName($context),
'data' => fn(Part $context) => $this->partDataTableHelper->renderName($context),
'orderField' => 'NATSORT(part.name)'
])
->add('si_value', TextColumn::class, [
'label' => $this->translator->trans('part.table.si_value'),
'render' => function ($value, Part $context): string {
'data' => function (Part $context): string {
$siValue = SiValueSort::sqliteSiValue($context->getName());
if ($siValue !== null) {
//Output it as scientific number with a big E
return htmlspecialchars(sprintf('%G', $siValue));
return sprintf('%G', $siValue);
}
return '';
},
@ -156,24 +162,24 @@ final class PartsDataTable implements DataTableTypeInterface
'label' => $this->translator->trans('part.table.manufacturer'),
'orderField' => 'NATSORT(_manufacturer.name)'
])
->add('storelocation', TextColumn::class, [
->add('storelocation', HTMLColumn::class, [
'label' => $this->translator->trans('part.table.storeLocations'),
//We need to use a aggregate function to get the first store location, as we have a one-to-many relation
'orderField' => 'NATSORT(MIN(_storelocations.name))',
'render' => fn($value, Part $context) => $this->partDataTableHelper->renderStorageLocations($context),
'data' => fn(Part $context) => $this->partDataTableHelper->renderStorageLocations($context),
], alias: 'storage_location')
->add('amount', TextColumn::class, [
->add('amount', HTMLColumn::class, [
'label' => $this->translator->trans('part.table.amount'),
'render' => fn($value, Part $context) => $this->partDataTableHelper->renderAmount($context),
'data' => fn(Part $context) => $this->partDataTableHelper->renderAmount($context),
'orderField' => 'amountSum'
])
->add('minamount', TextColumn::class, [
'label' => $this->translator->trans('part.table.minamount'),
'render' => fn($value, Part $context): string => htmlspecialchars($this->amountFormatter->format(
'data' => fn(Part $context, $value): string => $this->amountFormatter->format(
$value,
$context->getPartUnit()
)),
),
])
->add('orderamount', TextColumn::class, [
'label' => $this->translator->trans('part.table.orderamount'),
@ -187,16 +193,16 @@ final class PartsDataTable implements DataTableTypeInterface
->add('partUnit', TextColumn::class, [
'label' => $this->translator->trans('part.table.partUnit'),
'orderField' => 'NATSORT(_partUnit.name)',
'render' => function ($value, Part $context): string {
'data' => function (Part $context): string {
$partUnit = $context->getPartUnit();
if ($partUnit === null) {
return '';
}
$tmp = htmlspecialchars($partUnit->getName());
$tmp = $partUnit->getName();
if ($partUnit->getUnit()) {
$tmp .= ' (' . htmlspecialchars($partUnit->getUnit()) . ')';
$tmp .= ' (' . $partUnit->getUnit() . ')';
}
return $tmp;
}
@ -204,14 +210,14 @@ final class PartsDataTable implements DataTableTypeInterface
->add('partCustomState', TextColumn::class, [
'label' => $this->translator->trans('part.table.partCustomState'),
'orderField' => 'NATSORT(_partCustomState.name)',
'render' => function($value, Part $context): string {
'data' => function(Part $context): string {
$partCustomState = $context->getPartCustomState();
if ($partCustomState === null) {
return '';
}
return htmlspecialchars($partCustomState->getName());
return $partCustomState->getName();
}
])
->add('addedDate', LocaleDateTimeColumn::class, [
@ -257,25 +263,25 @@ final class PartsDataTable implements DataTableTypeInterface
])
->add('eda_reference', TextColumn::class, [
'label' => $this->translator->trans('part.table.eda_reference'),
'render' => static fn($value, Part $context) => htmlspecialchars($context->getEdaInfo()->getReferencePrefix() ?? ''),
'data' => static fn(Part $context) => $context->getEdaInfo()->getReferencePrefix() ?? '',
'orderField' => 'NATSORT(part.eda_info.reference_prefix)'
])
->add('eda_value', TextColumn::class, [
'label' => $this->translator->trans('part.table.eda_value'),
'render' => static fn($value, Part $context) => htmlspecialchars($context->getEdaInfo()->getValue() ?? ''),
'data' => static fn(Part $context) => $context->getEdaInfo()->getValue() ?? '',
'orderField' => 'NATSORT(part.eda_info.value)'
])
->add('eda_status', TextColumn::class, [
->add('eda_status', HTMLColumn::class, [
'label' => $this->translator->trans('part.table.eda_status'),
'render' => fn($value, Part $context) => $this->partDataTableHelper->renderEdaStatus($context),
'data' => fn(Part $context) => $this->partDataTableHelper->renderEdaStatus($context),
'className' => 'text-center',
]);
//Add a column to list the projects where the part is used, when the user has the permission to see the projects
if ($this->security->isGranted('read', Project::class)) {
$this->csh->add('projects', TextColumn::class, [
$this->csh->add('projects', HTMLColumn::class, [
'label' => $this->translator->trans('project.labelp'),
'render' => function ($value, Part $context): string {
'data' => function (Part $context): string {
//Only show the first 5 projects names
$projects = $context->getProjects();
$tmp = "";
@ -295,7 +301,7 @@ final class PartsDataTable implements DataTableTypeInterface
}
return $tmp;
}
},
]);
}

View file

@ -25,6 +25,7 @@ namespace App\DataTables;
use App\DataTables\Adapters\TwoStepORMAdapter;
use App\DataTables\Column\EntityColumn;
use App\DataTables\Column\EnumColumn;
use App\DataTables\Column\HTMLColumn;
use App\DataTables\Column\LocaleDateTimeColumn;
use App\DataTables\Column\MarkdownColumn;
use App\DataTables\Helpers\PartDataTableHelper;
@ -48,7 +49,7 @@ use Omines\DataTablesBundle\DataTable;
use Omines\DataTablesBundle\DataTableTypeInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class ProjectBomEntriesDataTable implements DataTableTypeInterface
final readonly class ProjectBomEntriesDataTable implements DataTableTypeInterface
{
public function __construct(
protected EntityURLGenerator $entityURLGenerator,
@ -63,17 +64,22 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
public function configure(DataTable $dataTable, array $options): void
{
/*************************************************************************************************************
* Avoid using render, as it has no escaping, and is a potential security risk. Use data on TextColumn or the
* HTMLColumn, if necessary
************************************************************************************************************/
$dataTable
//->add('select', SelectColumn::class)
->add('picture', TextColumn::class, [
->add('picture', HTMLColumn::class, [
'label' => '',
'className' => 'no-colvis',
'render' => function ($value, ProjectBOMEntry $context) {
'data' => function (ProjectBOMEntry $context) {
if(!$context->getPart() instanceof Part) {
return '';
}
return $this->partDataTableHelper->renderPicture($context->getPart());
}
},
])
->add('id', TextColumn::class, [
@ -85,27 +91,27 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
'label' => $this->translator->trans('project.bom.quantity'),
'className' => 'text-center',
'orderField' => 'bom_entry.quantity',
'render' => function ($value, ProjectBOMEntry $context): float|string {
'data' => function (ProjectBOMEntry $context): float|string {
//If we have a non-part entry, only show the rounded quantity
if (!$context->getPart() instanceof Part) {
return round($context->getQuantity());
}
//Otherwise use the unit of the part to format the quantity
return htmlspecialchars($this->amountFormatter->format($context->getQuantity(), $context->getPart()->getPartUnit()));
return $this->amountFormatter->format($context->getQuantity(), $context->getPart()->getPartUnit());
},
])
->add('partId', TextColumn::class, [
'label' => $this->translator->trans('project.bom.part_id'),
'visible' => true,
'orderField' => 'part.id',
'render' => function ($value, ProjectBOMEntry $context) {
'data' => function (ProjectBOMEntry $context) {
return $context->getPart() instanceof Part ? (string) $context->getPart()->getId() : '';
},
])
->add('name', TextColumn::class, [
->add('name', HTMLColumn::class, [
'label' => $this->translator->trans('part.table.name'),
'orderField' => 'NATSORT(part.name)',
'render' => function ($value, ProjectBOMEntry $context) {
'data' => function (ProjectBOMEntry $context) {
if(!$context->getPart() instanceof Part) {
return htmlspecialchars((string) $context->getName());
}
@ -123,11 +129,7 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
'label' => $this->translator->trans('part.table.ipn'),
'orderField' => 'NATSORT(part.ipn)',
'visible' => false,
'render' => function ($value, ProjectBOMEntry $context) {
if($context->getPart() instanceof Part) {
return $context->getPart()->getIpn();
}
}
'data' => fn (ProjectBOMEntry $context) => $context->getPart()?->getIpn()
])
->add('description', MarkdownColumn::class, [
'label' => $this->translator->trans('part.table.description'),
@ -172,9 +174,9 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
},
])
->add('mountnames', TextColumn::class, [
->add('mountnames', HTMLColumn::class, [
'label' => 'project.bom.mountnames',
'render' => function ($value, ProjectBOMEntry $context) {
'data' => function (ProjectBOMEntry $context) {
$html = '';
foreach (explode(',', $context->getMountnames()) as $mountname) {
@ -184,58 +186,58 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
},
])
->add('instockAmount', TextColumn::class, [
->add('instockAmount', HTMLColumn::class, [
'label' => 'project.bom.instockAmount',
'visible' => false,
'render' => function ($value, ProjectBOMEntry $context) {
'data' => function (ProjectBOMEntry $context) {
if ($context->getPart() !== null) {
return $this->partDataTableHelper->renderAmount($context->getPart());
}
return '';
}
},
])
->add('minAmount', TextColumn::class, [
->add('minAmount', HTMLColumn::class, [
'label' => $this->translator->trans('part.table.minamount'),
'visible' => false,
'orderField' => 'part.minamount',
'render' => function ($value, ProjectBOMEntry $context): string {
'data' => function (ProjectBOMEntry $context): string {
if (!$context->getPart() instanceof Part) {
return '';
}
return htmlspecialchars($this->amountFormatter->format($context->getPart()->getMinAmount(), $context->getPart()->getPartUnit()));
return $this->amountFormatter->format($context->getPart()->getMinAmount(), $context->getPart()->getPartUnit());
},
])
->add('orderAmount', TextColumn::class, [
->add('orderAmount', HTMLColumn::class, [
'label' => $this->translator->trans('part.table.orderamount'),
'visible' => false,
'orderField' => 'part.orderamount',
'render' => function ($value, ProjectBOMEntry $context): string {
'data' => function (ProjectBOMEntry $context): string {
if (!$context->getPart() instanceof Part) {
return '';
}
return htmlspecialchars($this->amountFormatter->format($context->getPart()->getOrderAmount(), $context->getPart()->getPartUnit()));
return $this->amountFormatter->format($context->getPart()->getOrderAmount(), $context->getPart()->getPartUnit());
},
])
->add('storelocation', TextColumn::class, [
->add('storelocation', HTMLColumn::class, [
'label' => $this->translator->trans('part.table.storeLocations'),
//We need to use a aggregate function to get the first store location, as we have a one-to-many relation
'orderField' => 'NATSORT(MIN(_storelocations.name))',
'visible' => false,
'render' => function ($value, ProjectBOMEntry $context) {
'data' => function (ProjectBOMEntry $context) {
if ($context->getPart() !== null) {
return $this->partDataTableHelper->renderStorageLocations($context->getPart());
}
return '';
}
},
])
->add('price', TextColumn::class, [
'label' => 'project.bom.price',
'visible' => false,
'render' => function ($value, ProjectBOMEntry $context) {
'data' => function (ProjectBOMEntry $context) {
$price = $this->projectBuildHelper->getEntryUnitPrice($context);
return $this->moneyFormatter->format($price->toScale(2, RoundingMode::Up)->toFloat(), null, 2, true);
},
@ -243,7 +245,7 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
->add('ext_price', TextColumn::class, [
'label' => 'project.bom.ext_price',
'visible' => false,
'render' => function ($value, ProjectBOMEntry $context) {
'data' => function (ProjectBOMEntry $context) {
$price = $this->projectBuildHelper->getEntryUnitPrice($context);
return $this->moneyFormatter->format(
$price->multipliedBy(BigDecimal::fromFloatShortest($context->getQuantity()))

View file

@ -35,8 +35,10 @@ class SetSQLModeMiddlewareDriver extends AbstractDriverMiddleware
{
//Only set this on MySQL connections, as other databases don't support this parameter
if($params['driver'] === 'pdo_mysql') {
//1002 is \PDO::MYSQL_ATTR_INIT_COMMAND constant value
$params['driverOptions'][\PDO::MYSQL_ATTR_INIT_COMMAND] = 'SET SESSION sql_mode=(SELECT REPLACE(@@sql_mode, \'ONLY_FULL_GROUP_BY\', \'\'))';
//PDO::MYSQL_ATTR_INIT_COMMAND is deprecated since PHP 8.5 in favor of Pdo\Mysql::ATTR_INIT_COMMAND,
//but the Pdo\Mysql class only exists since PHP 8.4. Both constants have the same value (1002).
$initCommandAttr = class_exists(\Pdo\Mysql::class) ? \Pdo\Mysql::ATTR_INIT_COMMAND : \PDO::MYSQL_ATTR_INIT_COMMAND;
$params['driverOptions'][$initCommandAttr] = 'SET SESSION sql_mode=(SELECT REPLACE(@@sql_mode, \'ONLY_FULL_GROUP_BY\', \'\'))';
}
return parent::connect($params);

View file

@ -80,7 +80,7 @@ use Symfony\Component\Validator\Constraints as Assert;
)
],
uriVariables: [
'id' => new Link(fromProperty: 'children', fromClass: Manufacturer::class)
'id' => new Link(fromProperty: 'children', fromClass: StorageLocation::class)
],
normalizationContext: ['groups' => ['location:read', 'api:basic:read'], 'openapi_definition_name' => 'Read']
)]

View file

@ -69,7 +69,7 @@ class AttachmentSubmitHandler
protected const BLACKLISTED_EXTENSIONS = ['php', 'phtml', 'php3', 'ph3', 'php4', 'ph4', 'php5', 'ph5', 'phtm', 'sh',
'asp', 'cgi', 'py', 'pl', 'exe', 'aspx', 'js', 'mjs', 'jsp', 'css', 'jar', 'html', 'htm', 'shtm', 'shtml', 'htaccess',
'htpasswd', ''];
'htpasswd', 'phar', 'phps', ''];
public function __construct(
protected AttachmentPathResolver $pathResolver,
@ -543,8 +543,10 @@ class AttachmentSubmitHandler
return $attachment;
}
$guessed_mime_type = $this->mimeTypes->guessMimeType($path);
//Check if the file is an SVG
if ($attachment->getExtension() === "svg") {
if ($guessed_mime_type === "image/svg+xml" || $attachment->getExtension() === "svg") {
$this->SVGSanitizer->sanitizeFile($path);
}

View file

@ -29,13 +29,13 @@ use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Service for validating BOM import data with comprehensive validation rules
* and user-friendly error messages.
* and user-friendly error messages. The results are not HTML safe, and must be escaped before display!
*/
class BOMValidationService
readonly class BOMValidationService
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly TranslatorInterface $translator
private EntityManagerInterface $entityManager,
private TranslatorInterface $translator
) {
}
@ -473,4 +473,4 @@ class BOMValidationService
: 0,
];
}
}
}

View file

@ -169,8 +169,8 @@ class LogEntryExtraFormatter
$array['log.collection_deleted.deleted'] = sprintf(
'%s: %s (%s)',
$this->elementTypeNameGenerator->getLocalizedTypeLabel($context->getDeletedElementClass()),
$context->getOldName() ?? (string) $context->getDeletedElementID(),
$context->getCollectionName()
htmlspecialchars($context->getOldName() ?? (string) $context->getDeletedElementID()),
htmlspecialchars($context->getCollectionName())
);
}
@ -218,4 +218,4 @@ class LogEntryExtraFormatter
return implode(', ', $output);
}
}
}

View file

@ -0,0 +1,63 @@
<?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\Services\System;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
/**
* Checks whether APP_SECRET has been changed from the default value shipped with Part-DB.
*/
final readonly class AppSecretChecker
{
/** Known default/example secrets that must not be used in production. */
public const INSECURE_SECRETS = [
'a03498528f5a5fc089273ec9ae5b2849', // default in .env
'318b5d659e07a0b3f96d9b3a83b254ca', // default in .env.dev
'CHANGE_ME' //example secret used in documentation and error messages
];
public function __construct(
#[Autowire('%kernel.secret%')]
private string $appSecret,
) {
}
/**
* @return bool True if the app secret is one of the known insecure default secrets, false otherwise.
*/
public function isInsecureSecret(): bool
{
return in_array($this->appSecret, self::INSECURE_SECRETS, true);
}
/**
* Generates a new random app secret that can be used to replace the default insecure one.
* @return string
* @throws \Random\RandomException
*/
public function generateSecret(): string
{
//Symfony docs recommend 32 characters for the app secret, which are 16 random bytes when hex-encoded.
return bin2hex(random_bytes(16));
}
}

View file

@ -85,6 +85,16 @@
{% block content %}
{% if insecure_app_secret and is_granted('@system.server_infos') %}
<div class="alert alert-warning" role="alert">
<h5><i class="fa-solid fa-triangle-exclamation fa-fw"></i> {% trans %}system.app_secret.insecure.title{% endtrans %}</h5>
<p class="mb-1">{% trans %}system.app_secret.insecure.message{% endtrans %}</p>
<p class="mb-0">{% trans %}system.app_secret.insecure.suggestion{% endtrans %}
<br><code>APP_SECRET={{ suggested_app_secret }}</code></p>
<small>{% trans %}update_manager.new_version_available.only_administrators_can_see{% endtrans %}</small>
</div>
{% endif %}
{% if is_granted('@system.show_updates') %}
{{ nv.new_version_alert(new_version_available, new_version, new_version_url) }}
{% endif %}

View file

@ -58,7 +58,7 @@
</tr>
<tr>
<td>{% trans %}log.target{% endtrans %}</td>
<td>{{ target_html|raw }}</td>
<td>{{ target_html|sanitize_html }}</td>
</tr>
</table>
@ -111,7 +111,7 @@
{% elseif log_entry is instanceof('App\\Entity\\LogSystem\\CollectionElementDeleted') %}
{% include "log_system/details/_extra_collection_element_deleted.html.twig" %}
{% else %}
{{ extra_html | raw }}
{{ extra_html | sanitize_html }}
{% endif %}
</div>
{% endblock %}
{% endblock %}

View file

@ -68,7 +68,7 @@
<p class="mb-2">{% trans %}project.bom_import.validation.errors.description{% endtrans %}</p>
<ul class="mb-0">
{% for error in validation_result.errors %}
<li>{{ error|raw }}</li>
<li>{{ error }}</li>
{% endfor %}
</ul>
</div>
@ -80,7 +80,7 @@
<p class="mb-2">{% trans %}project.bom_import.validation.warnings.description{% endtrans %}</p>
<ul class="mb-0">
{% for warning in validation_result.warnings %}
<li>{{ warning|raw }}</li>
<li>{{ warning }}</li>
{% endfor %}
</ul>
</div>
@ -91,7 +91,7 @@
<h4><i class="fa-solid fa-info-circle fa-fw"></i> {% trans %}project.bom_import.validation.info.title{% endtrans %}</h4>
<ul class="mb-0">
{% for info in validation_result.info %}
<li>{{ info|raw }}</li>
<li>{{ info }}</li>
{% endfor %}
</ul>
</div>
@ -139,21 +139,21 @@
{% if line_result.errors is not empty %}
<div class="text-danger">
{% for error in line_result.errors %}
<div><i class="fa-solid fa-exclamation-triangle fa-fw"></i> {{ error|raw }}</div>
<div><i class="fa-solid fa-exclamation-triangle fa-fw"></i> {{ error }}</div>
{% endfor %}
</div>
{% endif %}
{% if line_result.warnings is not empty %}
<div class="text-warning">
{% for warning in line_result.warnings %}
<div><i class="fa-solid fa-exclamation-circle fa-fw"></i> {{ warning|raw }}</div>
<div><i class="fa-solid fa-exclamation-circle fa-fw"></i> {{ warning }}</div>
{% endfor %}
</div>
{% endif %}
{% if line_result.info is not empty %}
<div class="text-info">
{% for info in line_result.info %}
<div><i class="fa-solid fa-info-circle fa-fw"></i> {{ info|raw }}</div>
<div><i class="fa-solid fa-info-circle fa-fw"></i> {{ info }}</div>
{% endfor %}
</div>
{% endif %}

View file

@ -132,8 +132,8 @@
<script nonce="{{ csp_nonce('script') }}">
// Function to initialize the field mapping page
function initializeFieldMapping() {
const suggestions = {{ suggested_mapping|json_encode|raw }};
const fieldNameMapping = {{ field_name_mapping|json_encode|raw }};
const suggestions = JSON.parse("{{ suggested_mapping|json_encode|escape('js')}}");
const fieldNameMapping = JSON.parse("{{ field_name_mapping|json_encode|escape('js') }}");
Object.keys(suggestions).forEach(function(field) {
// Use the sanitized field name from the server-side mapping

View file

@ -64,7 +64,7 @@
<unit id="KSFhj_3" name="currency.iso_code.caption">
<segment state="translated">
<source>currency.iso_code.caption</source>
<target>ISO Code</target>
<target>ISO-Code</target>
</segment>
</unit>
<unit id="UqMuNZQ" name="currency.symbol.caption">
@ -13677,5 +13677,23 @@ Buerklin-API-Authentication-Server:
<target>Aktiviert das Browser-Plugin für diese Instanz.</target>
</segment>
</unit>
<unit id="JqcvhX_" name="system.app_secret.insecure.title">
<segment state="translated">
<source>system.app_secret.insecure.title</source>
<target>Unsicheres APP_SECRET</target>
</segment>
</unit>
<unit id="XSSYWTJ" name="system.app_secret.insecure.message">
<segment state="translated">
<source>system.app_secret.insecure.message</source>
<target>Die Umgebungsvariable &lt;code&gt;APP_SECRET&lt;/code&gt; ist auf den Standardwert gesetzt, der mit Part-DB ausgeliefert wird. Dies stellt ein Sicherheitsrisiko dar! Setzen Sie sie in Ihrer Datei &lt;code&gt;.env.local&lt;/code&gt; oder &lt;code&gt;docker-compose.yaml&lt;/code&gt; auf einen neuen Zufallswert.</target>
</segment>
</unit>
<unit id="e7.f3m0" name="system.app_secret.insecure.suggestion">
<segment state="translated">
<source>system.app_secret.insecure.suggestion</source>
<target>Sie können diesen zufällig generierten Wert verwenden (geben Sie ihn niemandem weiter):</target>
</segment>
</unit>
</file>
</xliff>

View file

@ -13679,5 +13679,23 @@ Buerklin-API Authentication server:
<target>When enabled users with the info provider permission can submit pages to Part-DB and retrieve them later.</target>
</segment>
</unit>
<unit id="JqcvhX_" name="system.app_secret.insecure.title">
<segment state="translated">
<source>system.app_secret.insecure.title</source>
<target>Insecure APP_SECRET</target>
</segment>
</unit>
<unit id="XSSYWTJ" name="system.app_secret.insecure.message">
<segment state="translated">
<source>system.app_secret.insecure.message</source>
<target>The &lt;code&gt;APP_SECRET&lt;/code&gt; environment variable is set to the default value shipped with Part-DB. This is a security risk! Set it to a new random value in your &lt;code&gt;.env.local&lt;/code&gt; or &lt;code&gt;docker-compose.yaml&lt;/code&gt; file.</target>
</segment>
</unit>
<unit id="e7.f3m0" name="system.app_secret.insecure.suggestion">
<segment state="translated">
<source>system.app_secret.insecure.suggestion</source>
<target>You can use this randomly generated value (share it with nobody):</target>
</segment>
</unit>
</file>
</xliff>

1428
yarn.lock

File diff suppressed because it is too large Load diff