Compare commits

...

37 commits

Author SHA1 Message Date
github-actions[bot]
5c7bf673ee Update KiCad symbols and footprints lists [no ci] 2026-07-20 05:47:16 +00:00
Jan Böhmer
12b53f15ba Fixed phpunit tests
Some checks failed
Build assets artifact / Build assets artifact (push) Has been cancelled
Docker Image Build / build (linux/amd64, amd64, ubuntu-latest) (push) Has been cancelled
Docker Image Build / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/amd64, amd64, ubuntu-latest) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Has been cancelled
Static analysis / Static analysis (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, sqlite) (push) Has been cancelled
Docker Image Build / merge (push) Has been cancelled
Docker Image Build (FrankenPHP) / merge (push) Has been cancelled
2026-07-20 00:04:55 +02:00
Jan Böhmer
07d106e44d Fixed exception in LCSCProvider 2026-07-20 00:02:13 +02:00
Jan Böhmer
16bb09c153 Merge remote-tracking branch 'origin/master' 2026-07-19 23:55:10 +02:00
Jan Böhmer
9504553e88 Bumped version to 2.13.3 2026-07-19 23:55:07 +02:00
Jan Böhmer
ad539e00a7
New Crowdin updates (#1451)
* New translations messages.en.xlf (English)

[ci skip]

* New translations messages.en.xlf (German)

[ci skip]
2026-07-19 23:54:26 +02:00
Jan Böhmer
c6983c6516 Fixed LCSC provider search
Fixes issue #1440
2026-07-19 23:52:39 +02:00
Jan Böhmer
a47e56bd93 Only disable translation for canopy choice options not the label
Fixes issue #1386
2026-07-19 23:11:33 +02:00
Jan Böhmer
65631dde4c Ensure that the Password reset link is generated on a trusted domain, and cannot be manipulated by an attacker by setting the host header 2026-07-19 23:05:02 +02:00
Jan Böhmer
3f7dd59dd1 Use debian 13 with php8.4 as base for direct debian installation 2026-07-19 22:07:40 +02:00
Jan Böhmer
9fb69bfc03 Added documentation about the TRUSTED_HOSTS env and add it to installation guides 2026-07-19 21:40:26 +02:00
Jan Böhmer
43ac8c23c7 Show a warning banner on the homepage recommending to configure the trusted headers env
Some checks are pending
Build assets artifact / Build assets artifact (push) Waiting to run
Docker Image Build / build (linux/amd64, amd64, ubuntu-latest) (push) Waiting to run
Docker Image Build / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build / merge (push) Blocked by required conditions
Docker Image Build (FrankenPHP) / build (linux/amd64, amd64, ubuntu-latest) (push) Waiting to run
Docker Image Build (FrankenPHP) / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build (FrankenPHP) / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build (FrankenPHP) / merge (push) Blocked by required conditions
Static analysis / Static analysis (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, sqlite) (push) Waiting to run
2026-07-19 16:46:46 +02:00
Jan Böhmer
e80e4a1bcd Show an specific error message, if an requests violates the trusted host policy 2026-07-19 16:30:36 +02:00
Jan Böhmer
e23dde8208 Added korean as selectable language 2026-07-19 15:54:28 +02:00
Jan Böhmer
c8a1670f0b Merge remote-tracking branch 'origin/master' 2026-07-19 15:51:30 +02:00
Jan Böhmer
7edbf35e46
New Crowdin updates (#1450)
* New translations messages.en.xlf (Korean)

[ci skip]

* New translations validators.en.xlf (Korean)

[ci skip]

* New translations security.en.xlf (Korean)

[ci skip]

* New translations frontend.en.xlf (Korean)

[ci skip]
2026-07-19 15:43:25 +02:00
Jan Böhmer
30aba1db6f Updated ai libraries to latest version 2026-07-19 14:25:15 +02:00
Jan Böhmer
dcc431d28c Properly HTML escape fields for label template placeholders
Fixes issue #1446
2026-07-19 14:03:14 +02:00
Jan Böhmer
5054642d41 Updated dependencies 2026-07-19 13:47:11 +02:00
Jan Böhmer
1a8ff831ab Bumped version to 2.13.2
Some checks failed
Build assets artifact / Build assets artifact (push) Has been cancelled
Docker Image Build / build (linux/amd64, amd64, ubuntu-latest) (push) Has been cancelled
Docker Image Build / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/amd64, amd64, ubuntu-latest) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Has been cancelled
Static analysis / Static analysis (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, sqlite) (push) Has been cancelled
Docker Image Build / merge (push) Has been cancelled
Docker Image Build (FrankenPHP) / merge (push) Has been cancelled
2026-07-17 00:09:58 +02:00
Jan Böhmer
8161e9af87 Fixed phpstan issue 2026-07-17 00:09:42 +02:00
Jan Böhmer
c9a1bb12a5 Merge remote-tracking branch 'origin/master' 2026-07-17 00:08:46 +02:00
Jan Böhmer
92f62f69bd Updated dependencies 2026-07-17 00:08:44 +02:00
Alex
a396cd0f58
Clarify library addition as HTTP in KiCad documentation (#1438)
Updated instructions to specify adding the file as a HTTP library.
2026-07-17 00:07:52 +02:00
Jan Böhmer
fc2cd5dc62
New Crowdin updates (#1432)
* New translations messages.en.xlf (English)

[ci skip]

* New translations messages.en.xlf (English)

[ci skip]
2026-07-17 00:07:26 +02:00
Jan Böhmer
4060484248
Update KiCad symbols and footprints lists (#1437)
* Update KiCad symbols and footprints lists

* Update KiCad symbols and footprints lists

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-17 00:06:59 +02:00
Jan Böhmer
1b740ed674 Fixed apache CSP policy to exclude index.php served requests
When using mod_php the setifempty directive does not work , which blocks assets from loading. Fixes issue #1423
2026-07-17 00:05:27 +02:00
Jan Böhmer
90a5ab74ff Added docker environment emulating a manual installed Part-DB on debian
Used to debug issues like #1423
2026-07-16 23:51:02 +02:00
Jan Böhmer
805319c6b0 Updated dependencies
Some checks failed
Build assets artifact / Build assets artifact (push) Has been cancelled
Docker Image Build / build (linux/amd64, amd64, ubuntu-latest) (push) Has been cancelled
Docker Image Build / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/amd64, amd64, ubuntu-latest) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Has been cancelled
Static analysis / Static analysis (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, sqlite) (push) Has been cancelled
Docker Image Build / merge (push) Has been cancelled
Docker Image Build (FrankenPHP) / merge (push) Has been cancelled
2026-07-14 18:42:06 +02:00
Jan Böhmer
b604128afa Merge remote-tracking branch 'origin/master' 2026-07-14 18:15:03 +02:00
Jan Böhmer
5110915de2 Prevent formula injection into XLSX export 2026-07-14 18:14:59 +02:00
dependabot[bot]
97f914a85a
Bump actions/cache from 5 to 6 (#1431)
Some checks are pending
Build assets artifact / Build assets artifact (push) Waiting to run
Docker Image Build / build (linux/amd64, amd64, ubuntu-latest) (push) Waiting to run
Docker Image Build / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build / merge (push) Blocked by required conditions
Docker Image Build (FrankenPHP) / build (linux/amd64, amd64, ubuntu-latest) (push) Waiting to run
Docker Image Build (FrankenPHP) / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build (FrankenPHP) / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build (FrankenPHP) / merge (push) Blocked by required conditions
Static analysis / Static analysis (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, sqlite) (push) Waiting to run
Bumps [actions/cache](https://github.com/actions/cache) from 5 to 6.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-14 11:38:09 +02:00
Jan Böhmer
f53f77f6dc Updated dependencies 2026-07-11 17:38:45 +02:00
Jan Böhmer
76a36386fc Refactored size checks for attachments and check base64 upload length 2026-07-09 19:33:11 +02:00
Jan Böhmer
a356b94c34 Respect the attachment max file size limit for attachment file downloads
Some checks failed
Build assets artifact / Build assets artifact (push) Has been cancelled
Docker Image Build / build (linux/amd64, amd64, ubuntu-latest) (push) Has been cancelled
Docker Image Build / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/amd64, amd64, ubuntu-latest) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Has been cancelled
Static analysis / Static analysis (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, sqlite) (push) Has been cancelled
Docker Image Build / merge (push) Has been cancelled
Docker Image Build (FrankenPHP) / merge (push) Has been cancelled
2026-07-08 23:30:16 +02:00
Jan Böhmer
27afa013c1 Merge remote-tracking branch 'origin/master' 2026-07-08 23:12:59 +02:00
Jan Böhmer
a608551309 Updated dependencies 2026-07-08 23:12:52 +02:00
55 changed files with 17266 additions and 2420 deletions

View file

@ -0,0 +1,61 @@
# Reproduces https://docs.part-db.de/installation/installation_guide-debian.html
# on Debian 13 (trixie) as closely as possible, for debugging issues reported
# by users who followed that guide (Apache2 + mod_php, not the project's own
# FrankenPHP-based Dockerfile at the repo root).
FROM debian:13
ARG HOST_UID=1000
ARG HOST_GID=1000
ENV DEBIAN_FRONTEND=noninteractive
# --- Prerequisites (guide: "Prerequisites Installation") ---
RUN apt-get update && apt-get upgrade -y && \
apt-get install -y --no-install-recommends \
git curl zip ca-certificates software-properties-common \
apt-transport-https lsb-release nano wget sqlite3 gnupg openssl \
&& rm -rf /var/lib/apt/lists/*
# --- PHP and Apache2 (guide: "PHP and Apache2 Setup") ---
# Debian 13 ships PHP 8.4 by default, so no extra PHP repo is needed.
RUN apt-get update && apt-get install -y --no-install-recommends \
apache2 php8.4 libapache2-mod-php8.4 \
php8.4-opcache php8.4-curl php8.4-gd php8.4-mbstring \
php8.4-xml php8.4-bcmath php8.4-intl php8.4-zip php8.4-xsl \
php8.4-sqlite3 php8.4-mysql \
&& rm -rf /var/lib/apt/lists/*
# --- Composer (guide: "Composer Installation") ---
RUN wget -O /tmp/composer-setup.php https://getcomposer.org/installer \
&& php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer \
&& chmod +x /usr/local/bin/composer \
&& rm /tmp/composer-setup.php
# --- Node.js and Yarn (guide: "Node.js and Yarn Setup") ---
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& curl -sL https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor -o /usr/share/keyrings/yarnkey.gpg \
&& echo "deb [signed-by=/usr/share/keyrings/yarnkey.gpg] https://dl.yarnpkg.com/debian stable main" > /etc/apt/sources.list.d/yarn.list \
&& apt-get update && apt-get install -y --no-install-recommends yarn \
&& rm -rf /var/lib/apt/lists/*
# Map www-data to the host user's uid/gid so files created inside the
# bind-mounted repo (composer/yarn artifacts, chown'd dirs) don't end up
# owned by a foreign uid on the host.
RUN groupmod -g ${HOST_GID} www-data && usermod -u ${HOST_UID} -g ${HOST_GID} www-data \
&& mkdir -p /var/www/partdb && chown -R www-data:www-data /var/www
# --- Apache2 Configuration (guide: "Apache2 Configuration") ---
COPY partdb.conf /etc/apache2/sites-available/partdb.conf
RUN ln -sf /etc/apache2/sites-available/partdb.conf /etc/apache2/sites-enabled/partdb.conf \
&& a2enmod rewrite headers access_compat \
&& rm -f /etc/apache2/sites-enabled/000-default.conf
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
WORKDIR /var/www/partdb
EXPOSE 80
ENTRYPOINT ["/entrypoint.sh"]
CMD ["apache2ctl", "-D", "FOREGROUND"]

View file

@ -0,0 +1,41 @@
# Reproduces a Debian 12 install following
# https://docs.part-db.de/installation/installation_guide-debian.html
# using the repo checkout two levels up, for debugging.
#
# Usage (from this directory):
# HOST_UID=$(id -u) HOST_GID=$(id -g) docker compose up --build
#
# Then open http://localhost:8080 (login: admin / password printed in the
# container logs by doctrine:migrations:migrate on first run).
#
# vendor/, node_modules/, var/, public/build, public/media and uploads live
# in named volumes so composer/yarn/console never write into the host's own
# working copy - only src/, config/, templates/, assets/, migrations/, etc.
# are shared live for debugging.
services:
partdb:
build:
context: .
args:
HOST_UID: ${HOST_UID:-1000}
HOST_GID: ${HOST_GID:-1000}
container_name: partdb-debian-guide
ports:
- "8080:80"
volumes:
- ../..:/var/www/partdb
- ./env.local:/var/www/partdb/.env.local:ro
- partdb_vendor:/var/www/partdb/vendor
- partdb_node_modules:/var/www/partdb/node_modules
- partdb_var:/var/www/partdb/var
- partdb_public_build:/var/www/partdb/public/build
- partdb_public_media:/var/www/partdb/public/media
- partdb_uploads:/var/www/partdb/uploads
volumes:
partdb_vendor:
partdb_node_modules:
partdb_var:
partdb_public_build:
partdb_public_media:
partdb_uploads:

View file

@ -0,0 +1,48 @@
#!/bin/bash
# Reproduces the app-setup steps from
# https://docs.part-db.de/installation/installation_guide-debian.html
# against the repo bind-mounted at /var/www/partdb.
set -euo pipefail
cd /var/www/partdb
# Only chown the named-volume mount points (see docker-compose.yml) - they're
# fresh docker volumes owned by root until first use. The bind-mounted source
# tree doesn't need it (www-data's uid/gid is mapped to the host user's at
# image build time, so ownership already matches), and recursing into it
# would also try (and fail) to chown the read-only .env.local mount.
chown -R www-data:www-data \
var vendor node_modules public/build public/media uploads
as_www() {
# -p preserves the environment (APP_ENV/APP_SECRET/DATABASE_URL from
# docker-compose) across the user switch. Plain `su` resets it, which
# would silently fall back to whatever's baked into .env.local.
su -p -s /bin/bash www-data -c "$*"
}
if [ ! -f vendor/autoload.php ] || [ "${FORCE_REINSTALL:-0}" = "1" ]; then
echo "==> composer install -o"
as_www "composer install -o"
fi
if [ ! -d node_modules/.bin ] || [ "${FORCE_REINSTALL:-0}" = "1" ]; then
echo "==> yarn install"
as_www "yarn install"
fi
if [ ! -f public/build/manifest.json ] || [ "${FORCE_REINSTALL:-0}" = "1" ]; then
echo "==> yarn build"
as_www "yarn build"
fi
echo "==> cache:clear"
as_www "php bin/console cache:clear"
echo "==> partdb:check-requirements"
as_www "php bin/console partdb:check-requirements" || true
echo "==> doctrine:migrations:migrate"
as_www "php bin/console doctrine:migrations:migrate --no-interaction"
exec "$@"

View file

@ -0,0 +1,11 @@
# Bind-mounted over /var/www/partdb/.env.local inside the container only.
# The host's real .env.local (dev, MySQL on 127.0.0.1) is never touched or
# read - this guarantees APP_ENV/DATABASE_URL are correct for BOTH Apache/
# mod_php web requests and CLI (console/composer) commands, without relying
# on environment variables being propagated into Apache's subprocess env
# (which is what caused the "WebProfilerBundle" dev-bundle error: web
# requests were silently falling back to the host .env.local's APP_ENV=dev,
# while --no-dev composer install never installed that bundle).
APP_ENV=prod
APP_SECRET=0000000000000000000000000000000000000000000000000000000000debug
DATABASE_URL="sqlite:///%kernel.project_dir%/var/app.db"

View file

@ -0,0 +1,15 @@
# Verbatim from https://docs.part-db.de/installation/installation_guide-debian.html
<VirtualHost *:80>
ServerName partdb.lan
ServerAlias www.partdb.lan
DocumentRoot /var/www/partdb/public
<Directory /var/www/partdb/public>
AllowOverride All
Order Allow,Deny
Allow from All
</Directory>
ErrorLog /var/log/apache2/partdb_error.log
CustomLog /var/log/apache2/partdb_access.log combined
</VirtualHost>

8
.env
View file

@ -5,6 +5,14 @@
# Share that value with nobody and keep it secret
APP_SECRET=a03498528f5a5fc089273ec9ae5b2849
# Change this and uncomment the following line to set the trusted hosts for your Part-DB installation, aka under which
# domain names it is reachable. This is makes things more secure, because it can prevent certain header attacks.
# You have to escape dots in the domain name with a backslash
#TRUSTED_HOSTS='^(your-domain\.invalid)$'
# You can also allow multiple domain names, e.g. for testing or development purposes, with an | between the domain names.
#TRUSTED_HOSTS='^(localhost|your-domain\.invalid)$'
###################################################################################
# Database settings
###################################################################################

View file

@ -42,7 +42,7 @@ jobs:
run: |
echo "::set-output name=dir::$(composer config cache-files-dir)"
- uses: actions/cache@v5
- uses: actions/cache@v6
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
@ -56,7 +56,7 @@ jobs:
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v5
- uses: actions/cache@v6
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}

View file

@ -34,7 +34,7 @@ jobs:
run: |
echo "::set-output name=dir::$(composer config cache-files-dir)"
- uses: actions/cache@v5
- uses: actions/cache@v6
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}

View file

@ -81,7 +81,7 @@ jobs:
id: composer-cache
run: |
echo "::set-output name=dir::$(composer config cache-files-dir)"
- uses: actions/cache@v5
- uses: actions/cache@v6
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
@ -92,7 +92,7 @@ jobs:
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v5
- uses: actions/cache@v6
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}

View file

@ -1 +1 @@
2.13.1
2.13.3

View file

@ -27,7 +27,7 @@
"doctrine/doctrine-migrations-bundle": "^3.0",
"doctrine/orm": "^3.2.0",
"dompdf/dompdf": "^3.1.2",
"gregwar/captcha-bundle": "^2.1.0",
"gregwar/captcha-bundle": "^3.0.0",
"hshn/base64-encoded-file": "^6.0",
"jbtronics/2fa-webauthn": "^3.0.0",
"jbtronics/dompdf-font-loader-bundle": "^1.0.0",
@ -57,10 +57,10 @@
"scheb/2fa-trusted-device": "^v7.11.0",
"shivas/versioning-bundle": "^4.0",
"spatie/db-dumper": "^3.3.1",
"symfony/ai-bundle": "^0.10.0",
"symfony/ai-lm-studio-platform": "^v0.10.0",
"symfony/ai-ollama-platform": "^0.10.0",
"symfony/ai-open-router-platform": "^0.10.0",
"symfony/ai-bundle": "^0.11.0",
"symfony/ai-lm-studio-platform": "^v0.11.0",
"symfony/ai-ollama-platform": "^0.11.0",
"symfony/ai-open-router-platform": "^0.11.0",
"symfony/apache-pack": "^1.0",
"symfony/asset": "7.4.*",
"symfony/console": "7.4.*",

788
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -8,7 +8,7 @@ parameters:
# This is used as workaround for places where we can not access the settings directly (like the 2FA application names)
partdb.title: '%env(string:settings:customization:instanceName)%' # The title shown inside of Part-DB (e.g. in the navbar and on homepage)
partdb.locale_menu: ['en', 'de', 'it', 'fr', 'ru', 'ja', 'cs', 'da', 'zh', 'pl', 'hu'] # The languages that are shown in user drop down menu
partdb.locale_menu: ['en', 'de', 'it', 'fr', 'ru', 'ja', 'cs', 'da', 'zh', 'pl', 'hu', 'ko'] # The languages that are shown in user drop down menu
partdb.default_uri: '%env(addSlash:string:DEFAULT_URI)%' # The default URI to use for the Part-DB instance (e.g. https://part-db.example.com/). This is used for generating links in emails

View file

@ -2770,6 +2770,11 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* host?: string|Param, // Default: "https://api.decart.ai/v1"
* http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client"
* },
* deepgram?: array{
* api_key?: string|Param,
* endpoint?: string|Param, // Deepgram REST API endpoint // Default: "https://api.deepgram.com/v1/"
* http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client"
* },
* deepseek?: array{
* api_key?: string|Param,
* http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client"
@ -2810,6 +2815,11 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* host_url?: string|Param, // Default: "http://127.0.0.1:1234"
* http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client"
* },
* minimax?: array{
* endpoint?: string|Param, // Default: "https://api.minimax.io/v1"
* api_key?: string|Param,
* http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client"
* },
* mistral?: array{
* api_key?: string|Param,
* http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client"
@ -2931,6 +2941,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* account_id?: string|Param,
* api_key?: string|Param,
* index_name?: string|Param,
* http_client?: string|Param, // Default: "http_client"
* dimensions?: int|Param, // Default: 1536
* metric?: string|Param, // Default: "cosine"
* endpoint?: string|Param,
@ -3087,6 +3098,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* namespace?: string|Param,
* database?: string|Param,
* table?: string|Param,
* http_client?: string|Param, // Default: "http_client"
* vector_field?: string|Param, // Default: "_vectors"
* strategy?: string|Param, // Default: "cosine"
* dimensions?: int|Param, // Default: 1536
@ -3096,6 +3108,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* endpoint?: string|Param,
* api_key?: string|Param,
* collection?: string|Param,
* http_client?: string|Param, // Default: "http_client"
* vector_field?: string|Param, // Default: "_vectors"
* dimensions?: int|Param, // Default: 1536
* }>,

View file

@ -262,7 +262,15 @@ See the [information providers]({% link usage/information_provider_system.md %})
* `TRUSTED_PROXIES` (env only): Set the IP addresses (or IP blocks) of trusted reverse proxies here. This is needed to get correct
IP information (see [here](https://symfony.com/doc/current/deployment/proxies.html) for more info).
* `TRUSTED_HOSTS` (env only): To prevent `HTTP Host header attacks` you can set a regex containing all host names via which Part-DB
should be accessible. If accessed via the wrong hostname, an error will be shown.
should be accessible. If accessed via a hostname not matching the regex, an error page will be shown instead. By default this
is empty, meaning Part-DB accepts requests for any host name, which is not recommended for production use.
For example, if Part-DB should only be reachable under `part-db.example.invalid`, set:
```
TRUSTED_HOSTS='^(part-db\.example\.invalid)$'
```
You can specify multiple host names separated by `|`, e.g. `^(localhost|part-db\.example\.invalid)$`.
Part-DB displays a warning on the homepage (visible to administrators only) as long as this value is not set.
* `DEMO_MODE` (env only): Set Part-DB into demo mode, which forbids users to change their passwords and settings. Used for the demo
instance. This should not be needed for normal installations.
* `NO_URL_REWRITE_AVAILABLE` (allowed values `true` or `false`) (env only): Set this value to true, if your webserver does not

View file

@ -84,7 +84,11 @@ services:
# If you use a reverse proxy in front of Part-DB, you must configure the trusted proxies IP addresses here (see reverse proxy documentation for more information):
# - TRUSTED_PROXIES=127.0.0.0/8,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
# To prevent HTTP Host header attacks, set this to a regex matching all host names Part-DB should be reachable under.
# By default (unset) Part-DB accepts requests for any host name, which is not recommended for production use.
# - TRUSTED_HOSTS=^(part-db\.example\.invalid)$
# If you need to install additional composer packages (e.g., for specific mailer transports), you can specify them here:
# The packages will be installed automatically when the container starts
# - COMPOSER_EXTRA_PACKAGES=symfony/mailgun-mailer symfony/sendgrid-mailer
@ -96,7 +100,10 @@ services:
```bash
openssl rand -hex 32
```
6. Inside the folder, run
6. Uncomment and set the `TRUSTED_HOSTS` variable to a regex matching the host name(s) Part-DB should be reachable under
(e.g. `^(part-db\.example\.invalid)$`), to prevent HTTP Host header attacks. See [Configuration]({% link configuration.md %})
for more information.
7. Inside the folder, run
```bash
docker-compose up -d
@ -107,7 +114,7 @@ openssl rand -hex 32
> Otherwise Part-DB console might use the wrong configuration to execute commands.
7. Create the initial database with
8. Create the initial database with
```bash
docker exec --user=www-data partdb php bin/console doctrine:migrations:migrate
@ -115,7 +122,7 @@ docker exec --user=www-data partdb php bin/console doctrine:migrations:migrate
and watch for the password output
8. Part-DB is available under `http://localhost:8080` and you can log in with the username `admin` and the password shown
9. 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
@ -171,7 +178,11 @@ services:
# Override value if you want to show a given text on homepage.
# When this is commented out the webUI can be used to configure the banner
#- BANNER=This is a test banner<br>with a line break
# To prevent HTTP Host header attacks, set this to a regex matching all host names Part-DB should be reachable under.
# By default (unset) Part-DB accepts requests for any host name, which is not recommended for production use.
# - TRUSTED_HOSTS=^(part-db\.example\.invalid)$
# If you need to install additional composer packages (e.g., for specific mailer transports), you can specify them here:
# - COMPOSER_EXTRA_PACKAGES=symfony/mailgun-mailer symfony/sendgrid-mailer

View file

@ -1,13 +1,13 @@
---
title: Direct Installation on Debian 12
title: Direct Installation on Debian 13
layout: default
parent: Installation
nav_order: 4
---
# Part-DB installation guide for Debian 12 (Bookworm)
# Part-DB installation guide for Debian 13 (Trixie)
This guide shows you how to install Part-DB directly on Debian 12 using apache2 and SQLite. This guide should work with
This guide shows you how to install Part-DB directly on Debian 13 using apache2 and SQLite. This guide should work with
recent Ubuntu and other Debian-based distributions with little to no changes.
Depending on what you want to do, using the prebuilt docker images may be a better choice, as you don't need to install
this many dependencies. See [here]({% link installation/installation_docker.md %}) for more information on the docker
@ -45,10 +45,10 @@ compatibility.
Install PHP with required extensions and apache2:
```bash
sudo apt install apache2 php8.2 libapache2-mod-php8.2 \
php8.2-opcache php8.2-curl php8.2-gd php8.2-mbstring \
php8.2-xml php8.2-bcmath php8.2-intl php8.2-zip php8.2-xsl \
php8.2-sqlite3 php8.2-mysql
sudo apt install apache2 php8.4 libapache2-mod-php8.4 \
php8.4-opcache php8.4-curl php8.4-gd php8.4-mbstring \
php8.4-xml php8.4-bcmath php8.4-intl php8.4-zip php8.4-xsl \
php8.4-sqlite3 php8.4-mysql
```
### Install composer
@ -145,6 +145,16 @@ A full list of configuration options can be found [here](../configuration.md).
> ```
> 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`).
{: .important }
> **Set `TRUSTED_HOSTS` before going live.** By default Part-DB accepts requests for any host name, which makes it
> vulnerable to `HTTP Host header attacks`. Edit your `.env.local` with a text editor and set `TRUSTED_HOSTS` to a
> regex matching all host names Part-DB should be reachable under, e.g. if Part-DB should only be reachable via
> `part-db.example.invalid`:
> ```
> TRUSTED_HOSTS='^(part-db\.example\.invalid)$'
> ```
> See the [configuration options](../configuration.md) page for more information about `TRUSTED_HOSTS`.
Please check that the configured base currency matches your mainly used currency, as
this can not be changed after creating price information.
@ -291,7 +301,7 @@ sudo -u www-data php bin/console cache:clear
## MySQL/MariaDB database
To use a MySQL database, follow the steps from above (except the creation of the database, we will do this later).
Debian 12 does not ship MySQL in its repositories anymore, so we use the compatible MariaDB instead:
Debian does not ship MySQL in its repositories anymore, so we use the compatible MariaDB instead:
1. Install maria-db with:

View file

@ -22,6 +22,16 @@ TRUSTED_PROXIES=192.168.2.10
Set the `DEFAULT_URI` environment variable to the URL of your Part-DB installation, available from the outside (so via
the reverse proxy).
You should also set the `TRUSTED_HOSTS` environment variable to a regex matching the host name(s) Part-DB is reachable
under via the reverse proxy, to prevent `HTTP Host header attacks`. For example, if Part-DB is reachable via
`part-db.example.invalid`, set:
```
TRUSTED_HOSTS='^(part-db\.example\.invalid)$'
```
See the [configuration options](../configuration.md) page for more information about `TRUSTED_HOSTS`.
## Part-DB in a subpath via reverse proxy
If you put Part-DB into a subpath via the reverse proxy, you have to configure your webserver to include `X-Forwarded-Prefix` in the request headers.

View file

@ -54,7 +54,7 @@ To connect KiCad with Part-DB do the following steps:
```
4. Replace the `root_url` with the URL of your Part-DB instance plus `/en/kicad-api/`. You can find the right value for this in the Part-DB user settings page under "API endpoints" in the "API tokens" panel.
5. Replace the `token` field value with the token you have generated in step 1.
6. Open KiCad and add this created file as a library in the KiCad symbol table under (Preferences --> Manage Symbol Libraries)
6. Open KiCad and add this created file as a HTTP library in the KiCad symbol table under (Preferences --> Manage Symbol Libraries)
If you then place a new part, the library dialog opens, and you should be able to see the categories and parts from Part-DB.

View file

@ -120,12 +120,23 @@ DirectoryIndex index.php
</IfModule>
<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"
# Set a strict CSP for everything Apache serves, except the front controller (index.php),
# whose response CSP is set by NelmioSecurityBundle in PHP. Excluding it via a negative
# FilesMatch (rather than a blanket rule relying on "setifempty" to skip PHP responses)
# avoids depending on mod_headers detecting the PHP-set header at the right time: on a
# real Apache 2.4/mod_php install, "setifempty" did not reliably suppress the static policy,
# so BOTH Content-Security-Policy headers ended up on the same PHP response, and browsers
# enforce the intersection of all CSP headers on a response - so "script-src 'none';
# sandbox" silently disabled all scripts/styles site-wide. mod_rewrite's internal redirect
# to index.php re-runs directory-scoped matching against the new target, so this FilesMatch
# reliably excludes it regardless of the originally requested URL.
<FilesMatch "^(?!index\.php$).+$">
Header always set Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; sandbox;"
Header always set X-Content-Type-Options "nosniff"
</FilesMatch>
# SVG files get a slightly different CSP because they can embed resources and must not be framed.
# (Declared after the general rule above so its more specific "set" wins for SVG files.)
<FilesMatch "\.(svg|svg\.gz|svg\.br)$">
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>

View file

@ -1,4 +1,4 @@
# Generated on Mon Jun 22 07:31:48 UTC 2026
# Generated on Mon Jul 20 05:45:44 UTC 2026
# This file contains all footprints available in the offical KiCAD library
Audio_Module:Reverb_BTDR-1H
Audio_Module:Reverb_BTDR-1V
@ -7732,6 +7732,15 @@ Connector_TE-Connectivity:TE_Micro-MaTch_2-215079-0_2x10_P1.27mm_Vertical
Connector_TE-Connectivity:TE_Micro-MaTch_215079-4_2x02_P1.27mm_Vertical
Connector_TE-Connectivity:TE_Micro-MaTch_215079-6_2x03_P1.27mm_Vertical
Connector_TE-Connectivity:TE_Micro-MaTch_215079-8_2x04_P1.27mm_Vertical
Connector_TE-Connectivity:TE_Micro-MaTch_7-188275-4_2x02_P1.27mm_Vertical
Connector_TE-Connectivity:TE_Micro-MaTch_7-188275-6_2x03_P1.27mm_Vertical
Connector_TE-Connectivity:TE_Micro-MaTch_7-188275-8_2x04_P1.27mm_Vertical
Connector_TE-Connectivity:TE_Micro-MaTch_8-188275-0_2x05_P1.27mm_Vertical
Connector_TE-Connectivity:TE_Micro-MaTch_8-188275-2_2x06_P1.27mm_Vertical
Connector_TE-Connectivity:TE_Micro-MaTch_8-188275-4_2x07_P1.27mm_Vertical
Connector_TE-Connectivity:TE_Micro-MaTch_8-188275-6_2x08_P1.27mm_Vertical
Connector_TE-Connectivity:TE_Micro-MaTch_8-188275-8_2x09_P1.27mm_Vertical
Connector_TE-Connectivity:TE_Micro-MaTch_9-188275-0_2x10_P1.27mm_Vertical
Connector_TE-Connectivity:TE_T4041037031-000_M8_03_Socket_Straight
Connector_TE-Connectivity:TE_T4041037041-000_M8_04_Socket_Straight
Connector_USB:USB3_A_Molex_48393-001
@ -9150,6 +9159,7 @@ Inductor_SMD:L_APV_APH1770
Inductor_SMD:L_APV_APH2213
Inductor_SMD:L_AVX_LMLP07A7
Inductor_SMD:L_Abracon_ASPI-0425
Inductor_SMD:L_Abracon_ASPI-0628
Inductor_SMD:L_Abracon_ASPI-0630LR
Inductor_SMD:L_Abracon_ASPI-3012S
Inductor_SMD:L_Abracon_ASPI-4030S
@ -12565,6 +12575,8 @@ Package_SO:HTSSOP-16-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask2.46x2.31mm
Package_SO:HTSSOP-16-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask2.46x2.31mm_ThermalVias
Package_SO:HTSSOP-16-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask2.66x2.46mm
Package_SO:HTSSOP-16-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask2.66x2.46mm_ThermalVias
Package_SO:HTSSOP-16-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask2.78x3.37mm
Package_SO:HTSSOP-16-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask2.78x3.37mm_ThermalVias
Package_SO:HTSSOP-16-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask3x3mm_ThermalVias
Package_SO:HTSSOP-16-1EP_4.4x5mm_P0.65mm_EP3x3mm
Package_SO:HTSSOP-20-1EP_4.4x6.5mm_P0.65mm_EP2.74x3.86mm
@ -12919,6 +12931,8 @@ Package_SO:Texas_DYY0016A_TSOT-23-16_2x4.2mm_P0.5mm
Package_SO:Texas_HSOP-8-1EP_3.9x4.9mm_P1.27mm
Package_SO:Texas_HSOP-8-1EP_3.9x4.9mm_P1.27mm_ThermalVias
Package_SO:Texas_HTSOP-8-1EP_3.9x4.9mm_P1.27mm_EP2.95x4.9mm_Mask2.4x3.1mm_ThermalVias
Package_SO:Texas_HTSSOP-14-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask2.94x3.34mm
Package_SO:Texas_HTSSOP-14-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask2.94x3.34mm_ThermalVias
Package_SO:Texas_HTSSOP-14-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask3.155x3.255mm
Package_SO:Texas_HTSSOP-14-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask3.155x3.255mm_ThermalVias
Package_SO:Texas_PW0020A_TSSOP-20_4.4x6.5mm_P0.65mm

File diff suppressed because it is too large Load diff

View file

@ -27,6 +27,7 @@ use App\Entity\Parts\Part;
use App\Services\System\AppSecretChecker;
use App\Services\System\BannerHelper;
use App\Services\System\GitVersionInfoProvider;
use App\Services\System\TrustedHostsChecker;
use App\Services\System\UpdateAvailableFacade;
use Doctrine\ORM\EntityManagerInterface;
use Omines\DataTablesBundle\DataTableFactory;
@ -41,6 +42,7 @@ class HomepageController extends AbstractController
private readonly DataTableFactory $dataTable,
private readonly BannerHelper $bannerHelper,
private readonly AppSecretChecker $appSecretChecker,
private readonly TrustedHostsChecker $trustedHostsChecker,
) {
}
@ -90,6 +92,7 @@ class HomepageController extends AbstractController
'new_version_url' => $updateAvailableManager->getLatestVersionUrl(),
'insecure_app_secret' => $this->appSecretChecker->isInsecureSecret(),
'suggested_app_secret' => $this->appSecretChecker->isInsecureSecret() ? $this->appSecretChecker->generateSecret() : null,
'trusted_hosts_unconfigured' => $this->trustedHostsChecker->isTrustedHostsUnconfigured(),
]);
}
}

View file

@ -74,7 +74,7 @@ class ToolsController extends AbstractController
'detailed_error_pages' => $this->getParameter('partdb.error_pages.show_help'),
'error_page_admin_email' => $this->getParameter('partdb.error_pages.admin_email'),
'configured_max_file_size' => $settings->system->attachments->maxFileSize,
'effective_max_file_size' => $attachmentSubmitHandler->getMaximumAllowedUploadSize(),
'effective_max_file_size' => $attachmentSubmitHandler->getMaximumEffectiveUploadSize(),
'saml_enabled' => $this->getParameter('partdb.saml.enabled'),
//PHP section

View file

@ -209,7 +209,7 @@ class AttachmentFormType extends AbstractType
public function finishView(FormView $view, FormInterface $form, array $options): void
{
$view->vars['max_upload_size'] = $this->submitHandler->getMaximumAllowedUploadSize();
$view->vars['max_upload_size'] = $this->submitHandler->getMaximumEffectiveUploadSize();
}
public function getBlockPrefix(): string

View file

@ -217,6 +217,14 @@ class AttachmentSubmitHandler
//If no file was uploaded, but we have base64 encoded data, create a file from it
if (!$file && $upload->data !== null) {
if (strlen($upload->data) > $this->getMaximumUserConfiguredUploadSize() * 4 / 3) { //Base64 encoding increases the size of the data by 4/3, so we have to check for that
throw new RuntimeException(
sprintf(
'The given base64 data is too big! Maximum size is %.1f MB!',
$this->getMaximumUserConfiguredUploadSize() / 1000 / 1000
));
}
$file = new UploadedBase64EncodedFile(new Base64EncodedFile($upload->data), $upload->filename ?? 'base64');
}
@ -225,6 +233,15 @@ class AttachmentSubmitHandler
//When a file is given then upload it, otherwise check if we need to download the URL
if ($file instanceof UploadedFile) {
//Check the file size, to avoid uploading too big files.
//The file is not necessarily validated as it can also come from an Base64 source
if ($file->getSize() > $this->getMaximumUserConfiguredUploadSize()) {
throw new RuntimeException(
sprintf(
'The uploaded file is too big! Maximum size is %.1f MB!',
$this->getMaximumUserConfiguredUploadSize() / 1000 / 1000
));
}
$this->upload($attachment, $file, $secure_attachment);
} elseif ($upload->downloadUrl && $attachment->hasExternal()) {
@ -399,9 +416,30 @@ class AttachmentSubmitHandler
//Open a temporary file in the attachment folder
$fs->mkdir($attachment_folder);
$fileHandler = fopen($tmp_path, 'wb');
$bytesDownloaded = 0;
$maxSize = $this->getMaximumUserConfiguredUploadSize(); //We use the maximum user configured size here, PHPs limits dont apply
//Write the downloaded data to file
foreach ($this->httpClient->stream($response) as $chunk) {
fwrite($fileHandler, $chunk->getContent());
$content = $chunk->getContent();
$bytesDownloaded += strlen($content);
//Ensure the size does not get too large to avoid filling up the disk easily.
//If the file is too big, cancel the download and delete the temporary file.
if ($bytesDownloaded > $maxSize) {
$response->cancel();
fclose($fileHandler);
unlink($tmp_path); //Delete the temporary file, because it is too big
throw new AttachmentDownloadException(
sprintf(
'The downloaded file is too big! Maximum size is %.1f MB!',
$maxSize / 1000 / 1000
));
}
fwrite($fileHandler, $content);
}
fclose($fileHandler);
@ -505,10 +543,10 @@ class AttachmentSubmitHandler
}
/*
* Returns the maximum allowed upload size in bytes.
* Returns the maximum effective upload size in bytes.
* This is the minimum value of Part-DB max_file_size, and php.ini's post_max_size and upload_max_filesize.
*/
public function getMaximumAllowedUploadSize(): int
public function getMaximumEffectiveUploadSize(): int
{
if ($this->max_upload_size_bytes) {
return $this->max_upload_size_bytes;
@ -523,6 +561,15 @@ class AttachmentSubmitHandler
return $this->max_upload_size_bytes;
}
/**
* Returns the maximum user configured upload size in bytes.
* @return int
*/
public function getMaximumUserConfiguredUploadSize(): int
{
return $this->parseFileSizeString($this->settings->maxFileSize);
}
/**
* Sanitizes the given SVG file, if the attachment is an internal SVG file.
* @param Attachment $attachment

View file

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace App\Services\ImportExportSystem;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use App\Entity\Base\AbstractNamedDBElement;
use App\Entity\Base\AbstractStructuralDBElement;
use App\Helpers\FilenameSanatizer;
@ -179,7 +180,11 @@ class EntityExporter
foreach ($columns as $column) {
$cellCoordinate = Coordinate::stringFromColumnIndex($colIndex) . $rowIndex;
$worksheet->setCellValue($cellCoordinate, $column);
if (is_numeric(trim($column, '"'))) { // Check if the column value is numeric after trimming quotes, as values are surrounded by quotes in CSV
$worksheet->setCellValueExplicit($cellCoordinate, $column, DataType::TYPE_NUMERIC);
} else {
$worksheet->setCellValueExplicit($cellCoordinate, $column, DataType::TYPE_STRING);
}
$colIndex++;
}
$rowIndex++;
@ -268,7 +273,7 @@ class EntityExporter
//Remove percent for fallback
$fallback = str_replace("%", "_", $filename);
// Create the disposition of the file
$disposition = $response->headers->makeDisposition(
ResponseHeaderBag::DISPOSITION_ATTACHMENT,

View file

@ -136,7 +136,9 @@ class LCSCProvider implements BatchInfoProviderInterface, URLHandlerInfoProvider
}
}
$response = $this->lcscClient->request('POST', self::ENDPOINT_URL . "/search/v2/global", [
//First we try the search v3 endpoint, which seems to give better results including pictures, but it only works
//on quite exact mpn matches
$response = $this->lcscClient->request('POST', self::ENDPOINT_URL . "/search/v3/global", [
'headers' => [
'Cookie' => new Cookie('currencyCode', $this->settings->currency)
],
@ -147,20 +149,29 @@ class LCSCProvider implements BatchInfoProviderInterface, URLHandlerInfoProvider
$arr = $response->toArray();
// Get products list
$products = $arr['result']['productSearchResultVO']['productList'] ?? [];
// Get product tip
$tipProductCode = $arr['result']['tipProductDetailUrlVO']['productCode'] ?? null;
//If we get exact matches, use them
if (!empty($arr['result']['exactMatchResult'])) {
$products = $arr['result']['exactMatchResult'];
} else { //Otherwise fallback onto the third search endpoint, which has a worse data quality but is more likely to return results for vague search terms
$response = $this->lcscClient->request('POST', self::ENDPOINT_URL."/search/third", [
'headers' => [
'Cookie' => new Cookie('currencyCode', $this->settings->currency)
],
'json' => [
'keyword' => $term,
'currentPage' => 1,
'pageSize' => 10,
],
]);
$arr = $response->toArray();
// Get products list
$products = $arr['result']['productList'] ?? [];
}
$result = [];
// LCSC does not display LCSC codes in the search, instead taking you directly to the
// detailed product listing. It does so utilizing a product tip field.
// If product tip exists and there are no products in the product list try a detail query
if (count($products) === 0 && $tipProductCode !== null) {
$result[] = $this->queryDetail($tipProductCode, $lightweight);
}
foreach ($products as $product) {
$result[] = $this->getPartDetail($product, $lightweight);
}
@ -219,7 +230,7 @@ class LCSCProvider implements BatchInfoProviderInterface, URLHandlerInfoProvider
provider_key: $this->getProviderKey(),
provider_id: $product['productCode'],
name: $product['productModel'],
description: $this->sanitizeField($product['productIntroEn']),
description: $this->sanitizeField($product['productIntroEn']) ?? '',
category: $this->sanitizeField($category ?? null),
manufacturer: $this->sanitizeField($product['brandNameEn'] ?? null),
mpn: $this->sanitizeField($product['productModel'] ?? null),
@ -383,7 +394,7 @@ class LCSCProvider implements BatchInfoProviderInterface, URLHandlerInfoProvider
]);
} else {
// Search API call for other terms
$responses[$keyword] = $this->lcscClient->request('POST', self::ENDPOINT_URL . "/search/v2/global", [
$responses[$keyword] = $this->lcscClient->request('POST', self::ENDPOINT_URL . "/search/v3/global", [
'headers' => [
'Cookie' => new Cookie('currencyCode', $this->settings->currency)
],

View file

@ -67,8 +67,8 @@ class BarcodeHelper
{
$svg = $this->barcodeAsSVG($content, $type);
$base64 = $this->dataUri($svg, 'image/svg+xml');
$alt_text ??= $content;
$alt_text ??= htmlspecialchars($content);
return '<img src="'.$base64.'" width="'.$width.'" style="min-height: 25px;" alt="'.$alt_text.'"/>';
}
@ -94,4 +94,4 @@ class BarcodeHelper
return $repr;
}
}
}

View file

@ -44,9 +44,9 @@ namespace App\Services\LabelSystem\PlaceholderProviders;
use App\Entity\Base\AbstractDBElement;
use App\Services\ElementTypeNameGenerator;
final class AbstractDBElementProvider implements PlaceholderProviderInterface
final readonly class AbstractDBElementProvider implements PlaceholderProviderInterface
{
public function __construct(private readonly ElementTypeNameGenerator $elementTypeNameGenerator)
public function __construct(private ElementTypeNameGenerator $elementTypeNameGenerator)
{
}
@ -54,7 +54,7 @@ final class AbstractDBElementProvider implements PlaceholderProviderInterface
{
if ($label_target instanceof AbstractDBElement) {
if ('[[TYPE]]' === $placeholder) {
return $this->elementTypeNameGenerator->getLocalizedTypeLabel($label_target);
return $this->elementTypeNameGenerator->typeLabel($label_target);
}
if ('[[ID]]' === $placeholder) {

View file

@ -31,11 +31,11 @@ use App\Services\LabelSystem\LabelBarcodeGenerator;
use App\Services\LabelSystem\Barcodes\BarcodeContentGenerator;
use Com\Tecnick\Barcode\Exception;
final class BarcodeProvider implements PlaceholderProviderInterface
final readonly class BarcodeProvider implements PlaceholderProviderInterface
{
public function __construct(private readonly LabelBarcodeGenerator $barcodeGenerator,
private readonly BarcodeContentGenerator $barcodeContentGenerator,
private readonly BarcodeHelper $barcodeHelper)
public function __construct(private LabelBarcodeGenerator $barcodeGenerator,
private BarcodeContentGenerator $barcodeContentGenerator,
private BarcodeHelper $barcodeHelper)
{
}

View file

@ -53,11 +53,11 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
* Provides Placeholders for infos about global infos like Installation name or datetimes.
* @see \App\Tests\Services\LabelSystem\PlaceholderProviders\GlobalProvidersTest
*/
final class GlobalProviders implements PlaceholderProviderInterface
final readonly class GlobalProviders implements PlaceholderProviderInterface
{
public function __construct(
private readonly Security $security,
private readonly UrlGeneratorInterface $url_generator,
private Security $security,
private UrlGeneratorInterface $url_generator,
private CustomizationSettings $customizationSettings,
)
{
@ -66,13 +66,13 @@ final class GlobalProviders implements PlaceholderProviderInterface
public function replace(string $placeholder, object $label_target, array $options = []): ?string
{
if ('[[INSTALL_NAME]]' === $placeholder) {
return $this->customizationSettings->instanceName;
return htmlspecialchars($this->customizationSettings->instanceName);
}
$user = $this->security->getUser();
if ('[[USERNAME]]' === $placeholder) {
if ($user instanceof User) {
return $user->getName();
return htmlspecialchars($user->getName());
}
return 'anonymous';
@ -80,7 +80,7 @@ final class GlobalProviders implements PlaceholderProviderInterface
if ('[[USERNAME_FULL]]' === $placeholder) {
if ($user instanceof User) {
return $user->getFullName(true);
return htmlspecialchars($user->getFullName(true));
}
return 'anonymous';

View file

@ -51,7 +51,7 @@ final class NamedElementProvider implements PlaceholderProviderInterface
public function replace(string $placeholder, object $label_target, array $options = []): ?string
{
if ($label_target instanceof NamedElementInterface && '[[NAME]]' === $placeholder) {
return $label_target->getName();
return htmlspecialchars($label_target->getName());
}
return null;

View file

@ -52,9 +52,9 @@ use Locale;
/**
* @see \App\Tests\Services\LabelSystem\PlaceholderProviders\PartLotProviderTest
*/
final class PartLotProvider implements PlaceholderProviderInterface
final readonly class PartLotProvider implements PlaceholderProviderInterface
{
public function __construct(private readonly LabelTextReplacer $labelTextReplacer, private readonly AmountFormatter $amountFormatter)
public function __construct(private LabelTextReplacer $labelTextReplacer, private AmountFormatter $amountFormatter)
{
}
@ -66,11 +66,11 @@ final class PartLotProvider implements PlaceholderProviderInterface
}
if ('[[LOT_NAME]]' === $placeholder) {
return $label_target->getName();
return htmlspecialchars($label_target->getName());
}
if ('[[LOT_COMMENT]]' === $placeholder) {
return $label_target->getComment();
return htmlspecialchars($label_target->getComment());
}
if ('[[EXPIRATION_DATE]]' === $placeholder) {
@ -95,19 +95,19 @@ final class PartLotProvider implements PlaceholderProviderInterface
}
if ('[[LOCATION]]' === $placeholder) {
return $label_target->getStorageLocation() instanceof StorageLocation ? $label_target->getStorageLocation()->getName() : '';
return $label_target->getStorageLocation() instanceof StorageLocation ? htmlspecialchars($label_target->getStorageLocation()->getName()) : '';
}
if ('[[LOCATION_FULL]]' === $placeholder) {
return $label_target->getStorageLocation() instanceof StorageLocation ? $label_target->getStorageLocation()->getFullPath() : '';
return $label_target->getStorageLocation() instanceof StorageLocation ? htmlspecialchars($label_target->getStorageLocation()->getFullPath()) : '';
}
if ('[[OWNER]]' === $placeholder) {
return $label_target->getOwner() instanceof User ? $label_target->getOwner()->getFullName() : '';
return $label_target->getOwner() instanceof User ? htmlspecialchars($label_target->getOwner()->getFullName()) : '';
}
if ('[[OWNER_USERNAME]]' === $placeholder) {
return $label_target->getOwner() instanceof User ? $label_target->getOwner()->getUsername() : '';
return $label_target->getOwner() instanceof User ? htmlspecialchars($label_target->getOwner()->getUsername()) : '';
}
return $this->labelTextReplacer->handlePlaceholder($placeholder, $label_target->getPart());

View file

@ -54,11 +54,11 @@ use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @see \App\Tests\Services\LabelSystem\PlaceholderProviders\PartProviderTest
*/
final class PartProvider implements PlaceholderProviderInterface
final readonly class PartProvider implements PlaceholderProviderInterface
{
private readonly MarkdownConverter $inlineConverter;
private MarkdownConverter $inlineConverter;
public function __construct(private readonly SIFormatter $siFormatter, private readonly TranslatorInterface $translator)
public function __construct(private SIFormatter $siFormatter, private TranslatorInterface $translator)
{
$environment = new Environment();
$environment->addExtension(new InlinesOnlyExtension());
@ -72,27 +72,27 @@ final class PartProvider implements PlaceholderProviderInterface
}
if ('[[CATEGORY]]' === $placeholder) {
return $part->getCategory() instanceof Category ? $part->getCategory()->getName() : '';
return $part->getCategory() instanceof Category ? htmlspecialchars($part->getCategory()->getName()) : '';
}
if ('[[CATEGORY_FULL]]' === $placeholder) {
return $part->getCategory() instanceof Category ? $part->getCategory()->getFullPath() : '';
return $part->getCategory() instanceof Category ? htmlspecialchars($part->getCategory()->getFullPath()) : '';
}
if ('[[MANUFACTURER]]' === $placeholder) {
return $part->getManufacturer() instanceof Manufacturer ? $part->getManufacturer()->getName() : '';
return $part->getManufacturer() instanceof Manufacturer ? htmlspecialchars($part->getManufacturer()->getName()) : '';
}
if ('[[MANUFACTURER_FULL]]' === $placeholder) {
return $part->getManufacturer() instanceof Manufacturer ? $part->getManufacturer()->getFullPath() : '';
return $part->getManufacturer() instanceof Manufacturer ? htmlspecialchars($part->getManufacturer()->getFullPath()) : '';
}
if ('[[FOOTPRINT]]' === $placeholder) {
return $part->getFootprint() instanceof Footprint ? $part->getFootprint()->getName() : '';
return $part->getFootprint() instanceof Footprint ? htmlspecialchars($part->getFootprint()->getName()) : '';
}
if ('[[FOOTPRINT_FULL]]' === $placeholder) {
return $part->getFootprint() instanceof Footprint ? $part->getFootprint()->getFullPath() : '';
return $part->getFootprint() instanceof Footprint ? htmlspecialchars($part->getFootprint()->getFullPath()) : '';
}
if ('[[MASS]]' === $placeholder) {
@ -100,15 +100,15 @@ final class PartProvider implements PlaceholderProviderInterface
}
if ('[[MPN]]' === $placeholder) {
return $part->getManufacturerProductNumber();
return htmlspecialchars($part->getManufacturerProductNumber());
}
if ('[[IPN]]' === $placeholder) {
return $part->getIpn() ?? '';
return $part->getIpn() ? htmlspecialchars($part->getIpn()) : '';
}
if ('[[TAGS]]' === $placeholder) {
return $part->getTags();
return htmlspecialchars($part->getTags());
}
if ('[[M_STATUS]]' === $placeholder) {

View file

@ -46,6 +46,7 @@ interface PlaceholderProviderInterface
/**
* Determines the real value of this placeholder.
* If the placeholder is not supported, null must be returned.
* The placeholder provider has to handle HTML escaping, as the output of this function will be used directly in the label template.
*
* @param string $placeholder The placeholder (e.g. "%%PLACEHOLDER%%") that should be replaced
* @param object $label_target The object that is targeted by the label

View file

@ -31,11 +31,11 @@ class StorelocationProvider implements PlaceholderProviderInterface
{
if ($label_target instanceof StorageLocation) {
if ('[[OWNER]]' === $placeholder) {
return $label_target->getOwner() instanceof User ? $label_target->getOwner()->getFullName() : '';
return $label_target->getOwner() instanceof User ? htmlspecialchars($label_target->getOwner()->getFullName()) : '';
}
if ('[[OWNER_USERNAME]]' === $placeholder) {
return $label_target->getOwner() instanceof User ? $label_target->getOwner()->getUsername() : '';
return $label_target->getOwner() instanceof User ? htmlspecialchars($label_target->getOwner()->getUsername()) : '';
}
}

View file

@ -55,13 +55,13 @@ final class StructuralDBElementProvider implements PlaceholderProviderInterface
return strip_tags((string) $label_target->getComment());
}
if ('[[FULL_PATH]]' === $placeholder) {
return $label_target->getFullPath();
return htmlspecialchars($label_target->getFullPath());
}
if ('[[PARENT]]' === $placeholder) {
return $label_target->getParent() instanceof AbstractStructuralDBElement ? $label_target->getParent()->getName() : '';
return $label_target->getParent() instanceof AbstractStructuralDBElement ? htmlspecialchars($label_target->getParent()->getName()) : '';
}
if ('[[PARENT_FULL_PATH]]' === $placeholder) {
return $label_target->getParent() instanceof AbstractStructuralDBElement ? $label_target->getParent()->getFullPath() : '';
return $label_target->getParent() instanceof AbstractStructuralDBElement ? htmlspecialchars($label_target->getParent()->getFullPath()) : '';
}
}

View file

@ -0,0 +1,45 @@
<?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 the TRUSTED_HOSTS environment variable has been configured.
*/
final readonly class TrustedHostsChecker
{
public function __construct(
#[Autowire('%env(TRUSTED_HOSTS)%')]
private string $trustedHosts,
) {
}
/**
* @return bool True if TRUSTED_HOSTS is not configured (meaning Part-DB accepts requests with any Host header), false otherwise.
*/
public function isTrustedHostsUnconfigured(): bool
{
return trim($this->trustedHosts) === '';
}
}

View file

@ -0,0 +1,96 @@
<?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;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
/**
* Generates absolute URLs for routes whose host can be trusted, for use in contexts where the host must not
* be attacker-controllable (e.g. links sent out via email).
*
* If no TRUSTED_HOSTS is configured, Symfony does not validate the Host header of the incoming request in
* any way, so it must not be trusted to generate such links (otherwise an attacker could poison them via a
* forged Host header). In that case, the host/scheme/base path are forced to the ones configured via
* DEFAULT_URI instead of the ones from the current HTTP request.
* If TRUSTED_HOSTS is configured, Symfony already rejects requests with a non-matching Host header, so the
* request's host can be trusted and is used as usual (which allows the app to be reachable under multiple
* trusted hostnames).
*/
final class TrustedUrlGenerator
{
public function __construct(
private readonly UrlGeneratorInterface $urlGenerator,
private readonly TrustedHostsChecker $trustedHostsChecker,
#[Autowire('%partdb.default_uri%')]
private readonly string $defaultUri,
) {
}
/**
* @param array<string, mixed> $parameters
*/
public function generate(string $route, array $parameters = []): string
{
//If TRUSTED_HOSTS is configured, Symfony already validated the request's Host header, so it can be trusted
if (!$this->trustedHostsChecker->isTrustedHostsUnconfigured()) {
return $this->urlGenerator->generate($route, $parameters, UrlGeneratorInterface::ABSOLUTE_URL);
}
$parts = parse_url(rtrim($this->defaultUri, '/'));
$context = $this->urlGenerator->getContext();
//Backup the original context, so we can restore it after generating the URL
$original = [
'host' => $context->getHost(),
'scheme' => $context->getScheme(),
'httpPort' => $context->getHttpPort(),
'httpsPort' => $context->getHttpsPort(),
'baseUrl' => $context->getBaseUrl(),
];
$scheme = $parts['scheme'] ?? 'https';
$context->setScheme($scheme);
$context->setHost($parts['host'] ?? '');
if (isset($parts['port'])) {
if ($scheme === 'https') {
$context->setHttpsPort($parts['port']);
} else {
$context->setHttpPort($parts['port']);
}
}
$context->setBaseUrl($parts['path'] ?? '');
try {
return $this->urlGenerator->generate($route, $parameters, UrlGeneratorInterface::ABSOLUTE_URL);
} finally {
//Restore the original context, so the rest of the request is not affected
$context->setHost($original['host']);
$context->setScheme($original['scheme']);
$context->setHttpPort($original['httpPort']);
$context->setHttpsPort($original['httpsPort']);
$context->setBaseUrl($original['baseUrl']);
}
}
}

View file

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace App\Services\UserSystem;
use App\Entity\UserSystem\User;
use App\Services\System\TrustedUrlGenerator;
use DateTime;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
@ -39,7 +40,8 @@ class PasswordResetManager
public function __construct(protected MailerInterface $mailer, protected EntityManagerInterface $em,
protected TranslatorInterface $translator, protected UserPasswordHasherInterface $userPasswordEncoder,
PasswordHasherFactoryInterface $encoderFactory)
PasswordHasherFactoryInterface $encoderFactory,
protected TrustedUrlGenerator $trustedUrlGenerator)
{
$this->passwordEncoder = $encoderFactory->getPasswordHasher(User::class);
}
@ -63,6 +65,9 @@ class PasswordResetManager
$user->setPwResetExpires($expiration_date);
if ($user->getEmail() !== null && $user->getEmail() !== '') {
$reset_url = $this->trustedUrlGenerator->generate('pw_reset_new_pw', ['user' => $user->getName(), 'token' => $unencrypted_token]);
$reset_url_fallback = $this->trustedUrlGenerator->generate('pw_reset_new_pw');
$address = new Address($user->getEmail(), $user->getFullName());
$mail = new TemplatedEmail();
$mail->to($address);
@ -72,6 +77,8 @@ class PasswordResetManager
'expiration_date' => $expiration_date,
'token' => $unencrypted_token,
'user' => $user,
'reset_url' => $reset_url,
'reset_url_fallback' => $reset_url_fallback,
]);
//Send email

View file

@ -72,7 +72,7 @@ class CanopySettings
/**
* @var string The domain used internally for the API requests. This is not necessarily the same as the domain shown to the user, which is determined by the keys of the ALLOWED_DOMAINS constant
*/
#[SettingsParameter(label: new TM("settings.ips.tme.country"), formType: ChoiceType::class, formOptions: ["choices" => self::ALLOWED_DOMAINS, 'translation_domain' => false])]
#[SettingsParameter(label: new TM("settings.ips.tme.country"), formType: ChoiceType::class, formOptions: ["choices" => self::ALLOWED_DOMAINS, 'choice_translation_domain' => false])]
public string $domain = "DE";
/**

View file

@ -0,0 +1,39 @@
{% extends "bundles/TwigBundle/Exception/error.html.twig" %}
{% set untrusted_host = exception.message starts with 'Untrusted Host' or exception.message starts with 'Invalid Host' %}
{% set seen_host = untrusted_host ? (exception.message|split('"'))[1]|default(null) : null %}
{% block status_comment %}
{% if untrusted_host %}
Part-DB was accessed using a host name that is not marked as trusted.<br>
{% if seen_host %}
The host name Part-DB saw was: <code>{{ seen_host }}</code>
{% else %}
<code>{{ exception.message }}</code>
{% endif %}
{% else %}
Your browser sent a request that the server could not understand.
{% endif %}
{% endblock %}
{% block admin_info %}
{% if untrusted_host %}
<b><i>Untrusted host name.</i></b><br>
<p>For security reasons (to prevent HTTP Host header attacks), Part-DB only responds to requests using host names that were explicitly marked as trusted, once more than one host name is used to access it (e.g. behind a reverse proxy).</p>
<p>Try following things:</p>
<ul>
<li>Ensure that you are using the correct URL to access Part-DB.</li>
<li>Set the <code>TRUSTED_HOSTS</code> environment variable in your <code>.env.local</code> file (or your docker-compose environment) to a regular expression matching all host names Part-DB should be reachable under, e.g.
{% if seen_host %}
<code>TRUSTED_HOSTS='^({{ seen_host|replace({'.': '\\.'}) }})$'</code> (if <code>{{ seen_host }}</code> is a host name you trust)
{% else %}
<code>TRUSTED_HOSTS='^(localhost|example\.com)$'</code>
{% endif %}
</li>
<li>If Part-DB is running behind a reverse proxy, also make sure that <code>TRUSTED_PROXIES</code> is configured correctly, so the original host name is forwarded properly.</li>
<li>Run <kbd>php bin/console cache:clear</kbd> after changing the configuration.</li>
</ul>
{% else %}
{{ parent() }}
{% endif %}
{% endblock %}

View file

@ -95,6 +95,16 @@
</div>
{% endif %}
{% if trusted_hosts_unconfigured 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.trusted_hosts.unconfigured.title{% endtrans %}</h5>
<p class="mb-1">{% trans %}system.trusted_hosts.unconfigured.message{% endtrans %}</p>
<p class="mb-0">{% trans %}system.trusted_hosts.unconfigured.suggestion{% endtrans %}
<br><code>TRUSTED_HOSTS='^({{ app.request.host|replace({'.': '\\.'}) }})$'</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

@ -6,9 +6,9 @@
<h4>{% trans with {'%name%': user.fullName|escape } %}email.hi %name%{% endtrans %},</h4>
{% trans %}email.pw_reset.message{% endtrans %}
<br>
<button class="large expand" href="{{ url('pw_reset_new_pw', {user: user.name, token: token}) }}">{% trans %}email.pw_reset.button{% endtrans %}</button>
<button class="large expand" href="{{ reset_url }}">{% trans %}email.pw_reset.button{% endtrans %}</button>
<br>
{% trans with {'%url%': url('pw_reset_new_pw') } %}email.pw_reset.fallback{% endtrans %}:
{% trans with {'%url%': reset_url_fallback } %}email.pw_reset.fallback{% endtrans %}:
<callout class="secondary">
<row>
<columns>

View file

@ -101,6 +101,28 @@ final class EntityExporterTest extends WebTestCase
unlink($tempFile);
}
public function testExportExcelFormulaInjectionPrevention(): void
{
// Values starting with formula characters must be stored as plain strings, not evaluated formulas
$entity = (new Category())->setName('=1+1')->setComment('@SUM(A1)');
$xlsxData = $this->service->exportEntities([$entity], ['format' => 'xlsx', 'level' => 'simple']);
$this->assertNotEmpty($xlsxData);
$tempFile = tempnam(sys_get_temp_dir(), 'test_formula') . '.xlsx';
file_put_contents($tempFile, $xlsxData);
$spreadsheet = IOFactory::load($tempFile);
$worksheet = $spreadsheet->getActiveSheet();
// The formula-prefixed name must be stored as a plain string, not a formula
$cell = $worksheet->getCell('A2');
$this->assertSame(\PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_STRING, $cell->getDataType());
$this->assertSame('=1+1', $cell->getValue());
unlink($tempFile);
}
public function testExportExcelFromRequest(): void
{
$entities = $this->getEntities();

View file

@ -133,25 +133,21 @@ final class LCSCProviderTest extends TestCase
{
$mockResponse = new MockResponse(json_encode([
'result' => [
'productSearchResultVO' => [
'productList' => [
[
'productCode' => 'C789012',
'productModel' => 'Regular Component',
'productIntroEn' => 'Regular description',
'brandNameEn' => 'Regular Manufacturer',
'encapStandard' => '0805',
'productImageUrl' => 'https://example.com/regular.jpg',
'productImages' => ['https://example.com/regular1.jpg'],
'productPriceList' => [
['ladder' => 10, 'productPrice' => '0.08', 'currencySymbol' => '€']
],
'paramVOList' => [],
'pdfUrl' => null,
'weight' => null
]
]
]
'exactMatchResult' => [[
'productCode' => 'C789012',
'productModel' => 'Regular Component',
'productIntroEn' => 'Regular description',
'brandNameEn' => 'Regular Manufacturer',
'encapStandard' => '0805',
'productImageUrl' => 'https://example.com/regular.jpg',
'productImages' => ['https://example.com/regular1.jpg'],
'productPriceList' => [
['ladder' => 10, 'productPrice' => '0.08', 'currencySymbol' => '€']
],
'paramVOList' => [],
'pdfUrl' => null,
'weight' => null
]]
]
]));
@ -166,45 +162,6 @@ final class LCSCProviderTest extends TestCase
$this->assertSame('Regular Component', $results[0]->name);
}
public function testSearchByKeywordWithTipProduct(): void
{
$mockResponse = new MockResponse(json_encode([
'result' => [
'productSearchResultVO' => [
'productList' => []
],
'tipProductDetailUrlVO' => [
'productCode' => 'C555555'
]
]
]));
$detailResponse = new MockResponse(json_encode([
'result' => [
'productCode' => 'C555555',
'productModel' => 'Tip Component',
'productIntroEn' => 'Tip description',
'brandNameEn' => 'Tip Manufacturer',
'encapStandard' => '1206',
'productImageUrl' => null,
'productImages' => [],
'productPriceList' => [],
'paramVOList' => [],
'pdfUrl' => null,
'weight' => null
]
]));
$this->httpClient->setResponseFactory([$mockResponse, $detailResponse]);
$results = $this->provider->searchByKeyword('special');
$this->assertIsArray($results);
$this->assertCount(1, $results);
$this->assertInstanceOf(PartDetailDTO::class, $results[0]);
$this->assertSame('C555555', $results[0]->provider_id);
$this->assertSame('Tip Component', $results[0]->name);
}
public function testSearchByKeywordsBatch(): void
{
@ -310,7 +267,7 @@ final class LCSCProviderTest extends TestCase
]
]));
$this->httpClient->setResponseFactory([$mockResponse]);
$this->httpClient->setResponseFactory([$mockResponse, $mockResponse]);
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('No part found with ID INVALID');
@ -322,8 +279,7 @@ final class LCSCProviderTest extends TestCase
{
$mockResponse = new MockResponse(json_encode([
'result' => [
'productSearchResultVO' => [
'productList' => [
'exactMatchResult' => [
[
'productCode' => 'C123456',
'productModel' => 'Component 1',
@ -350,7 +306,6 @@ final class LCSCProviderTest extends TestCase
'pdfUrl' => null,
'weight' => null
]
]
]
]
]));

View file

@ -0,0 +1,172 @@
<?php
/*
* 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/>.
*/
declare(strict_types=1);
namespace App\Tests\Services\System;
use App\Services\System\TrustedHostsChecker;
use App\Services\System\TrustedUrlGenerator;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RequestContext;
final class TrustedUrlGeneratorTest extends TestCase
{
private function createGenerator(UrlGeneratorInterface $urlGenerator, string $trustedHosts, string $defaultUri): TrustedUrlGenerator
{
return new TrustedUrlGenerator($urlGenerator, new TrustedHostsChecker($trustedHosts), $defaultUri);
}
public function testUsesCurrentRequestContextWhenTrustedHostsIsConfigured(): void
{
//If TRUSTED_HOSTS is configured, the request's host is already validated by Symfony, so it should be used as-is
$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
$urlGenerator->expects($this->never())->method('getContext');
$urlGenerator->expects($this->once())
->method('generate')
->with('pw_reset_new_pw', ['user' => 'john', 'token' => 'abc'], UrlGeneratorInterface::ABSOLUTE_URL)
->willReturn('https://request-host.example.com/pw_reset/new_pw/john/abc');
$generator = $this->createGenerator($urlGenerator, 'example\.com', 'https://trusted.example.com/');
$url = $generator->generate('pw_reset_new_pw', ['user' => 'john', 'token' => 'abc']);
$this->assertSame('https://request-host.example.com/pw_reset/new_pw/john/abc', $url);
}
public function testForcesDefaultUriHostWhenTrustedHostsIsNotConfigured(): void
{
//If TRUSTED_HOSTS is not configured, the request's Host header is attacker-controllable, so the
//host/scheme/base path configured via DEFAULT_URI must be used instead.
$context = new RequestContext();
$context->setScheme('http');
$context->setHost('attacker.evil');
$context->setBaseUrl('');
$capturedHost = null;
$capturedScheme = null;
$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
$urlGenerator->method('getContext')->willReturn($context);
$urlGenerator->expects($this->once())
->method('generate')
->with('pw_reset_new_pw', ['user' => 'john', 'token' => 'abc'], UrlGeneratorInterface::ABSOLUTE_URL)
->willReturnCallback(function () use ($context, &$capturedHost, &$capturedScheme) {
//Capture the context state as it is seen by the URL generator during the call
$capturedHost = $context->getHost();
$capturedScheme = $context->getScheme();
return $capturedScheme.'://'.$capturedHost.'/pw_reset/new_pw/john/abc';
});
$generator = $this->createGenerator($urlGenerator, '', 'https://trusted.example.com/');
$url = $generator->generate('pw_reset_new_pw', ['user' => 'john', 'token' => 'abc']);
$this->assertSame('trusted.example.com', $capturedHost);
$this->assertSame('https', $capturedScheme);
$this->assertSame('https://trusted.example.com/pw_reset/new_pw/john/abc', $url);
}
public function testRestoresOriginalContextAfterGenerating(): void
{
//The context is shared with the rest of the application, so it must not be left modified afterward
$context = new RequestContext();
$context->setScheme('http');
$context->setHost('attacker.evil');
$context->setHttpPort(8080);
$context->setBaseUrl('/original');
$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
$urlGenerator->method('getContext')->willReturn($context);
$urlGenerator->method('generate')->willReturn('https://trusted.example.com/foo');
$generator = $this->createGenerator($urlGenerator, '', 'https://trusted.example.com/sub/');
$generator->generate('some_route');
$this->assertSame('attacker.evil', $context->getHost());
$this->assertSame('http', $context->getScheme());
$this->assertSame(8080, $context->getHttpPort());
$this->assertSame('/original', $context->getBaseUrl());
}
public function testRestoresOriginalContextEvenWhenGenerateThrows(): void
{
$context = new RequestContext();
$context->setScheme('http');
$context->setHost('attacker.evil');
$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
$urlGenerator->method('getContext')->willReturn($context);
$urlGenerator->method('generate')->willThrowException(new \RuntimeException('route not found'));
$generator = $this->createGenerator($urlGenerator, '', 'https://trusted.example.com/');
try {
$generator->generate('unknown_route');
$this->fail('Expected exception was not thrown');
} catch (\RuntimeException) {
//Expected
}
$this->assertSame('attacker.evil', $context->getHost());
$this->assertSame('http', $context->getScheme());
}
public function testUsesHttpsPortForHttpsDefaultUri(): void
{
$context = new RequestContext();
$capturedHttpsPort = null;
$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
$urlGenerator->method('getContext')->willReturn($context);
$urlGenerator->method('generate')->willReturnCallback(function () use ($context, &$capturedHttpsPort) {
$capturedHttpsPort = $context->getHttpsPort();
return 'https://trusted.example.com:8443/foo';
});
$generator = $this->createGenerator($urlGenerator, '', 'https://trusted.example.com:8443/');
$generator->generate('some_route');
$this->assertSame(8443, $capturedHttpsPort);
}
public function testUsesBasePathFromDefaultUri(): void
{
$context = new RequestContext();
$capturedBaseUrl = null;
$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
$urlGenerator->method('getContext')->willReturn($context);
$urlGenerator->method('generate')->willReturnCallback(function () use ($context, &$capturedBaseUrl) {
$capturedBaseUrl = $context->getBaseUrl();
return 'https://trusted.example.com/partdb/foo';
});
$generator = $this->createGenerator($urlGenerator, '', 'https://trusted.example.com/partdb/');
$generator->generate('some_route');
$this->assertSame('/partdb', $capturedBaseUrl);
}
}

View file

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="ko">
<file id="frontend.en">
<unit id="eLrezdb" name="search.placeholder">
<segment state="translated">
<source>search.placeholder</source>
<target>검색</target>
</segment>
</unit>
<unit id="R4hoCqe" name="part.labelp">
<segment state="translated">
<source>part.labelp</source>
<target>부품</target>
</segment>
</unit>
<unit id="S4CxO.T" name="entity.select.group.new_not_added_to_DB">
<segment state="translated">
<source>entity.select.group.new_not_added_to_DB</source>
<target>신규 (아직 DB에 추가되지 않음)</target>
</segment>
</unit>
<unit id="9rnHbSK" name="user.password_strength.very_weak">
<segment state="translated">
<source>user.password_strength.very_weak</source>
<target>매우 약함</target>
</segment>
</unit>
<unit id="gKHmHwM" name="user.password_strength.weak">
<segment state="translated">
<source>user.password_strength.weak</source>
<target>약함</target>
</segment>
</unit>
<unit id="c44gN8b" name="user.password_strength.medium">
<segment state="translated">
<source>user.password_strength.medium</source>
<target>보통</target>
</segment>
</unit>
<unit id="NwiBLHc" name="user.password_strength.strong">
<segment state="translated">
<source>user.password_strength.strong</source>
<target>강함</target>
</segment>
</unit>
<unit id="Bw.iCUm" name="user.password_strength.very_strong">
<segment state="translated">
<source>user.password_strength.very_strong</source>
<target>매우 강함</target>
</segment>
</unit>
<unit id="U5IhkwB" name="search.submit">
<segment state="translated">
<source>search.submit</source>
<target>이동!</target>
</segment>
</unit>
<unit id="8d38e7538" name="user.password_strength.crack_time">
<segment state="translated">
<source>user.password_strength.crack_time</source>
<target>예상 해독 시간: %time%</target>
</segment>
</unit>
<unit id="dBtnOk01" name="dialog.btn.ok">
<segment state="translated">
<source>dialog.btn.ok</source>
<target>확인</target>
</segment>
</unit>
<unit id="dBtnCcl1" name="dialog.btn.cancel">
<segment state="translated">
<source>dialog.btn.cancel</source>
<target>취소</target>
</segment>
</unit>
<unit id="dBtnDny1" name="dialog.btn.deny">
<segment state="translated">
<source>dialog.btn.deny</source>
<target>아니요</target>
</segment>
</unit>
</file>
</xliff>

View file

@ -13695,6 +13695,24 @@ Buerklin-API-Authentication-Server:
<target>Sie können diesen zufällig generierten Wert verwenden (geben Sie ihn niemandem weiter):</target>
</segment>
</unit>
<unit id="OP7MacF" name="system.trusted_hosts.unconfigured.title">
<segment state="translated">
<source>system.trusted_hosts.unconfigured.title</source>
<target>TRUSTED_HOSTS nicht konfiguriert</target>
</segment>
</unit>
<unit id="88NkHLV" name="system.trusted_hosts.unconfigured.message">
<segment state="translated">
<source>system.trusted_hosts.unconfigured.message</source>
<target>Die Umgebungsvariable &lt;code&gt;TRUSTED_HOSTS&lt;/code&gt; ist nicht gesetzt. Das bedeutet, dass Part-DB Anfragen für jeden beliebigen Hostnamen akzeptiert, was ein Sicherheitsrisiko darstellen kann (z. B. HTTP-Host-Header-Injection). Es wird empfohlen, dies auf die Hostnamen zu beschränken, unter denen Part-DB tatsächlich erreichbar ist.</target>
</segment>
</unit>
<unit id="qZ_kgWT" name="system.trusted_hosts.unconfigured.suggestion">
<segment state="translated">
<source>system.trusted_hosts.unconfigured.suggestion</source>
<target>Legen Sie einen regulären Ausdruck fest, der alle Hostnamen abdeckt, unter denen Part-DB erreichbar sein soll, z. B. basierend auf dem Hostnamen, den Sie für den Zugriff auf diese Seite verwendet haben. Dies ist möglich in Ihrer Datei &lt;code&gt;.env.local&lt;/code&gt; oder &lt;code&gt;docker-compose.yaml&lt;/code&gt;:</target>
</segment>
</unit>
<unit id="cEwxoSj" name="info_providers.provider_key">
<segment state="translated">
<source>info_providers.provider_key</source>

View file

@ -675,7 +675,7 @@ Sub elements will be moved upwards.</target>
<unit id="maYlJO8" name="part_list.loading.message">
<segment state="translated">
<source>part_list.loading.message</source>
<target>This can take a moment. If this message do not disappear, try to reload the page.</target>
<target>This can take a moment. If this message does not disappear, try to reload the page.</target>
</segment>
</unit>
<unit id="wKzcAZd" name="vendor.base.javascript_hint">
@ -960,7 +960,7 @@ Sub elements will be moved upwards.</target>
<unit id="I6zehgT" name="email.pw_reset.message">
<segment state="translated">
<source>email.pw_reset.message</source>
<target>somebody (hopefully you) requested a reset of your password. If this request was not made by you, ignore this mail.</target>
<target>Somebody (hopefully you) requested a reset of your password. If this request was not made by you, ignore this mail.</target>
</segment>
</unit>
<unit id="P8NjvoC" name="email.pw_reset.button">
@ -2143,7 +2143,7 @@ Also note that without two-factor authentication, your account is no longer as w
<unit id="h3376Kl" name="tfa_google.step.download">
<segment state="translated">
<source>tfa_google.step.download</source>
<target>Download an authenticator app (e.g. &lt;a class="link-external" target="_blank" href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2"&gt;Google Authenticator&lt;/a&gt; oder &lt;a class="link-external" target="_blank" href="https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp"&gt;FreeOTP Authenticator&lt;/a&gt;)</target>
<target>Download an authenticator app (e.g. &lt;a class="link-external" target="_blank" href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2"&gt;Google Authenticator&lt;/a&gt; or &lt;a class="link-external" target="_blank" href="https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp"&gt;FreeOTP Authenticator&lt;/a&gt;)</target>
</segment>
</unit>
<unit id="eriwJoR" name="tfa_google.step.scan">
@ -3065,7 +3065,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can
<unit id="AdYtBQG" name="attachment.edit.url.help">
<segment state="translated">
<source>attachment.edit.url.help</source>
<target>You can specify an URL to an external file here, or input an keyword which is used to search in built-in resources (e.g. footprints)</target>
<target>You can specify a URL to an external file here, or input a keyword which is used to search in built-in resources (e.g. footprints)</target>
</segment>
</unit>
<unit id="jXplbqd" name="attachment.edit.name">
@ -4208,7 +4208,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can
</notes>
<segment state="translated">
<source>validator.noLockout</source>
<target>You can not withdraw yourself the "change permission" permission, to prevent that you lockout yourself accidentally.</target>
<target>You can not withdraw yourself the "change permission" permission, to prevent that you lock yourself out accidentally.</target>
</segment>
</unit>
<unit id="tS2P7Jn" name="attachment_type.edit.filetype_filter">
@ -4703,7 +4703,7 @@ Element 1 -&gt; Element 1.2</target>
</notes>
<segment state="translated">
<source>storelocation.edit.is_full.label</source>
<target>Storelocation full</target>
<target>Storage location full</target>
</segment>
</unit>
<unit id="XF4C7ZG" name="storelocation.edit.is_full.help">
@ -4713,7 +4713,7 @@ Element 1 -&gt; Element 1.2</target>
</notes>
<segment state="translated">
<source>storelocation.edit.is_full.help</source>
<target>If this option is selected, it is neither possible to add new [[part]] to this storelocation or to increase the amount of existing [[part]].</target>
<target>If this option is selected, it is neither possible to add new [[part]] to this storage location or to increase the amount of existing [[part]].</target>
</segment>
</unit>
<unit id="V1su4ac" name="storelocation.limit_to_existing.label">
@ -4733,7 +4733,7 @@ Element 1 -&gt; Element 1.2</target>
</notes>
<segment state="translated">
<source>storelocation.limit_to_existing.help</source>
<target>If this option is activated, it is not possible to add new [[part]] to this storelocation, but the amount of existing [[part]] can be increased.</target>
<target>If this option is activated, it is not possible to add new [[part]] to this storage location, but the amount of existing [[part]] can be increased.</target>
</segment>
</unit>
<unit id="tGVVeof" name="storelocation.only_single_part.label">
@ -5988,7 +5988,7 @@ Element 1 -&gt; Element 1.2</target>
</notes>
<segment state="translated">
<source>log.type.security.trusted_device_reset</source>
<target>Trusted devices resetted</target>
<target>Trusted devices reset</target>
</segment>
</unit>
<unit id="a.Le1i0" name="log.type.collection_element_deleted">
@ -6123,7 +6123,7 @@ Element 1 -&gt; Element 1.2</target>
<unit id="uxw7_77" name="group.edit.enforce_2fa.help">
<segment state="translated">
<source>group.edit.enforce_2fa.help</source>
<target>If this option is enabled, every direct member of this group, has to configure at least one second-factor for authentication. Recommended for administrative groups with much permissions.</target>
<target>If this option is enabled, every direct member of this group, has to configure at least one second-factor for authentication. Recommended for administrative groups with many permissions.</target>
</segment>
</unit>
<unit id="NwsYm0P" name="selectpicker.nothing_selected">
@ -6363,7 +6363,7 @@ Element 1 -&gt; Element 1.2</target>
<unit id="CdphPiM" name="tools.reel_calc.explanation">
<segment state="translated">
<source>tools.reel_calc.explanation</source>
<target>This calculator gives you an estimation, how many parts are remaining on an SMD reel. Measure the noted the dimensions on the reel (or use some of the presets) and click "Update" to get an result.</target>
<target>This calculator gives you an estimation, how many parts are remaining on an SMD reel. Measure the noted the dimensions on the reel (or use the presets) and click "Update" to get a result.</target>
</segment>
</unit>
<unit id="21dEh63" name="perm.tools.reel_calculator">
@ -7893,7 +7893,7 @@ Element 1 -&gt; Element 1.2</target>
<unit id="h9kfyaE" name="log.element_edited.changed_fields.is_full">
<segment state="translated">
<source>log.element_edited.changed_fields.is_full</source>
<target>Storelocation full</target>
<target>Storage location full</target>
</segment>
</unit>
<unit id="K754HtO" name="log.element_edited.changed_fields.limit_to_existing_parts">
@ -8289,13 +8289,13 @@ Element 1 -&gt; Element 1.2</target>
<unit id="gt2SlQN" name="import.create_unknown_datastructures">
<segment state="translated">
<source>import.create_unknown_datastructures</source>
<target>Create unknown datastructures</target>
<target>Create unknown data structures</target>
</segment>
</unit>
<unit id=".TjGPnB" name="import.create_unknown_datastructures.help">
<segment state="translated">
<source>import.create_unknown_datastructures.help</source>
<target>If this is selected, datastructures (like categories, footprints, etc.) which does not exist in the database yet, will be automatically created. If this is not selected, only existing data structures will be used, and if no matching data structure is found, the part will get assigned nothing</target>
<target>If this is selected, data structures (like categories, footprints, etc.) which does not exist in the database yet, will be automatically created. If this is not selected, only existing data structures will be used, and if no matching data structure is found, the part will get assigned nothing</target>
</segment>
</unit>
<unit id="y_rsaV8" name="import.path_delimiter">
@ -8307,7 +8307,7 @@ Element 1 -&gt; Element 1.2</target>
<unit id="AOG.hXc" name="import.path_delimiter.help">
<segment state="translated">
<source>import.path_delimiter.help</source>
<target>The delimiter used to mark different levels in data structure pathes like category, footprint, etc.</target>
<target>The delimiter used to mark different levels in data structure paths like category, footprint, etc.</target>
</segment>
</unit>
<unit id="p_IxB9K" name="parts.import.help_documentation">
@ -8367,7 +8367,7 @@ Element 1 -&gt; Element 1.2</target>
<unit id="tNnYa0u" name="project.bom_import.type.kicad_pcbnew">
<segment state="translated">
<source>project.bom_import.type.kicad_pcbnew</source>
<target>KiCAD Pcbnew BOM (CSV file)</target>
<target>KiCad Pcbnew BOM (CSV file)</target>
</segment>
</unit>
<unit id="AtW_ooO" name="project.bom_import.clear_existing_bom">
@ -8535,7 +8535,7 @@ Element 1 -&gt; Element 1.2</target>
<unit id="sg8NMZk" name="log.user_login.ip_anonymize_hint">
<segment state="translated">
<source>log.user_login.ip_anonymize_hint</source>
<target>If the last digits of the IP address are missing, then the GDPR mode is enabled, in which IP addresses are anynomized.</target>
<target>If the last digits of the IP address are missing, then the GDPR mode is enabled, in which IP addresses are anonymized.</target>
</segment>
</unit>
<unit id="K_LOvwj" name="log.user_not_allowed.unauthorized_access_attempt_to">
@ -8571,7 +8571,7 @@ Element 1 -&gt; Element 1.2</target>
<unit id="YQafwyc" name="error_table.error">
<segment state="translated">
<source>error_table.error</source>
<target>An error occured during your request.</target>
<target>An error occurred during your request.</target>
</segment>
</unit>
<unit id="SgZGiGG" name="part.table.invalid_regex">
@ -9347,7 +9347,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g
<unit id="Y8bGSEj" name="permission.legend.dependency_note">
<segment state="translated">
<source>permission.legend.dependency_note</source>
<target>Please note that some permission operations depend on each other. If you encounter a warning that missing permissions were corrected and a permission was set to allow again, you have to set the dependent operation to forbid too. The dependents can normally found right of an operation.</target>
<target>Please note that some permission operations depend on each other. If you encounter a warning that missing permissions were corrected and a permission was set to allow again, you have to set the dependent operation to forbid too. The dependents can normally be found right of an operation.</target>
</segment>
</unit>
<unit id="zWj5h2X" name="log.part_stock_changed.timestamp">
@ -12152,7 +12152,7 @@ Buerklin-API Authentication server:
<unit id="RFhwYWd" name="info_providers.search.error.transport_exception">
<segment state="translated">
<source>info_providers.search.error.transport_exception</source>
<target>Transport error while retrieving information from the providers. Check that your server has internet accesss. See server logs for more info.</target>
<target>Transport error while retrieving information from the providers. Check that your server has internet access. See server logs for more info.</target>
</segment>
</unit>
<unit id="PExOL_J" name="update_manager.title">
@ -13697,6 +13697,24 @@ Buerklin-API Authentication server:
<target>You can use this randomly generated value (share it with nobody):</target>
</segment>
</unit>
<unit id="OP7MacF" name="system.trusted_hosts.unconfigured.title">
<segment state="translated">
<source>system.trusted_hosts.unconfigured.title</source>
<target>TRUSTED_HOSTS not configured</target>
</segment>
</unit>
<unit id="88NkHLV" name="system.trusted_hosts.unconfigured.message">
<segment state="translated">
<source>system.trusted_hosts.unconfigured.message</source>
<target>The &lt;code&gt;TRUSTED_HOSTS&lt;/code&gt; environment variable is not set. This means Part-DB will accept requests for any host name, which can be a security risk (e.g. HTTP Host header injection). It is recommended to restrict this to the host names Part-DB is actually reachable under.</target>
</segment>
</unit>
<unit id="qZ_kgWT" name="system.trusted_hosts.unconfigured.suggestion">
<segment state="translated">
<source>system.trusted_hosts.unconfigured.suggestion</source>
<target>Set it to a regular expression matching all host names Part-DB should be reachable under in your &lt;code&gt;.env.local&lt;/code&gt; or &lt;code&gt;docker-compose.yaml&lt;/code&gt; file, e.g. based on the host name you used to access this page:</target>
</segment>
</unit>
<unit id="cEwxoSj" name="info_providers.provider_key">
<segment state="translated">
<source>info_providers.provider_key</source>

13749
translations/messages.ko.xlf Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="ko">
<file id="security.en">
<unit id="GrLNa9P" name="user.login_error.user_disabled">
<segment state="translated">
<source>user.login_error.user_disabled</source>
<target>계정이 비활성화되어 있습니다! 오류라고 생각되면 관리자에게 문의하세요.</target>
</segment>
</unit>
<unit id="IFQ5XrG" name="saml.error.cannot_login_local_user_per_saml">
<segment state="translated">
<source>saml.error.cannot_login_local_user_per_saml</source>
<target>로컬 사용자는 SSO로 로그인할 수 없습니다! 로컬 사용자 비밀번호를 사용하세요.</target>
</segment>
</unit>
<unit id="wOYPZmb" name="saml.error.cannot_login_saml_user_locally">
<segment state="translated">
<source>saml.error.cannot_login_saml_user_locally</source>
<target>SAML 사용자는 로컬 인증으로 로그인할 수 없습니다! SSO 로그인을 사용하세요.</target>
</segment>
</unit>
</file>
</xliff>

View file

@ -0,0 +1,275 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="ko">
<file id="validators.en">
<unit id="cRbk.cm" name="part.master_attachment.must_be_picture">
<segment state="translated">
<source>part.master_attachment.must_be_picture</source>
<target>미리보기 첨부파일은 유효한 사진이어야 합니다!</target>
</segment>
</unit>
<unit id="v8HkcJB" name="structural.entity.unique_name">
<segment state="translated">
<source>structural.entity.unique_name</source>
<target>이 계층에 같은 이름의 요소가 이미 있습니다!</target>
</segment>
</unit>
<unit id="dW7b2B_" name="parameters.validator.min_lesser_typical">
<segment state="translated">
<source>parameters.validator.min_lesser_typical</source>
<target>값은 일반값({{ compared_value }}) 이하여야 합니다.</target>
</segment>
</unit>
<unit id="Yfp2uC5" name="parameters.validator.min_lesser_max">
<segment state="translated">
<source>parameters.validator.min_lesser_max</source>
<target>값은 최대값({{ compared_value }})보다 작아야 합니다.</target>
</segment>
</unit>
<unit id="P6b.8Ou" name="parameters.validator.max_greater_typical">
<segment state="translated">
<source>parameters.validator.max_greater_typical</source>
<target>값은 일반값({{ compared_value }}) 이상이어야 합니다.</target>
</segment>
</unit>
<unit id="P41193Y" name="validator.user.username_already_used">
<segment state="translated">
<source>validator.user.username_already_used</source>
<target>같은 이름의 사용자가 이미 있습니다</target>
</segment>
</unit>
<unit id="EKPQiyf" name="user.invalid_username">
<segment state="translated">
<source>user.invalid_username</source>
<target>사용자 이름은 문자, 숫자, 밑줄, 마침표, 더하기, 빼기만 포함할 수 있으며 @로 시작할 수 없습니다!</target>
</segment>
</unit>
<unit id="_v.DMg." name="validator.noneofitschild.self">
<notes>
<note category="state" priority="1">obsolete</note>
</notes>
<segment state="translated">
<source>validator.noneofitschild.self</source>
<target>요소는 자기 자신의 상위가 될 수 없습니다!</target>
</segment>
</unit>
<unit id="W90LyFQ" name="validator.noneofitschild.children">
<notes>
<note category="state" priority="1">obsolete</note>
</notes>
<segment state="translated">
<source>validator.noneofitschild.children</source>
<target>하위 요소를 상위 요소로 지정할 수 없습니다 (순환이 발생합니다)!</target>
</segment>
</unit>
<unit id="GAUS.LK" name="validator.select_valid_category">
<segment state="translated">
<source>validator.select_valid_category</source>
<target>유효한 카테고리를 선택하세요!</target>
</segment>
</unit>
<unit id="h6qELde" name="validator.part_lot.only_existing">
<segment state="translated">
<source>validator.part_lot.only_existing</source>
<target>이 위치는 "기존 항목만"으로 표시되어 새 부품을 추가할 수 없습니다</target>
</segment>
</unit>
<unit id="Prriyy0" name="validator.part_lot.location_full.no_increase">
<segment state="translated">
<source>validator.part_lot.location_full.no_increase</source>
<target>위치가 가득 찼습니다. 수량을 늘릴 수 없습니다 (새 값은 {{ old_amount }}보다 작아야 합니다).</target>
</segment>
</unit>
<unit id="eeEjB4s" name="validator.part_lot.location_full">
<segment state="translated">
<source>validator.part_lot.location_full</source>
<target>위치가 가득 찼습니다. 새 부품을 추가할 수 없습니다.</target>
</segment>
</unit>
<unit id="2yWi8eP" name="validator.part_lot.single_part">
<segment state="translated">
<source>validator.part_lot.single_part</source>
<target>이 위치는 단일 부품만 보관할 수 있으며 이미 가득 찼습니다!</target>
</segment>
</unit>
<unit id="A.TFhbb" name="validator.attachment.must_not_be_null">
<segment state="translated">
<source>validator.attachment.must_not_be_null</source>
<target>첨부파일 형식을 선택해야 합니다!</target>
</segment>
</unit>
<unit id=".lqKoij" name="validator.orderdetail.supplier_must_not_be_null">
<segment state="translated">
<source>validator.orderdetail.supplier_must_not_be_null</source>
<target>판매처를 선택해야 합니다!</target>
</segment>
</unit>
<unit id="bcNZzK." name="validator.measurement_unit.use_si_prefix_needs_unit">
<segment state="translated">
<source>validator.measurement_unit.use_si_prefix_needs_unit</source>
<target>SI 접두사를 사용하려면 단위 기호를 설정해야 합니다!</target>
</segment>
</unit>
<unit id="gZ5FFL1" name="part.ipn.must_be_unique">
<segment state="translated">
<source>part.ipn.must_be_unique</source>
<target>내부 부품 번호는 고유해야 합니다. {{ value }}은(는) 이미 사용 중입니다!</target>
</segment>
</unit>
<unit id="P31Yg.d" name="validator.project.bom_entry.name_or_part_needed">
<segment state="translated">
<source>validator.project.bom_entry.name_or_part_needed</source>
<target>부품 BOM 항목에는 부품을 선택하고, 부품이 아닌 BOM 항목에는 이름을 설정해야 합니다.</target>
</segment>
</unit>
<unit id="5CEup_N" name="project.bom_entry.name_already_in_bom">
<segment state="translated">
<source>project.bom_entry.name_already_in_bom</source>
<target>같은 이름의 BOM 항목이 이미 있습니다!</target>
</segment>
</unit>
<unit id="jB3B50E" name="project.bom_entry.part_already_in_bom">
<segment state="translated">
<source>project.bom_entry.part_already_in_bom</source>
<target>이 부품은 이미 BOM에 있습니다!</target>
</segment>
</unit>
<unit id="NdkzP1n" name="project.bom_entry.mountnames_quantity_mismatch">
<segment state="translated">
<source>project.bom_entry.mountnames_quantity_mismatch</source>
<target>장착 이름의 수는 BOM 수량과 일치해야 합니다!</target>
</segment>
</unit>
<unit id="8teRCgR" name="project.bom_entry.can_not_add_own_builds_part">
<segment state="translated">
<source>project.bom_entry.can_not_add_own_builds_part</source>
<target>프로젝트 자신의 빌드 부품을 BOM에 추가할 수 없습니다.</target>
</segment>
</unit>
<unit id="asBxPxe" name="project.bom_has_to_include_all_subelement_parts">
<segment state="translated">
<source>project.bom_has_to_include_all_subelement_parts</source>
<target>프로젝트 BOM에는 모든 하위 프로젝트의 빌드 부품이 포함되어야 합니다. %project_name% 프로젝트의 %part_name% 부품이 누락되었습니다!</target>
</segment>
</unit>
<unit id="uxaE9Ct" name="project.bom_entry.price_not_allowed_on_parts">
<segment state="translated">
<source>project.bom_entry.price_not_allowed_on_parts</source>
<target>부품과 연결된 BOM 항목에는 가격을 설정할 수 없습니다. 대신 부품에 가격을 정의하세요.</target>
</segment>
</unit>
<unit id="xZ68Nzl" name="validator.project_build.lot_bigger_than_needed">
<segment state="translated">
<source>validator.project_build.lot_bigger_than_needed</source>
<target>필요한 것보다 많은 출고 수량을 선택했습니다! 불필요한 수량을 제거하세요.</target>
</segment>
</unit>
<unit id="68_.V_X" name="validator.project_build.lot_smaller_than_needed">
<segment state="translated">
<source>validator.project_build.lot_smaller_than_needed</source>
<target>제작에 필요한 것보다 적은 출고 수량을 선택했습니다! 수량을 추가하세요.</target>
</segment>
</unit>
<unit id="yZGS8uZ" name="part.name.must_match_category_regex">
<segment state="translated">
<source>part.name.must_match_category_regex</source>
<target>부품 이름이 카테고리에서 지정한 정규 표현식과 일치하지 않습니다: %regex%</target>
</segment>
</unit>
<unit id="Q8wP5Jd" name="validator.attachment.name_not_blank">
<segment state="translated">
<source>validator.attachment.name_not_blank</source>
<target>여기에 값을 설정하거나, 파일을 업로드하여 파일 이름을 첨부파일 이름으로 자동 사용하세요.</target>
</segment>
</unit>
<unit id="DH0IkNR" name="validator.part_lot.owner_must_match_storage_location_owner">
<segment state="translated">
<source>validator.part_lot.owner_must_match_storage_location_owner</source>
<target>이 로트의 소유자는 선택한 보관 위치의 소유자(%owner_name%)와 일치해야 합니다!</target>
</segment>
</unit>
<unit id="TzySicw" name="validator.part_lot.owner_must_not_be_anonymous">
<segment state="translated">
<source>validator.part_lot.owner_must_not_be_anonymous</source>
<target>로트 소유자는 익명 사용자일 수 없습니다!</target>
</segment>
</unit>
<unit id="GthNWUb" name="validator.part_association.must_set_an_value_if_type_is_other">
<segment state="translated">
<source>validator.part_association.must_set_an_value_if_type_is_other</source>
<target>유형을 "기타"로 설정한 경우 이를 설명하는 값을 설정해야 합니다!</target>
</segment>
</unit>
<unit id="Be4Im81" name="validator.part_association.part_cannot_be_associated_with_itself">
<segment state="translated">
<source>validator.part_association.part_cannot_be_associated_with_itself</source>
<target>부품은 자기 자신과 연관될 수 없습니다!</target>
</segment>
</unit>
<unit id="q5Ej6Xm" name="validator.part_association.already_exists">
<segment state="translated">
<source>validator.part_association.already_exists</source>
<target>이 부품과의 연관이 이미 있습니다!</target>
</segment>
</unit>
<unit id="HbI5bga" name="validator.part_lot.vendor_barcode_must_be_unique">
<segment state="translated">
<source>validator.part_lot.vendor_barcode_must_be_unique</source>
<target>이 공급업체 바코드 값은 다른 로트에서 이미 사용되었습니다. 바코드는 고유해야 합니다!</target>
</segment>
</unit>
<unit id="ufQJh7E" name="validator.year_2038_bug_on_32bit">
<segment state="translated">
<source>validator.year_2038_bug_on_32bit</source>
<target>기술적 제약으로 인해 32비트 시스템에서는 2038-01-19 이후의 날짜를 선택할 수 없습니다!</target>
</segment>
</unit>
<unit id="iM9yb_p" name="validator.fileSize.invalidFormat">
<segment state="translated">
<source>validator.fileSize.invalidFormat</source>
<target>유효하지 않은 파일 크기 형식입니다. 정수와 함께 킬로, 메가, 기가바이트를 뜻하는 K, M, G 접미사를 사용하세요.</target>
</segment>
</unit>
<unit id="ZFxQ0BZ" name="validator.invalid_range">
<segment state="translated">
<source>validator.invalid_range</source>
<target>지정한 범위가 유효하지 않습니다!</target>
</segment>
</unit>
<unit id="m4gp2P_" name="validator.google_code.wrong_code">
<segment state="translated">
<source>validator.google_code.wrong_code</source>
<target>유효하지 않은 코드입니다. 인증 앱이 올바르게 설정되어 있는지, 서버와 인증 기기의 시간이 정확한지 확인하세요.</target>
</segment>
</unit>
<unit id="I330cr5" name="settings.synonyms.type_synonyms.collection_type.duplicate">
<segment state="translated">
<source>settings.synonyms.type_synonyms.collection_type.duplicate</source>
<target>이 유형과 언어에 대한 번역이 이미 정의되어 있습니다!</target>
</segment>
</unit>
<unit id="zT_j_oQ" name="validator.invalid_gtin">
<segment state="translated">
<source>validator.invalid_gtin</source>
<target>유효한 GTIN / EAN이 아닙니다!</target>
</segment>
</unit>
<unit id="vnpejmb" name="info_providers.validation.provider_id_without_key">
<segment state="translated">
<source>info_providers.validation.provider_id_without_key</source>
<target>제공자 ID를 지정하려면 정보 제공자도 지정하거나, 둘 다 제거해야 합니다.</target>
</segment>
</unit>
<unit id="yFlA5OA" name="info_providers.validation.provider_url_without_key">
<segment state="translated">
<source>info_providers.validation.provider_url_without_key</source>
<target>제공자 URL을 지정하려면 정보 제공자도 지정해야 합니다.</target>
</segment>
</unit>
<unit id="gUHUXoV" name="info_providers.validation.provider_key_without_id">
<segment state="translated">
<source>info_providers.validation.provider_key_without_id</source>
<target>정보 제공자를 지정하려면 제공자 ID도 지정하거나, 둘 다 제거해야 합니다.</target>
</segment>
</unit>
</file>
</xliff>

1459
yarn.lock

File diff suppressed because it is too large Load diff