Compare commits

..

No commits in common. "master" and "postgres" have entirely different histories.

436 changed files with 15646 additions and 50313 deletions

View file

@ -39,8 +39,8 @@ if [ -d /var/www/html/var/db ]; then
fi fi
fi fi
# Start PHP-FPM (the PHP_VERSION is replaced by the configured version in the Dockerfile) # Start PHP-FPM
service phpPHP_VERSION-fpm start service php8.1-fpm start
# first arg is `-f` or `--some-option` (taken from https://github.com/docker-library/php/blob/master/8.2/bullseye/apache/docker-php-entrypoint) # first arg is `-f` or `--some-option` (taken from https://github.com/docker-library/php/blob/master/8.2/bullseye/apache/docker-php-entrypoint)
if [ "${1#-}" != "$1" ]; then if [ "${1#-}" != "$1" ]; then

View file

@ -43,9 +43,6 @@
PassEnv PROVIDER_OCTOPART_CLIENT_ID PROVIDER_OCTOPART_SECRET PROVIDER_OCTOPART_CURRENCY PROVIDER_OCTOPART_COUNTRY PROVIDER_OCTOPART_SEARCH_LIMIT PROVIDER_OCTOPART_ONLY_AUTHORIZED_SELLERS PassEnv PROVIDER_OCTOPART_CLIENT_ID PROVIDER_OCTOPART_SECRET PROVIDER_OCTOPART_CURRENCY PROVIDER_OCTOPART_COUNTRY PROVIDER_OCTOPART_SEARCH_LIMIT PROVIDER_OCTOPART_ONLY_AUTHORIZED_SELLERS
PassEnv PROVIDER_MOUSER_KEY PROVIDER_MOUSER_SEARCH_OPTION PROVIDER_MOUSER_SEARCH_LIMIT PROVIDER_MOUSER_SEARCH_WITH_SIGNUP_LANGUAGE PassEnv PROVIDER_MOUSER_KEY PROVIDER_MOUSER_SEARCH_OPTION PROVIDER_MOUSER_SEARCH_LIMIT PROVIDER_MOUSER_SEARCH_WITH_SIGNUP_LANGUAGE
PassEnv PROVIDER_LCSC_ENABLED PROVIDER_LCSC_CURRENCY PassEnv PROVIDER_LCSC_ENABLED PROVIDER_LCSC_CURRENCY
PassEnv PROVIDER_OEMSECRETS_KEY PROVIDER_OEMSECRETS_COUNTRY_CODE PROVIDER_OEMSECRETS_CURRENCY PROVIDER_OEMSECRETS_ZERO_PRICE PROVIDER_OEMSECRETS_SET_PARAM PROVIDER_OEMSECRETS_SORT_CRITERIA
PassEnv PROVIDER_REICHELT_ENABLED PROVIDER_REICHELT_CURRENCY PROVIDER_REICHELT_COUNTRY PROVIDER_REICHELT_LANGUAGE PROVIDER_REICHELT_INCLUDE_VAT
PassEnv PROVIDER_POLLIN_ENABLED
PassEnv EDA_KICAD_CATEGORY_DEPTH PassEnv EDA_KICAD_CATEGORY_DEPTH
# For most configuration files from conf-available/, which are # For most configuration files from conf-available/, which are

60
.env
View file

@ -24,7 +24,7 @@ DATABASE_MYSQL_USE_SSL_CA=0
DATABASE_MYSQL_SSL_VERIFY_CERT=1 DATABASE_MYSQL_SSL_VERIFY_CERT=1
# Emulate natural sorting of strings even on databases that do not support it (like SQLite, MySQL or MariaDB < 10.7) # Emulate natural sorting of strings even on databases that do not support it (like SQLite, MySQL or MariaDB < 10.7)
# This can be slow on big databases and might have some problems and quirks, so use it with caution # This can be slow on big databases
DATABASE_EMULATE_NATURAL_SORT=0 DATABASE_EMULATE_NATURAL_SORT=0
################################################################################### ###################################################################################
@ -143,8 +143,7 @@ PROVIDER_TME_CURRENCY=EUR
PROVIDER_TME_LANGUAGE=en PROVIDER_TME_LANGUAGE=en
# The country to get results for # The country to get results for
PROVIDER_TME_COUNTRY=DE PROVIDER_TME_COUNTRY=DE
# [DEPRECATED] Set this to 1 to get gross prices (including VAT) instead of net prices # Set this to 1 to get gross prices (including VAT) instead of net prices
# With private API keys, this option cannot be used anymore is ignored by Part-DB. The VAT inclusion depends on your TME account settings.
PROVIDER_TME_GET_GROSS_PRICES=1 PROVIDER_TME_GET_GROSS_PRICES=1
# Octopart / Nexar Provider: # Octopart / Nexar Provider:
@ -182,61 +181,6 @@ PROVIDER_LCSC_ENABLED=0
# The currency to get prices in (e.g. EUR, USD, etc.) # The currency to get prices in (e.g. EUR, USD, etc.)
PROVIDER_LCSC_CURRENCY=EUR PROVIDER_LCSC_CURRENCY=EUR
# Oemsecrets Provider API 3.0.1:
# You can get your API key from https://www.oemsecrets.com/api
PROVIDER_OEMSECRETS_KEY=
# The country you want the output for
PROVIDER_OEMSECRETS_COUNTRY_CODE=DE
# Available country code are:
# AD, AE, AQ, AR, AT, AU, BE, BO, BR, BV, BY, CA, CH, CL, CN, CO, CZ, DE, DK, EC, EE, EH,
# ES, FI, FK, FO, FR, GB, GE, GF, GG, GI, GL, GR, GS, GY, HK, HM, HR, HU, IE, IM, IN, IS,
# IT, JM, JP, KP, KR, KZ, LI, LK, LT, LU, MC, MD, ME, MK, MT, NL, NO, NZ, PE, PH, PL, PT,
# PY, RO, RS, RU, SB, SD, SE, SG, SI, SJ, SK, SM, SO, SR, SY, SZ, TC, TF, TG, TH, TJ, TK,
# TM, TN, TO, TR, TT, TV, TW, TZ, UA, UG, UM, US, UY, UZ, VA, VE, VG, VI, VN, VU, WF, YE,
# ZA, ZM, ZW
#
# The currency you want the prices to be displayed in
PROVIDER_OEMSECRETS_CURRENCY=EUR
# Available currency are:AUD, CAD, CHF, CNY, DKK, EUR, GBP, HKD, ILS, INR, JPY, KRW, NOK,
# NZD, RUB, SEK, SGD, TWD, USD
#
# If PROVIDER_OEMSECRETS_ZERO_PRICE is set to 0, distributors with zero prices
# will be discarded from the creation of a new part (set to 1 otherwise)
PROVIDER_OEMSECRETS_ZERO_PRICE=0
#
# When PROVIDER_OEMSECRETS_SET_PARAM is set to 1 the parameters for the part are generated
# from the description transforming unstructured descriptions into structured parameters;
# each parameter in description should have the form: "...;name1:value1;name2:value2"
PROVIDER_OEMSECRETS_SET_PARAM=1
#
# This environment variable determines the sorting criteria for product results.
# The sorting process first arranges items based on the provided keyword.
# Then, if set to 'C', it further sorts by completeness (prioritizing items with the most
# detailed information). If set to 'M', it further sorts by manufacturer name.
#If unset or set to any other value, no sorting is performed.
PROVIDER_OEMSECRETS_SORT_CRITERIA=C
# Reichelt provider:
# Reichelt.com offers no official API, so this info provider webscrapes the website to extract info
# It could break at any time, use it at your own risk
# We dont require an API key for Reichelt, just set this to 1 to enable Reichelt support
PROVIDER_REICHELT_ENABLED=0
# The country to get prices for
PROVIDER_REICHELT_COUNTRY=DE
# The language to get results in (en, de, fr, nl, pl, it, es)
PROVIDER_REICHELT_LANGUAGE=en
# Include VAT in prices (set to 1 to include VAT, 0 to exclude VAT)
PROVIDER_REICHELT_INCLUDE_VAT=1
# The currency to get prices in (only for countries with countries other than EUR)
PROVIDER_REICHELT_CURRENCY=EUR
# Pollin provider:
# Pollin.de offers no official API, so this info provider webscrapes the website to extract info
# It could break at any time, use it at your own risk
# We dont require an API key for Pollin, just set this to 1 to enable Pollin support
PROVIDER_POLLIN_ENABLED=0
################################################################################## ##################################################################################
# EDA integration related settings # EDA integration related settings
################################################################################## ##################################################################################

View file

View file

@ -65,7 +65,7 @@ jobs:
- -
name: Build and push name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v5
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7 platforms: linux/amd64,linux/arm64,linux/arm/v7

View file

@ -65,7 +65,7 @@ jobs:
- -
name: Build and push name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v5
with: with:
context: . context: .
file: Dockerfile-frankenphp file: Dockerfile-frankenphp

View file

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

View file

@ -1,64 +1,22 @@
ARG BASE_IMAGE=debian:bookworm-slim FROM debian:bullseye-slim
ARG PHP_VERSION=8.3
FROM ${BASE_IMAGE} AS base
ARG PHP_VERSION
# Install needed dependencies for PHP build # Install needed dependencies for PHP build
#RUN apt-get update && apt-get install -y pkg-config curl libcurl4-openssl-dev libicu-dev \ #RUN apt-get update && apt-get install -y pkg-config curl libcurl4-openssl-dev libicu-dev \
# libpng-dev libjpeg-dev libfreetype6-dev gnupg zip libzip-dev libjpeg62-turbo-dev libonig-dev libxslt-dev libwebp-dev vim \ # libpng-dev libjpeg-dev libfreetype6-dev gnupg zip libzip-dev libjpeg62-turbo-dev libonig-dev libxslt-dev libwebp-dev vim \
# && apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/* # && apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*
RUN apt-get update && apt-get -y install \ RUN apt-get update && apt-get -y install apt-transport-https lsb-release ca-certificates curl zip mariadb-client \
apt-transport-https \
lsb-release \
ca-certificates \
curl \
zip \
mariadb-client \
postgresql-client \
&& curl -sSLo /usr/share/keyrings/deb.sury.org-php.gpg https://packages.sury.org/php/apt.gpg \ && curl -sSLo /usr/share/keyrings/deb.sury.org-php.gpg https://packages.sury.org/php/apt.gpg \
&& sh -c 'echo "deb [signed-by=/usr/share/keyrings/deb.sury.org-php.gpg] https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.list' \ && sh -c 'echo "deb [signed-by=/usr/share/keyrings/deb.sury.org-php.gpg] https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.list' \
&& apt-get update && apt-get upgrade -y \ && apt-get update && apt-get upgrade -y \
&& apt-get install -y \ && apt-get install -y apache2 php8.1 php8.1-fpm php8.1-opcache php8.1-curl php8.1-gd php8.1-mbstring php8.1-xml php8.1-bcmath php8.1-intl php8.1-zip php8.1-xsl php8.1-sqlite3 php8.1-mysql gpg sudo \
apache2 \ && apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*;
php${PHP_VERSION} \
php${PHP_VERSION}-fpm \ ENV APACHE_CONFDIR /etc/apache2
php${PHP_VERSION}-opcache \ ENV APACHE_ENVVARS $APACHE_CONFDIR/envvars
php${PHP_VERSION}-curl \
php${PHP_VERSION}-gd \
php${PHP_VERSION}-mbstring \
php${PHP_VERSION}-xml \
php${PHP_VERSION}-bcmath \
php${PHP_VERSION}-intl \
php${PHP_VERSION}-zip \
php${PHP_VERSION}-xsl \
php${PHP_VERSION}-sqlite3 \
php${PHP_VERSION}-mysql \
php${PHP_VERSION}-pgsql \
gpg \
sudo \
&& apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/* \
# Create workdir and set permissions if directory does not exists # Create workdir and set permissions if directory does not exists
&& mkdir -p /var/www/html \ RUN mkdir -p /var/www/html && chown -R www-data:www-data /var/www/html
&& chown -R www-data:www-data /var/www/html \
# delete the "index.html" that installing Apache drops in here
&& rm -rvf /var/www/html/*
# Install node and yarn
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
curl -sL https://deb.nodesource.com/setup_20.x | bash - && \
apt-get update && apt-get install -y \
nodejs \
yarn \
&& apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*
# Install composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
ENV APACHE_CONFDIR=/etc/apache2
ENV APACHE_ENVVARS=$APACHE_CONFDIR/envvars
# Configure apache 2 (taken from https://github.com/docker-library/php/blob/master/8.2/bullseye/apache/Dockerfile) # Configure apache 2 (taken from https://github.com/docker-library/php/blob/master/8.2/bullseye/apache/Dockerfile)
# generically convert lines like # generically convert lines like
@ -69,6 +27,8 @@ ENV APACHE_ENVVARS=$APACHE_CONFDIR/envvars
# so that they can be overridden at runtime ("-e APACHE_RUN_USER=...") # so that they can be overridden at runtime ("-e APACHE_RUN_USER=...")
RUN sed -ri 's/^export ([^=]+)=(.*)$/: ${\1:=\2}\nexport \1/' "$APACHE_ENVVARS"; \ RUN sed -ri 's/^export ([^=]+)=(.*)$/: ${\1:=\2}\nexport \1/' "$APACHE_ENVVARS"; \
set -eux; . "$APACHE_ENVVARS"; \ set -eux; . "$APACHE_ENVVARS"; \
# delete the "index.html" that installing Apache drops in here
rm -rvf /var/www/html/*; \
\ \
# logs should go to stdout / stderr # logs should go to stdout / stderr
ln -sfT /dev/stderr "$APACHE_LOG_DIR/error.log"; \ ln -sfT /dev/stderr "$APACHE_LOG_DIR/error.log"; \
@ -76,87 +36,82 @@ RUN sed -ri 's/^export ([^=]+)=(.*)$/: ${\1:=\2}\nexport \1/' "$APACHE_ENVVARS"
ln -sfT /dev/stdout "$APACHE_LOG_DIR/other_vhosts_access.log"; \ ln -sfT /dev/stdout "$APACHE_LOG_DIR/other_vhosts_access.log"; \
chown -R --no-dereference "$APACHE_RUN_USER:$APACHE_RUN_GROUP" "$APACHE_LOG_DIR"; chown -R --no-dereference "$APACHE_RUN_USER:$APACHE_RUN_GROUP" "$APACHE_LOG_DIR";
# --- # Enable php-fpm
RUN a2enmod proxy_fcgi setenvif && a2enconf php8.1-fpm
FROM scratch AS apache-config
ARG PHP_VERSION
# Configure php-fpm to log to stdout of the container (stdout of PID 1) # Configure php-fpm to log to stdout of the container (stdout of PID 1)
# We have to use /proc/1/fd/1 because /dev/stdout or /proc/self/fd/1 does not point to the container stdout (because we use apache as entrypoint) # We have to use /proc/1/fd/1 because /dev/stdout or /proc/self/fd/1 does not point to the container stdout (because we use apache as entrypoint)
# We also disable the clear_env option to allow the use of environment variables in php-fpm # We also disable the clear_env option to allow the use of environment variables in php-fpm
COPY <<EOF /etc/php/${PHP_VERSION}/fpm/pool.d/zz-docker.conf RUN { \
[global] echo '[global]'; \
error_log = /proc/1/fd/1 echo 'error_log = /proc/1/fd/1'; \
echo; \
[www] echo '[www]'; \
access.log = /proc/1/fd/1 echo 'access.log = /proc/1/fd/1'; \
catch_workers_output = yes echo 'catch_workers_output = yes'; \
decorate_workers_output = no echo 'decorate_workers_output = no'; \
clear_env = no echo 'clear_env = no'; \
EOF } | tee "/etc/php/8.1/fpm/pool.d/zz-docker.conf"
# PHP files should be handled by PHP, and should be preferred over any other file type # PHP files should be handled by PHP, and should be preferred over any other file type
COPY <<EOF /etc/apache2/conf-available/docker-php.conf RUN { \
<FilesMatch \\.php$> echo '<FilesMatch \.php$>'; \
SetHandler application/x-httpd-php echo '\tSetHandler application/x-httpd-php'; \
</FilesMatch> echo '</FilesMatch>'; \
echo; \
DirectoryIndex disabled echo 'DirectoryIndex disabled'; \
DirectoryIndex index.php index.html echo 'DirectoryIndex index.php index.html'; \
echo; \
<Directory /var/www/> echo '<Directory /var/www/>'; \
Options -Indexes echo '\tOptions -Indexes'; \
AllowOverride All echo '\tAllowOverride All'; \
</Directory> echo '</Directory>'; \
EOF } | tee "$APACHE_CONFDIR/conf-available/docker-php.conf" \
&& a2enconf docker-php
# Enable opcache and configure it recommended for symfony (see https://symfony.com/doc/current/performance.html) # Enable opcache and configure it recommended for symfony (see https://symfony.com/doc/current/performance.html)
COPY <<EOF /etc/php/${PHP_VERSION}/fpm/conf.d/symfony-recommended.ini RUN \
opcache.memory_consumption=256 { \
opcache.max_accelerated_files=20000 echo 'opcache.memory_consumption=256'; \
opcache.validate_timestamp=0 echo 'opcache.max_accelerated_files=20000'; \
# Configure Realpath cache for performance echo 'opcache.validate_timestamp=0'; \
realpath_cache_size=4096K # Configure Realpath cache for performance
realpath_cache_ttl=600 echo 'realpath_cache_size=4096K'; \
EOF echo 'realpath_cache_ttl=600'; \
} > /etc/php/8.1/fpm/conf.d/symfony-recommended.ini
# Increase upload limit and enable preloading # Increase upload limit and enable preloading
COPY <<EOF /etc/php/${PHP_VERSION}/fpm/conf.d/partdb.ini RUN \
upload_max_filesize=256M { \
post_max_size=300M echo 'upload_max_filesize=256M'; \
opcache.preload_user=www-data echo 'post_max_size=300M'; \
opcache.preload=/var/www/html/config/preload.php echo 'opcache.preload_user=www-data'; \
log_limit=8096 echo 'opcache.preload=/var/www/html/config/preload.php'; \
EOF } > /etc/php/8.1/fpm/conf.d/partdb.ini
COPY ./.docker/symfony.conf /etc/apache2/sites-available/symfony.conf # Install node and yarn
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
RUN curl -sL https://deb.nodesource.com/setup_18.x | bash - && apt-get update && apt-get install -y nodejs yarn && apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*
# --- # Install composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
FROM base
ARG PHP_VERSION
# Set working dir # Set working dir
WORKDIR /var/www/html WORKDIR /var/www/html
COPY --from=apache-config / /
COPY --chown=www-data:www-data . . COPY --chown=www-data:www-data . .
# Setup apache2 # Setup apache2
RUN a2dissite 000-default.conf && \ RUN a2dissite 000-default.conf
a2ensite symfony.conf && \ COPY ./.docker/symfony.conf /etc/apache2/sites-available/symfony.conf
# Enable php-fpm RUN a2ensite symfony.conf
a2enmod proxy_fcgi setenvif && \ RUN a2enmod rewrite
a2enconf php${PHP_VERSION}-fpm && \
a2enconf docker-php && \
a2enmod rewrite
# Install composer and yarn dependencies for Part-DB # Install composer and yarn dependencies for Part-DB
USER www-data USER www-data
RUN composer install -a --no-dev && \ RUN composer install -a --no-dev && composer clear-cache
composer clear-cache RUN yarn install --network-timeout 600000 && yarn build && yarn cache clean && rm -rf node_modules/
RUN yarn install --network-timeout 600000 && \
yarn build && \
yarn cache clean && \
rm -rf node_modules/
# Use docker env to output logs to stdout # Use docker env to output logs to stdout
ENV APP_ENV=docker ENV APP_ENV=docker
@ -164,12 +119,10 @@ ENV DATABASE_URL="sqlite:///%kernel.project_dir%/uploads/app.db"
USER root USER root
# Replace the php version placeholder in the entry point, with our php version # Copy entrypoint to /usr/local/bin and make it executable
RUN sed -i "s/PHP_VERSION/${PHP_VERSION}/g" ./.docker/partdb-entrypoint.sh RUN cp ./.docker/partdb-entrypoint.sh /usr/local/bin/partdb-entrypoint.sh && chmod +x /usr/local/bin/partdb-entrypoint.sh
# Copy apache2-foreground to /usr/local/bin and make it executable
# Copy entrypoint and apache2-foreground to /usr/local/bin and make it executable RUN cp ./.docker/apache2-foreground /usr/local/bin/apache2-foreground && chmod +x /usr/local/bin/apache2-foreground
RUN install ./.docker/partdb-entrypoint.sh /usr/local/bin && \
install ./.docker/apache2-foreground /usr/local/bin
ENTRYPOINT ["partdb-entrypoint.sh"] ENTRYPOINT ["partdb-entrypoint.sh"]
CMD ["apache2-foreground"] CMD ["apache2-foreground"]
@ -177,4 +130,4 @@ CMD ["apache2-foreground"]
STOPSIGNAL SIGWINCH STOPSIGNAL SIGWINCH
EXPOSE 80 EXPOSE 80
VOLUME ["/var/www/html/uploads", "/var/www/html/public/media"] VOLUME ["/var/www/html/uploads", "/var/www/html/public/media"]

View file

@ -1,25 +1,11 @@
FROM dunglas/frankenphp:1-php8.3 AS frankenphp_upstream FROM dunglas/frankenphp:1-php8.3 AS frankenphp_upstream
RUN apt-get update && apt-get -y install \ RUN apt-get update && apt-get -y install curl zip mariadb-client file acl git gettext ca-certificates gnupg \
curl \
ca-certificates \
mariadb-client \
postgresql-client \
file \
acl \
git \
gettext \
gnupg \
zip \
&& apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*; && apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*;
# Install node and yarn # Create workdir and set permissions if directory does not exists
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ RUN mkdir -p /app
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \ WORKDIR /app
curl -sL https://deb.nodesource.com/setup_20.x | bash - && \
apt-get update && apt-get install -y \
nodejs yarn \
&& apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*
# Install PHP # Install PHP
RUN set -eux; \ RUN set -eux; \
@ -31,7 +17,6 @@ RUN set -eux; \
zip \ zip \
pdo_mysql \ pdo_mysql \
pdo_sqlite \ pdo_sqlite \
pdo_pgsql \
gd \ gd \
bcmath \ bcmath \
xsl \ xsl \
@ -47,13 +32,15 @@ ENV FRANKENPHP_CONFIG="import worker.Caddyfile"
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
# Install node and yarn
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
RUN curl -sL https://deb.nodesource.com/setup_20.x | bash - && apt-get update && apt-get install -y nodejs yarn && apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*
# Install composer # Install composer
ENV COMPOSER_ALLOW_SUPERUSER=1 ENV COMPOSER_ALLOW_SUPERUSER=1
#COPY --from=composer:latest /usr/bin/composer /usr/bin/composer #COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Create workdir and set permissions if directory does not exists
WORKDIR /app
# prevent the reinstallation of vendors at every changes in the source code # prevent the reinstallation of vendors at every changes in the source code
COPY --link composer.* symfony.* ./ COPY --link composer.* symfony.* ./
RUN set -eux; \ RUN set -eux; \
@ -70,10 +57,7 @@ RUN set -eux; \
composer run-script --no-dev post-install-cmd; \ composer run-script --no-dev post-install-cmd; \
chmod +x bin/console; sync; chmod +x bin/console; sync;
RUN yarn install --network-timeout 600000 && \ RUN yarn install --network-timeout 600000 && yarn build && yarn cache clean && rm -rf node_modules/
yarn build && \
yarn cache clean && \
rm -rf node_modules/
# Use docker env to output logs to stdout # Use docker env to output logs to stdout
ENV APP_ENV=docker ENV APP_ENV=docker
@ -98,4 +82,4 @@ ENV XDG_DATA_HOME /data
EXPOSE 80 EXPOSE 80
EXPOSE 443 EXPOSE 443
EXPOSE 443/udp EXPOSE 443/udp
EXPOSE 2019 EXPOSE 2019

View file

@ -55,7 +55,7 @@ for the first time.
* Event log: Track what changes happen to your inventory, track which user does what. Revert your parts to older * Event log: Track what changes happen to your inventory, track which user does what. Revert your parts to older
versions. versions.
* Responsive design: You can use Part-DB on your PC, your tablet, and your smartphone using the same interface. * Responsive design: You can use Part-DB on your PC, your tablet, and your smartphone using the same interface.
* MySQL, SQLite and PostgreSQL are supported as database backends * MySQL and SQLite are supported as database backends
* Support for rich text descriptions and comments in parts * Support for rich text descriptions and comments in parts
* Support for multiple currencies and automatic update of exchange rates supported * Support for multiple currencies and automatic update of exchange rates supported
* Powerful search and filter function, including parametric search (search for parts according to some specifications) * Powerful search and filter function, including parametric search (search for parts according to some specifications)
@ -74,10 +74,10 @@ Part-DB is also used by small companies and universities for managing their inve
## Requirements ## Requirements
* A **web server** (like Apache2 or nginx) that is capable of * A **web server** (like Apache2 or nginx) that is capable of
running [Symfony 6](https://symfony.com/doc/current/reference/requirements.html), running [Symfony 5](https://symfony.com/doc/current/reference/requirements.html),
this includes a minimum PHP version of **PHP 8.1** this includes a minimum PHP version of **PHP 8.1**
* A **MySQL** (at least 5.7) /**MariaDB** (at least 10.4) database server, or **PostgreSQL** 10+ if you do not want to use SQLite. * A **MySQL** (at least 5.7) /**MariaDB** (at least 10.2.2) database server if you do not want to use SQLite.
* Shell access to your server is highly recommended! * Shell access to your server is highly suggested!
* For building the client-side assets **yarn** and **nodejs** (>= 18.0) is needed. * For building the client-side assets **yarn** and **nodejs** (>= 18.0) is needed.
## Installation ## Installation

View file

@ -1 +1 @@
1.16.1 1.12.1

View file

@ -88,8 +88,5 @@ export default class extends Controller {
} else { } else {
this.hideSidebar(); this.hideSidebar();
} }
//Hide the tootip on the button
this._toggle_button.blur();
} }
} }

View file

@ -23,12 +23,6 @@ import "tom-select/dist/css/tom-select.bootstrap5.css";
import '../../css/components/tom-select_extensions.css'; import '../../css/components/tom-select_extensions.css';
import TomSelect from "tom-select"; import TomSelect from "tom-select";
import TomSelect_click_to_edit from '../../tomselect/click_to_edit/click_to_edit'
import TomSelect_autoselect_typed from '../../tomselect/autoselect_typed/autoselect_typed'
TomSelect.define('click_to_edit', TomSelect_click_to_edit)
TomSelect.define('autoselect_typed', TomSelect_autoselect_typed)
export default class extends Controller { export default class extends Controller {
_tomSelect; _tomSelect;
@ -52,12 +46,6 @@ export default class extends Controller {
} }
return '<div>' + escape(data.label) + '</div>'; return '<div>' + escape(data.label) + '</div>';
} }
},
plugins: {
'autoselect_typed': {},
'click_to_edit': {},
'clear_button': {},
"restore_on_backspace": {}
} }
}; };

View file

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

View file

@ -186,15 +186,5 @@ export default class extends Controller {
]; ];
}, },
}); });
//Try to find the input field and register a defocus handler. This is necessarry, as by default the autocomplete
//lib has problems when multiple inputs are present on the page. (see https://github.com/algolia/autocomplete/issues/1216)
const inputs = this.element.getElementsByClassName('aa-Input');
for (const input of inputs) {
input.addEventListener('blur', () => {
this._autocomplete.setIsOpen(false);
});
}
} }
} }

View file

@ -23,12 +23,6 @@ import "tom-select/dist/css/tom-select.bootstrap5.css";
import '../../css/components/tom-select_extensions.css'; import '../../css/components/tom-select_extensions.css';
import TomSelect from "tom-select"; import TomSelect from "tom-select";
import TomSelect_click_to_edit from '../../tomselect/click_to_edit/click_to_edit'
import TomSelect_autoselect_typed from '../../tomselect/autoselect_typed/autoselect_typed'
TomSelect.define('click_to_edit', TomSelect_click_to_edit)
TomSelect.define('autoselect_typed', TomSelect_autoselect_typed)
/** /**
* This is the frontend controller for StaticFileAutocompleteType form element. * This is the frontend controller for StaticFileAutocompleteType form element.
* Basically it loads a text file from the given url (via data-url) and uses it as a source for the autocomplete. * Basically it loads a text file from the given url (via data-url) and uses it as a source for the autocomplete.
@ -52,13 +46,7 @@ export default class extends Controller {
orderField: 'text', orderField: 'text',
//This a an ugly solution to disable the delimiter parsing of the TomSelect plugin //This a an ugly solution to disable the delimiter parsing of the TomSelect plugin
delimiter: 'VERY_L0NG_D€LIMITER_WHICH_WILL_NEVER_BE_ENCOUNTERED_IN_A_STRING', delimiter: 'VERY_L0NG_D€LIMITER_WHICH_WILL_NEVER_BE_ENCOUNTERED_IN_A_STRING'
plugins: {
'autoselect_typed': {},
'click_to_edit': {},
'clear_button': {},
'restore_on_backspace': {}
}
}; };
if (this.element.dataset.url) { if (this.element.dataset.url) {

View file

@ -24,8 +24,6 @@ import {Controller} from "@hotwired/stimulus";
import {trans, ENTITY_SELECT_GROUP_NEW_NOT_ADDED_TO_DB} from '../../translator.js' import {trans, ENTITY_SELECT_GROUP_NEW_NOT_ADDED_TO_DB} from '../../translator.js'
import TomSelect_autoselect_typed from '../../tomselect/autoselect_typed/autoselect_typed'
TomSelect.define('autoselect_typed', TomSelect_autoselect_typed)
export default class extends Controller { export default class extends Controller {
_tomSelect; _tomSelect;
@ -40,15 +38,11 @@ export default class extends Controller {
const allowAdd = this.element.getAttribute("data-allow-add") === "true"; const allowAdd = this.element.getAttribute("data-allow-add") === "true";
const addHint = this.element.getAttribute("data-add-hint") ?? ""; const addHint = this.element.getAttribute("data-add-hint") ?? "";
let settings = { let settings = {
allowEmptyOption: true, allowEmptyOption: true,
selectOnTab: true, selectOnTab: true,
maxOptions: null, maxOptions: null,
create: allowAdd ? this.createItem.bind(this) : false, create: allowAdd ? this.createItem.bind(this) : false,
createFilter: this.createFilter.bind(this),
// This three options allow us to paste element names with commas: (see issue #538) // This three options allow us to paste element names with commas: (see issue #538)
maxItems: 1, maxItems: 1,
@ -64,21 +58,7 @@ export default class extends Controller {
render: { render: {
item: this.renderItem.bind(this), item: this.renderItem.bind(this),
option: this.renderOption.bind(this), option: this.renderOption.bind(this),
option_create: (data, escape) => { option_create: function(data, escape) {
//If the input starts with "->", we prepend the current selected value, for easier extension of existing values
//This here handles the display part, while the createItem function handles the actual creation
if (data.input.startsWith("->")) {
//Get current selected value
const current = this._tomSelect.getItem(this._tomSelect.getValue()).textContent.replaceAll("→", "->").trim();
//Prepend it to the input
if (current) {
data.input = current + " " + data.input;
} else {
//If there is no current value, we remove the "->"
data.input = data.input.substring(2);
}
}
return '<div class="create"><i class="fa-solid fa-plus fa-fw"></i>&nbsp;<strong>' + escape(data.input) + '</strong>&hellip;&nbsp;' + return '<div class="create"><i class="fa-solid fa-plus fa-fw"></i>&nbsp;<strong>' + escape(data.input) + '</strong>&hellip;&nbsp;' +
'<small class="text-muted float-end">(' + addHint +')</small>' + '<small class="text-muted float-end">(' + addHint +')</small>' +
'</div>'; '</div>';
@ -88,39 +68,14 @@ export default class extends Controller {
//Add callbacks to update validity //Add callbacks to update validity
onInitialize: this.updateValidity.bind(this), onInitialize: this.updateValidity.bind(this),
onChange: this.updateValidity.bind(this), onChange: this.updateValidity.bind(this),
plugins: {
"autoselect_typed": {},
}
}; };
//Add clear button plugin, if an empty option is present
if (this.element.querySelector("option[value='']") !== null) {
settings.plugins["clear_button"] = {};
}
this._tomSelect = new TomSelect(this.element, settings); this._tomSelect = new TomSelect(this.element, settings);
//Do not do a sync here as this breaks the initial rendering of the empty option //Do not do a sync here as this breaks the initial rendering of the empty option
//this._tomSelect.sync(); //this._tomSelect.sync();
} }
createItem(input, callback) { createItem(input, callback) {
//If the input starts with "->", we prepend the current selected value, for easier extension of existing values
if (input.startsWith("->")) {
//Get current selected value
let current = this._tomSelect.getItem(this._tomSelect.getValue()).textContent.replaceAll("→", "->").trim();
//Replace no break spaces with normal spaces
current = current.replaceAll("\u00A0", " ");
//Prepend it to the input
if (current) {
input = current + " " + input;
} else {
//If there is no current value, we remove the "->"
input = input.substring(2);
}
}
callback({ callback({
//$%$ is a special value prefix, that is used to identify items, that are not yet in the DB //$%$ is a special value prefix, that is used to identify items, that are not yet in the DB
value: '$%$' + input, value: '$%$' + input,
@ -129,31 +84,6 @@ export default class extends Controller {
}); });
} }
createFilter(input) {
//Normalize the input (replace spacing around arrows)
if (input.includes("->")) {
const inputs = input.split("->");
inputs.forEach((value, index) => {
inputs[index] = value.trim();
});
input = inputs.join("->");
} else {
input = input.trim();
}
const options = this._tomSelect.options;
//Iterate over all options and check if the input is already present
for (let index in options) {
const option = options[index];
if (option.path === input) {
return false;
}
}
return true;
}
updateValidity() { updateValidity() {
//Mark this input as invalid, if the selected option is disabled //Mark this input as invalid, if the selected option is disabled

View file

@ -23,21 +23,14 @@ import "tom-select/dist/css/tom-select.bootstrap5.css";
import '../../css/components/tom-select_extensions.css'; import '../../css/components/tom-select_extensions.css';
import TomSelect from "tom-select"; import TomSelect from "tom-select";
import TomSelect_click_to_edit from '../../tomselect/click_to_edit/click_to_edit'
import TomSelect_autoselect_typed from '../../tomselect/autoselect_typed/autoselect_typed'
TomSelect.define('click_to_edit', TomSelect_click_to_edit)
TomSelect.define('autoselect_typed', TomSelect_autoselect_typed)
export default class extends Controller { export default class extends Controller {
_tomSelect; _tomSelect;
connect() { connect() {
let settings = { let settings = {
plugins: { plugins: {
remove_button:{}, remove_button:{
'autoselect_typed': {}, }
'click_to_edit': {},
}, },
persistent: false, persistent: false,
selectOnTab: true, selectOnTab: true,

View file

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

View file

@ -22,13 +22,6 @@ import TomSelect from "tom-select";
import katex from "katex"; import katex from "katex";
import "katex/dist/katex.css"; import "katex/dist/katex.css";
import TomSelect_click_to_edit from '../../tomselect/click_to_edit/click_to_edit'
import TomSelect_autoselect_typed from '../../tomselect/autoselect_typed/autoselect_typed'
TomSelect.define('click_to_edit', TomSelect_click_to_edit)
TomSelect.define('autoselect_typed', TomSelect_autoselect_typed)
/* stimulusFetch: 'lazy' */ /* stimulusFetch: 'lazy' */
export default class extends Controller export default class extends Controller
{ {
@ -60,10 +53,7 @@ export default class extends Controller
connect() { connect() {
const settings = { const settings = {
plugins: { plugins: {
'autoselect_typed': {}, clear_button:{}
'click_to_edit': {},
'clear_button': {},
'restore_on_backspace': {}
}, },
persistent: false, persistent: false,
maxItems: 1, maxItems: 1,

View file

@ -51,6 +51,7 @@
.part-table-image { .part-table-image {
max-height: 40px; max-height: 40px;
object-fit: contain; object-fit: contain;
width: 100%;
} }
.part-info-image { .part-info-image {
@ -60,4 +61,4 @@
.object-fit-cover { .object-fit-cover {
object-fit: cover; object-fit: cover;
} }

View file

@ -108,8 +108,8 @@ body {
.back-to-top { .back-to-top {
cursor: pointer; cursor: pointer;
position: fixed; position: fixed;
bottom: 60px; bottom: 20px;
right: 40px; right: 20px;
display:none; display:none;
z-index: 1030; z-index: 1030;
} }

View file

@ -63,6 +63,10 @@ table.dataTable > tbody > tr.selected > td > a {
margin-block-end: 0; margin-block-end: 0;
} }
.card-footer-table {
padding-top: 0;
}
table.dataTable { table.dataTable {
margin-top: 0 !important; margin-top: 0 !important;
} }

View file

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

View file

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

View file

@ -21,7 +21,6 @@
import {Dropdown} from "bootstrap"; import {Dropdown} from "bootstrap";
import ClipboardJS from "clipboard"; import ClipboardJS from "clipboard";
import {Modal} from "bootstrap";
class RegisterEventHelper { class RegisterEventHelper {
constructor() { constructor() {
@ -32,11 +31,9 @@ class RegisterEventHelper {
//Initialize ClipboardJS //Initialize ClipboardJS
this.registerLoadHandler(() => { this.registerLoadHandler(() => {
new ClipboardJS('.btn'); new ClipboardJS('.btn');
}); })
this.registerModalDropRemovalOnFormSubmit(); this.registerModalDropRemovalOnFormSubmit();
} }
registerModalDropRemovalOnFormSubmit() { registerModalDropRemovalOnFormSubmit() {
@ -46,15 +43,6 @@ class RegisterEventHelper {
if (back_drop) { if (back_drop) {
back_drop.remove(); back_drop.remove();
} }
//Remove scroll-lock if it is still active
if (document.body.classList.contains('modal-open')) {
document.body.classList.remove('modal-open');
//Remove the padding-right and overflow:hidden from the body
document.body.style.paddingRight = '';
document.body.style.overflow = '';
}
}); });
} }

View file

@ -1,63 +0,0 @@
/**
* Autoselect Typed plugin for Tomselect
*
* This plugin allows automatically selecting an option matching the typed text when the Tomselect element goes out of
* focus (is blurred) and/or when the delimiter is typed.
*
* #select_on_blur option
* Tomselect natively supports the "createOnBlur" option. This option picks up any remaining text in the input field
* and uses it to create a new option and selects that option. It does behave a bit strangely though, in that it will
* not select an already existing option when the input is blurred, so if you typed something that matches an option in
* the list and then click outside the box (without pressing enter) the entered text is just removed (unless you have
* allow duplicates on in which case it will create a new option).
* This plugin fixes that, such that Tomselect will first try to select an option matching the remaining uncommitted
* text and only when no matching option is found tries to create a new one (if createOnBlur and create is on)
*
* #select_on_delimiter option
* Normally when typing the delimiter (space by default) Tomselect will try to create a new option (and select it) (if
* create is on), but if the typed text matches an option (and allow duplicates is off) it refuses to react at all until
* you press enter. With this option, the delimiter will also allow selecting an option, not just creating it.
*/
function select_current_input(self){
if(self.isLocked){
return
}
const val = self.inputValue()
//Do nothing if the input is empty
if (!val) {
return
}
if (self.options[val]) {
self.addItem(val)
self.setTextboxValue()
}
}
export default function(plugin_options_) {
const plugin_options = Object.assign({
//Autoselect the typed text when the input element goes out of focus
select_on_blur: true,
//Autoselect the typed text when the delimiter is typed
select_on_delimiter: true,
}, plugin_options_);
const self = this
if(plugin_options.select_on_blur) {
this.hook("before", "onBlur", function () {
select_current_input(self)
})
}
if(plugin_options.select_on_delimiter) {
this.hook("before", "onKeyPress", function (e) {
const character = String.fromCharCode(e.keyCode || e.which);
if (self.settings.mode === 'multi' && character === self.settings.delimiter) {
select_current_input(self)
}
})
}
}

View file

@ -1,93 +0,0 @@
/**
* click_to_edit plugin for Tomselect
*
* This plugin allows editing (and selecting text in) any selected item by clicking it.
*
* Usually, when the user typed some text and created an item in Tomselect that item cannot be edited anymore. To make
* a change, the item has to be deleted and retyped completely. There is also generally no way to copy text out of a
* tomselect item. The "restore_on_backspace" plugin improves that somewhat, by allowing the user to edit an item after
* pressing backspace. However, it is somewhat confusing to first have to focus the field an then hit backspace in order
* to copy a piece of text. It may also not be immediately obvious for editing.
* This plugin transforms an item into editable text when it is clicked, e.g. when the user tries to place the caret
* within an item or when they try to drag across the text to highlight it.
* It also plays nice with the remove_button plugin which still removes (deselects) an option entirely.
*
* It is recommended to also enable the autoselect_typed plugin when using this plugin. Without it, the text in the
* input field (i.e. the item that was just clicked) is lost when the user clicks outside the field. Also, when the user
* clicks an option (making it text) and then tries to enter another one by entering the delimiter (e.g. space) nothing
* happens until enter is pressed or the text is changed from what it was.
*/
/**
* Return a dom element from either a dom query string, jQuery object, a dom element or html string
* https://stackoverflow.com/questions/494143/creating-a-new-dom-element-from-an-html-string-using-built-in-dom-methods-or-pro/35385518#35385518
*
* param query should be {}
*/
const getDom = query => {
if (query.jquery) {
return query[0];
}
if (query instanceof HTMLElement) {
return query;
}
if (isHtmlString(query)) {
var tpl = document.createElement('template');
tpl.innerHTML = query.trim(); // Never return a text node of whitespace as the result
return tpl.content.firstChild;
}
return document.querySelector(query);
};
const isHtmlString = arg => {
if (typeof arg === 'string' && arg.indexOf('<') > -1) {
return true;
}
return false;
};
function plugin(plugin_options_) {
const self = this
const plugin_options = Object.assign({
//If there is unsubmitted text in the input field, should that text be automatically used to select a matching
//element? If this is off, clicking on item1 and then clicking on item2 will result in item1 being deselected
auto_select_before_edit: true,
//If there is unsubmitted text in the input field, should that text be automatically used to create a matching
//element if no matching element was found or auto_select_before_edit is off?
auto_create_before_edit: true,
//customize this function to change which text the item is replaced with when clicking on it
text: option => {
return option[self.settings.labelField];
}
}, plugin_options_);
self.hook('after', 'setupTemplates', () => {
const orig_render_item = self.settings.render.item;
self.settings.render.item = (data, escape) => {
const item = getDom(orig_render_item.call(self, data, escape));
item.addEventListener('click', evt => {
if (self.isLocked) {
return;
}
const val = self.inputValue();
if (self.options[val]) {
self.addItem(val)
} else if (self.settings.create) {
self.createItem();
}
const option = self.options[item.dataset.value]
self.setTextboxValue(plugin_options.text.call(self, option));
self.focus();
self.removeItem(item);
}
);
return item;
}
});
}
export { plugin as default };

View file

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

View file

@ -1,5 +1,4 @@
{ {
"name": "part-db/part-db-server",
"type": "project", "type": "project",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"require": { "require": {
@ -11,13 +10,12 @@
"ext-intl": "*", "ext-intl": "*",
"ext-json": "*", "ext-json": "*",
"ext-mbstring": "*", "ext-mbstring": "*",
"amphp/http-client": "^5.1",
"api-platform/core": "^3.1", "api-platform/core": "^3.1",
"beberlei/doctrineextensions": "^1.2", "beberlei/doctrineextensions": "^1.2",
"brick/math": "0.12.1 as 0.11.0", "brick/math": "0.12.1 as 0.11.0",
"composer/ca-bundle": "^1.3", "composer/ca-bundle": "^1.3",
"composer/package-versions-deprecated": "^1.11.99.5", "composer/package-versions-deprecated": "^1.11.99.5",
"doctrine/data-fixtures": "^2.0.0", "doctrine/data-fixtures": "^1.6.6",
"doctrine/dbal": "^4.0.0", "doctrine/dbal": "^4.0.0",
"doctrine/doctrine-bundle": "^2.0", "doctrine/doctrine-bundle": "^2.0",
"doctrine/doctrine-migrations-bundle": "^3.0", "doctrine/doctrine-migrations-bundle": "^3.0",
@ -40,9 +38,12 @@
"nelmio/cors-bundle": "^2.3", "nelmio/cors-bundle": "^2.3",
"nelmio/security-bundle": "^3.0", "nelmio/security-bundle": "^3.0",
"nyholm/psr7": "^1.1", "nyholm/psr7": "^1.1",
"omines/datatables-bundle": "^0.9.1", "ocramius/proxy-manager": "2.2.*",
"paragonie/sodium_compat": "^1.21", "omines/datatables-bundle": "^0.8.0",
"part-db/label-fonts": "^1.0", "part-db/label-fonts": "^1.0",
"php-translation/symfony-bundle": "^0.14.0",
"phpdocumentor/reflection-docblock": "^5.2",
"phpstan/phpdoc-parser": "^1.23",
"runtime/frankenphp-symfony": "^0.2.0", "runtime/frankenphp-symfony": "^0.2.0",
"s9e/text-formatter": "^2.1", "s9e/text-formatter": "^2.1",
"scheb/2fa-backup-code": "^6.8.0", "scheb/2fa-backup-code": "^6.8.0",
@ -54,8 +55,6 @@
"symfony/apache-pack": "^1.0", "symfony/apache-pack": "^1.0",
"symfony/asset": "6.4.*", "symfony/asset": "6.4.*",
"symfony/console": "6.4.*", "symfony/console": "6.4.*",
"symfony/css-selector": "6.4.*",
"symfony/dom-crawler": "6.4.*",
"symfony/dotenv": "6.4.*", "symfony/dotenv": "6.4.*",
"symfony/expression-language": "6.4.*", "symfony/expression-language": "6.4.*",
"symfony/flex": "^v2.3.1", "symfony/flex": "^v2.3.1",
@ -69,6 +68,7 @@
"symfony/process": "6.4.*", "symfony/process": "6.4.*",
"symfony/property-access": "6.4.*", "symfony/property-access": "6.4.*",
"symfony/property-info": "6.4.*", "symfony/property-info": "6.4.*",
"symfony/proxy-manager-bridge": "6.4.*",
"symfony/rate-limiter": "6.4.*", "symfony/rate-limiter": "6.4.*",
"symfony/runtime": "6.4.*", "symfony/runtime": "6.4.*",
"symfony/security-bundle": "6.4.*", "symfony/security-bundle": "6.4.*",
@ -90,28 +90,31 @@
"twig/intl-extra": "^3.8", "twig/intl-extra": "^3.8",
"twig/markdown-extra": "^3.8", "twig/markdown-extra": "^3.8",
"twig/string-extra": "^3.8", "twig/string-extra": "^3.8",
"web-auth/webauthn-symfony-bundle": "^4.0.0" "web-auth/webauthn-symfony-bundle": "^4.0.0",
"webmozart/assert": "^1.4"
}, },
"require-dev": { "require-dev": {
"dama/doctrine-test-bundle": "^v8.0.0", "dama/doctrine-test-bundle": "^v8.0.0",
"doctrine/doctrine-fixtures-bundle": "^4.0.0", "doctrine/doctrine-fixtures-bundle": "^3.2",
"ekino/phpstan-banned-code": "^v3.0.0", "ekino/phpstan-banned-code": "^v1.0.0",
"jbtronics/translation-editor-bundle": "^1.0",
"phpstan/extension-installer": "^1.0", "phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "^2.0.4", "phpstan/phpstan": "^1.4.7",
"phpstan/phpstan-doctrine": "^2.0.1", "phpstan/phpstan-doctrine": "^1.2.11",
"phpstan/phpstan-strict-rules": "^2.0.1", "phpstan/phpstan-strict-rules": "^1.5",
"phpstan/phpstan-symfony": "^2.0.0", "phpstan/phpstan-symfony": "^1.1.7",
"phpunit/phpunit": "^9.5", "phpunit/phpunit": "^9.5",
"rector/rector": "^2.0.4", "psalm/plugin-symfony": "^v5.0.1",
"rector/rector": "^0.18.0",
"roave/security-advisories": "dev-latest", "roave/security-advisories": "dev-latest",
"symfony/browser-kit": "6.4.*", "symfony/browser-kit": "6.4.*",
"symfony/css-selector": "6.4.*",
"symfony/debug-bundle": "6.4.*", "symfony/debug-bundle": "6.4.*",
"symfony/maker-bundle": "^1.13", "symfony/maker-bundle": "^1.13",
"symfony/phpunit-bridge": "6.4.*", "symfony/phpunit-bridge": "6.4.*",
"symfony/stopwatch": "6.4.*", "symfony/stopwatch": "6.4.*",
"symfony/web-profiler-bundle": "6.4.*", "symfony/web-profiler-bundle": "6.4.*",
"symplify/easy-coding-standard": "^12.0" "symplify/easy-coding-standard": "^12.0",
"vimeo/psalm": "^5.6.0"
}, },
"suggest": { "suggest": {
"ext-bcmath": "Used to improve price calculation performance", "ext-bcmath": "Used to improve price calculation performance",

6221
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -18,6 +18,7 @@ return [
DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true], DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
Gregwar\CaptchaBundle\GregwarCaptchaBundle::class => ['all' => true], Gregwar\CaptchaBundle\GregwarCaptchaBundle::class => ['all' => true],
Translation\Bundle\TranslationBundle::class => ['all' => true],
Florianv\SwapBundle\FlorianvSwapBundle::class => ['all' => true], Florianv\SwapBundle\FlorianvSwapBundle::class => ['all' => true],
Nelmio\SecurityBundle\NelmioSecurityBundle::class => ['all' => true], Nelmio\SecurityBundle\NelmioSecurityBundle::class => ['all' => true],
Symfony\UX\Turbo\TurboBundle::class => ['all' => true], Symfony\UX\Turbo\TurboBundle::class => ['all' => true],
@ -31,5 +32,4 @@ return [
KnpU\OAuth2ClientBundle\KnpUOAuth2ClientBundle::class => ['all' => true], KnpU\OAuth2ClientBundle\KnpUOAuth2ClientBundle::class => ['all' => true],
Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true], Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true], ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
Jbtronics\TranslationEditorBundle\JbtronicsTranslationEditorBundle::class => ['dev' => true],
]; ];

View file

@ -33,9 +33,4 @@ api_platform:
pagination_client_items_per_page: true # Allow clients to override the default items per page pagination_client_items_per_page: true # Allow clients to override the default items per page
keep_legacy_inflector: false keep_legacy_inflector: false
# Need to be true, or some tests will fail event_listeners_backward_compatibility_layer: false
use_symfony_listeners: true
serializer:
# Change this to false later, to remove the hydra prefix on the API
hydra_prefix: true

View file

@ -8,14 +8,15 @@ datatables:
# Set options, as documented at https://datatables.net/reference/option/ # Set options, as documented at https://datatables.net/reference/option/
options: options:
lengthMenu : [[10, 25, 50, 100], [10, 25, 50, 100]] # We add the "All" option, when part tables are generated lengthMenu : [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]]
pageLength: '%partdb.table.default_page_size%' # Set to -1 to disable pagination (i.e. show all rows) by default pageLength: '%partdb.table.default_page_size%' # Set to -1 to disable pagination (i.e. show all rows) by default
dom: " <'row' <'col mb-2 input-group flex-nowrap' B l > <'col-auto mb-2' < p >>> #dom: "<'row' <'col-sm-12' tr>><'row' <'col-sm-6'l><'col-sm-6 text-right'pif>>"
<'card' dom: " <'row'<'col mb-2 input-group' B l> <'col mb-2' <'pull-end' p>>>
rt <'card'
<'card-footer card-footer-table text-muted' i > rt
> <'card-footer card-footer-table text-muted' i >
<'row' <'col mt-2 input-group flex-nowrap' B l > <'col-auto mt-2' < p >>>" >
<'row'<'col mt-2 input-group' B l> <'col mt-2' <'pull-right' p>>>"
pagingType: 'simple_numbers' pagingType: 'simple_numbers'
searching: true searching: true
stateSave: true stateSave: true

View file

@ -0,0 +1,5 @@
translation:
symfony_profiler:
enabled: true
webui:
enabled: true

View file

@ -9,17 +9,10 @@ doctrine:
# either here or in the DATABASE_URL env var (see .env file) # either here or in the DATABASE_URL env var (see .env file)
types: types:
# UTC datetimes
datetime: datetime:
class: App\Doctrine\Types\UTCDateTimeType class: App\Doctrine\Types\UTCDateTimeType
date: date:
class: App\Doctrine\Types\UTCDateTimeType class: App\Doctrine\Types\UTCDateTimeType
datetime_immutable:
class: App\Doctrine\Types\UTCDateTimeImmutableType
date_immutable:
class: App\Doctrine\Types\UTCDateTimeImmutableType
big_decimal: big_decimal:
class: App\Doctrine\Types\BigDecimalType class: App\Doctrine\Types\BigDecimalType
tinyint: tinyint:
@ -40,8 +33,6 @@ doctrine:
validate_xml_mapping: true validate_xml_mapping: true
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
auto_mapping: true auto_mapping: true
controller_resolver:
auto_mapping: true
mappings: mappings:
App: App:
type: attribute type: attribute
@ -57,7 +48,6 @@ doctrine:
field2: App\Doctrine\Functions\Field2 field2: App\Doctrine\Functions\Field2
natsort: App\Doctrine\Functions\Natsort natsort: App\Doctrine\Functions\Natsort
array_position: App\Doctrine\Functions\ArrayPosition array_position: App\Doctrine\Functions\ArrayPosition
ilike: App\Doctrine\Functions\ILike
when@test: when@test:
doctrine: doctrine:

View file

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

View file

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

View file

@ -0,0 +1,11 @@
translation:
locales: ["en", "de"]
edit_in_place:
enabled: false
config_name: app
configs:
app:
dirs: ["%kernel.project_dir%/templates", "%kernel.project_dir%/src"]
output_dir: "%kernel.project_dir%/translations"
excluded_names: ["*TestCase.php", "*Test.php"]
excluded_dirs: [cache, data, logs]

View file

@ -11,7 +11,7 @@ parameters:
partdb.banner: '%env(trim:string:BANNER)%' # The info text shown in the homepage, if empty config/banner.md is used partdb.banner: '%env(trim:string:BANNER)%' # The info text shown in the homepage, if empty config/banner.md is used
partdb.default_currency: '%env(string:BASE_CURRENCY)%' # The currency that is used inside the DB (and is assumed when no currency is set). This can not be changed later, so be sure to set it the currency used in your country partdb.default_currency: '%env(string:BASE_CURRENCY)%' # The currency that is used inside the DB (and is assumed when no currency is set). This can not be changed later, so be sure to set it the currency used in your country
partdb.global_theme: '' # The theme to use globally (see public/build/themes/ for choices, use name without .css). Set to '' for default bootstrap theme partdb.global_theme: '' # The theme to use globally (see public/build/themes/ for choices, use name without .css). Set to '' for default bootstrap theme
partdb.locale_menu: ['en', 'de', 'it', 'fr', 'ru', 'ja', 'cs', 'da', 'zh', 'pl'] # The languages that are shown in user drop down menu partdb.locale_menu: ['en', 'de', 'it', 'fr', 'ru', 'ja', 'cs', 'da', 'zh'] # The languages that are shown in user drop down menu
partdb.enforce_change_comments_for: '%env(csv:ENFORCE_CHANGE_COMMENTS_FOR)%' # The actions for which a change comment is required (e.g. "part_edit", "part_create", etc.). If this is empty, change comments are not required at all. partdb.enforce_change_comments_for: '%env(csv:ENFORCE_CHANGE_COMMENTS_FOR)%' # The actions for which a change comment is required (e.g. "part_edit", "part_create", etc.). If this is empty, change comments are not required at all.
partdb.default_uri: '%env(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 partdb.default_uri: '%env(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

@ -0,0 +1,6 @@
_translation_webui:
resource: '@TranslationBundle/Resources/config/routing_webui.yaml'
prefix: /admin
_translation_profiler:
resource: '@TranslationBundle/Resources/config/routing_symfony_profiler.yaml'

View file

@ -1,3 +0,0 @@
when@dev:
translation_editor:
resource: '@JbtronicsTranslationEditorBundle/config/routes.php'

View file

@ -0,0 +1,3 @@
_translation_edit_in_place:
resource: '@TranslationBundle/Resources/config/routing_edit_in_place.yaml'
prefix: /admin

View file

@ -76,12 +76,18 @@ services:
# Only the event classes specified here are saved to DB (set to []) to log all events # Only the event classes specified here are saved to DB (set to []) to log all events
$whitelist: [] $whitelist: []
App\EventListener\LogSystem\EventLoggerListener: App\EventSubscriber\LogSystem\EventLoggerSubscriber:
arguments: arguments:
$save_changed_fields: '%env(bool:HISTORY_SAVE_CHANGED_FIELDS)%' $save_changed_fields: '%env(bool:HISTORY_SAVE_CHANGED_FIELDS)%'
$save_changed_data: '%env(bool:HISTORY_SAVE_CHANGED_DATA)%' $save_changed_data: '%env(bool:HISTORY_SAVE_CHANGED_DATA)%'
$save_removed_data: '%env(bool:HISTORY_SAVE_REMOVED_DATA)%' $save_removed_data: '%env(bool:HISTORY_SAVE_REMOVED_DATA)%'
$save_new_data: '%env(bool:HISTORY_SAVE_NEW_DATA)%' $save_new_data: '%env(bool:HISTORY_SAVE_NEW_DATA)%'
tags:
- { name: 'doctrine.event_subscriber' }
App\EventSubscriber\LogSystem\LogDBMigrationSubscriber:
tags:
- { name: 'doctrine.event_subscriber' }
App\Form\AttachmentFormType: App\Form\AttachmentFormType:
arguments: arguments:
@ -306,16 +312,6 @@ services:
$enabled: '%env(bool:PROVIDER_LCSC_ENABLED)%' $enabled: '%env(bool:PROVIDER_LCSC_ENABLED)%'
$currency: '%env(string:PROVIDER_LCSC_CURRENCY)%' $currency: '%env(string:PROVIDER_LCSC_CURRENCY)%'
App\Services\InfoProviderSystem\Providers\OEMSecretsProvider:
arguments:
$api_key: '%env(string:PROVIDER_OEMSECRETS_KEY)%'
$country_code: '%env(string:PROVIDER_OEMSECRETS_COUNTRY_CODE)%'
$currency: '%env(PROVIDER_OEMSECRETS_CURRENCY)%'
$zero_price: '%env(PROVIDER_OEMSECRETS_ZERO_PRICE)%'
$set_param: '%env(PROVIDER_OEMSECRETS_SET_PARAM)%'
$sort_criteria: '%env(PROVIDER_OEMSECRETS_SORT_CRITERIA)%'
#################################################################################################################### ####################################################################################################################
# API system # API system
#################################################################################################################### ####################################################################################################################
@ -410,4 +406,4 @@ when@test:
arguments: arguments:
- '@doctrine.fixtures.loader' - '@doctrine.fixtures.loader'
- '@doctrine' - '@doctrine'
- { default: '@App\Doctrine\Purger\DoNotUsePurgerFactory' } - { default: '@App\Doctrine\Purger\DoNotUsePurgerFactory' }

View file

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

View file

@ -32,22 +32,14 @@ options listed, see `.env` file for the full list of possible env variables.
### General options ### General options
* `DATABASE_URL`: Configures the database which Part-DB uses: * `DATABASE_URL`: Configures the database which Part-DB uses. For mysql use a string in the form
* For MySQL (or MariaDB) use a string in the form of `mysql://<USERNAME>:<PASSWORD>@<HOST>:<PORT>/<TABLE_NAME>` here of `mysql://<USERNAME>:<PASSWORD>@<HOST>:<PORT>/<TABLE_NAME>` here
(e.g. `DATABASE_URL=mysql://user:password@127.0.0.1:3306/part-db`). (e.g. `DATABASE_URL=mysql://user:password@127.0.0.1:3306/part-db`). For SQLite use the following format to specify the
* For SQLite use the following format to specify the
absolute path where it should be located `sqlite:///path/part/app.db`. You can use `%kernel.project_dir%` as absolute path where it should be located `sqlite:///path/part/app.db`. You can use `%kernel.project_dir%` as
placeholder for the Part-DB root folder (e.g. `sqlite:///%kernel.project_dir%/var/app.db`) placeholder for the Part-DB root folder (e.g. `sqlite:///%kernel.project_dir%/var/app.db`)
* For Postgresql use a string in the form of `DATABASE_URL=postgresql://user:password@127.0.0.1:5432/part-db?serverVersion=x.y`.
Please note that **`serverVersion=x.y`** variable is required due to dependency of Symfony framework.
* `DATABASE_MYSQL_USE_SSL_CA`: If this value is set to `1` or `true` and a MySQL connection is used, then the connection * `DATABASE_MYSQL_USE_SSL_CA`: If this value is set to `1` or `true` and a MySQL connection is used, then the connection
is encrypted by SSL/TLS and the server certificate is verified against the system CA certificates or the CA certificate is encrypted by SSL/TLS and the server certificate is verified against the system CA certificates or the CA certificate
bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept all certificates. bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept all certificates.
* `DATABASE_EMULATE_NATURAL_SORT` (default 0): If set to 1, Part-DB will emulate natural sorting, even if the database
does not support it natively. However this is much slower than the native sorting, and contain bugs or quirks, so use
it only, if you have to.
* `DEFAULT_LANG`: The default language to use server-wide (when no language is explicitly specified by a user or via * `DEFAULT_LANG`: The default language to use server-wide (when no language is explicitly specified by a user or via
language chooser). Must be something like `en`, `de`, `fr`, etc. language chooser). Must be something like `en`, `de`, `fr`, etc.
* `DEFAULT_TIMEZONE`: The default timezone to use globally, when a user has no timezone specified. Must be something * `DEFAULT_TIMEZONE`: The default timezone to use globally, when a user has no timezone specified. Must be something
@ -91,10 +83,6 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept
* `datastructure_create`: Creation of a new datastructure (e.g. category, manufacturer, ...) * `datastructure_create`: Creation of a new datastructure (e.g. category, manufacturer, ...)
* `CHECK_FOR_UPDATES` (default `1`): Set this to 0, if you do not want Part-DB to connect to GitHub to check for new * `CHECK_FOR_UPDATES` (default `1`): Set this to 0, if you do not want Part-DB to connect to GitHub to check for new
versions, or if your server can not connect to the internet. versions, or if your server can not connect to the internet.
* `APP_SECRET`: This variable is a configuration parameter used for various security-related purposes,
particularly for securing and protecting various aspects of your application. It's a secret key that is used for
cryptographic operations and security measures (session management, CSRF protection, etc..). Therefore this
value should be handled as confidential data and not shared publicly.
### E-Mail settings ### E-Mail settings
@ -252,4 +240,4 @@ The following options are available:
number of sidebar panels by changing the number of items in this list. number of sidebar panels by changing the number of items in this list.
* `partdb.sidebar.root_node_enable`: Show a root node in the sidebar trees, of which all nodes are children of * `partdb.sidebar.root_node_enable`: Show a root node in the sidebar trees, of which all nodes are children of
* `partdb.sidebar.root_expanded`: Expand the root node in the sidebar trees by default * `partdb.sidebar.root_expanded`: Expand the root node in the sidebar trees by default
* `partdb.available_themes`: The list of available themes a user can choose from. * `partdb.available_themes`: The list of available themes a user can choose from.

View file

@ -41,7 +41,7 @@ It is installed on a web server and so can be accessed with any browser without
* Event log: Track what changes happens to your inventory, track which user does what. Revert your parts to older * Event log: Track what changes happens to your inventory, track which user does what. Revert your parts to older
versions. versions.
* Responsive design: You can use Part-DB on your PC, your tablet and your smartphone using the same interface. * Responsive design: You can use Part-DB on your PC, your tablet and your smartphone using the same interface.
* MySQL, SQLite and PostgreSQL are supported as database backends * MySQL and SQLite supported as database backends
* Support for rich text descriptions and comments in parts * Support for rich text descriptions and comments in parts
* Support for multiple currencies and automatic update of exchange rates supported * Support for multiple currencies and automatic update of exchange rates supported
* Powerful search and filter function, including parametric search (search for parts according to some specifications) * Powerful search and filter function, including parametric search (search for parts according to some specifications)

View file

@ -7,18 +7,10 @@ nav_order: 1
# Choosing database: SQLite or MySQL # Choosing database: SQLite or MySQL
Part-DB saves its data in a [relational (SQL) database](https://en.wikipedia.org/wiki/Relational_database). Part-DB saves its data in a [relational (SQL) database](https://en.wikipedia.org/wiki/Relational_database). Part-DB
supports either the use of [SQLite](https://www.sqlite.org/index.html)
For this multiple database types are supported, currently these are: or [MySQL](https://www.mysql.com/) / [MariaDB](https://mariadb.org/) (which are mostly the same, except for some minor
differences).
* [SQLite](https://www.sqlite.org/index.html)
* [MySQL](https://www.mysql.com/) / [MariaDB](https://mariadb.org/) (which are mostly the same, except for some minor
differences)
* [PostgreSQL](https://www.postgresql.org/)
All these database types allow for the same basic functionality and allow Part-DB to run. However, there are some minor
differences between them, which might be important for you. Therefore the pros and cons of the different database types
are listed here.
{: .important } {: .important }
You have to choose between the database types before you start using Part-DB and **you can not change it (easily) after You have to choose between the database types before you start using Part-DB and **you can not change it (easily) after
@ -26,157 +18,29 @@ you have started creating data**. So you should choose the database type for you
## Comparison ## Comparison
### SQLite **SQLite** is the default database type which is configured out of the box. All data is saved in a single file (
normally `var/app.db` in the Part-DB folder) and no additional installation or configuration besides Part-DB is needed.
To use **MySQL/MariaDB** as database, you have to install and configure the MySQL server, configure it and create a
database and user for Part-DB, which needs some additional work. When using docker you need an additional docker
container, and volume for the data
#### Pros When using **SQLite** The database can be backuped easily by just copying the SQLite file to a safe place. Ideally, the *
*MySQL** database has to be dumped to a SQL file (using `mysqldump`). The `console partdb:backup` command can do this
automatically
* **Easy to use**: No additional installation or configuration is needed, just start Part-DB and it will work out of the box However, SQLite does not support certain operations like regex search, which has to be emulated by PHP and therefore is
* **Easy backup**: Just copy the SQLite file to a safe place, and you have a backup, which you can restore by copying it pretty slow compared to the same operation at MySQL. In the future, there might be features that may only be available, when
back. No need to work with SQL dumps using MySQL. Also, SQLite has limitations in comparisons and sorting of Unicode characters, which might lead to unexpected
behavior when using non-ASCII characters in your data. For example `µ` (micro sign) is not seen as equal to `μ(greek minuscule mu),
therefore searching for `µ` (micro sign) will not find parts containing `μ` (mu) and vice versa. In MySQL identical-looking characters are seen as equal, which is more intuitive in most cases.
#### Cons In general MySQL might perform better for big Part-DB instances with many entries, lots of users and high activity, than
SQLite.
* **Performance**: SQLite is not as fast as MySQL or PostgreSQL, especially when using complex queries or many users. ## Conclusion and Suggestion
* **Emulated RegEx search**: SQLite does not support RegEx search natively. Part-DB can emulate it, however that is pretty slow.
* **Emualted natural sorting**: SQLite does not support natural sorting natively. Part-DB can emulate it, but it is pretty slow.
* **Limitations with Unicode**: SQLite has limitations in comparisons and sorting of Unicode characters, which might lead to
unexpected behavior when using non-ASCII characters in your data. For example `µ` (micro sign) is not seen as equal to
`μ` (greek minuscule mu), therefore searching for `µ` (micro sign) will not find parts containing `μ` (mu) and vice versa.
The other databases behave more intuitive in this case.
* **No advanced features**: SQLite do no support many of the advanced features of MySQL or PostgreSQL, which might be utilized
in future versions of Part-DB
### MySQL/MariaDB
**If possible, it is recommended to use MariaDB 10.7+ (instead of MySQL), as it supports natural sorting of columns natively.**
#### Pros
* **Performance**: Compared to SQLite, MySQL/MariaDB will probably perform better, especially in large databases with many
users and high activity.
* **Natural Sorting**: MariaDB 10.7+ supports natural sorting of columns. On other databases it has to be emulated, which is pretty
slow.
* **Native RegEx search**: MySQL supports RegEx search natively, which is faster than emulating it in PHP.
* **Advanced features**: MySQL/MariaDB supports many advanced features, which might be utilized in future versions of Part-DB.
* **Full Unicode support**: MySQL/MariaDB has better support for Unicode characters, which makes it more intuitive to use
non-ASCII characters in your data.
#### Cons
* **Additional installation and configuration**: You have to install and configure the MySQL server, create a database and
user for Part-DB, which needs some additional work compared to SQLite.
* **Backup**: The MySQL database has to be dumped to a SQL file (using `mysqldump`). The `console partdb:backup` command can automate this.
### PostgreSQL
#### Pros
* **Performance**: PostgreSQL is known for its performance, especially in large databases with many users and high activity.
* **Advanced features**: PostgreSQL supports many advanced features, which might be utilized in future versions of Part-DB.
* **Full Unicode support**: PostgreSQL has better support for Unicode characters, which makes it more intuitive to use
non-ASCII characters in your data.
* **Native RegEx search**: PostgreSQL supports RegEx search natively, which is faster than emulating it in PHP.
* **Native Natural Sorting**: PostgreSQL supports natural sorting of columns natively in all versions and in general the support for it
is better than on MariaDB.
* **Support of transactional DDL**: PostgreSQL supports transactional DDL, which means that if you encounter a problem during a schema change,
the database will automatically rollback the changes. On MySQL/MariaDB you have to manually rollback the changes, by restoring from a database backup.
#### Cons
* **New backend**: The support of postgresql is new, and it was not tested as much as the other backends. There might be some bugs caused by this.
* **Additional installation and configuration**: You have to install and configure the PostgreSQL server, create a database and
user for Part-DB, which needs some additional work compared to SQLite.
* **Backup**: The PostgreSQL database has to be dumped to a SQL file (using `pg_dump`). The `console partdb:backup` command can automate this.
## Recommendation
When you are a hobbyist and use Part-DB for your own small inventory management with only you as user (or maybe sometimes When you are a hobbyist and use Part-DB for your own small inventory management with only you as user (or maybe sometimes
a few other people), then the easy-to-use SQLite database will be fine, as long as you can live with the limitations, stated above. a few other people), then the easy-to-use SQLite database will be fine.
However using MariaDB (or PostgreSQL), has no disadvantages in that situation (besides the initial setup requirements), so you might
want to use it, to be prepared for future use cases.
When you are planning to have a very big database, with a lot of entries and many users which regularly using Part-DB, then you should When you are planning to have a very big database, with a lot of entries and many users which regularly (and
use MariaDB or PostgreSQL, as they will perform better in that situation and allow for more advanced features. concurrently) using Part-DB you should maybe use MySQL as this will scale better.
If you should use MariaDB or PostgreSQL depends on your personal preference and what you already have installed on your servers and
what you are familiar with.
## Using the different databases
The only difference in using the different databases, is a different value in the `DATABASE_URL` environment variable in the `.env.local` file
or in the `DATABASE_URL` environment variable in your server or container configuration. It has the shape of a URL, where the scheme (the part before `://`)
is the database type, and the rest is connection information.
**The env var format below is for the `env.local` file. It might work differently for other env configuration. E.g. in a docker-compose file you have to remove the quotes!**
### SQLite
```shell
DATABASE_URL="sqlite:///%kernel.project_dir%/var/app.db"
```
Here you just need to configure the path to the SQLite file, which is created by Part-DB when performing the database migrations.
The `%kernel.project_dir%` is a placeholder for the path to the project directory, which is replaced by the actual path by Symfony, so that you do not
need to specify the path manually. In the example the database will be created as `app.db` in the `var` directory of your Part-DB installation folder.
### MySQL/MariaDB
```shell
DATABASE_URL="mysql://user:password@127.0.0.1:3306/database?serverVersion=8.0.37"
```
Here you have to replace `user`, `password` and `database` with the credentials of the MySQL/MariaDB user and the database name you want to use.
The host (here 127.0.0.1) and port should also be specified according to your MySQL/MariaDB server configuration.
In the `serverVersion` parameter you can specify the version of the MySQL/MariaDB server you are using, in the way the server returns it
(e.g. `8.0.37` for MySQL and `10.4.14-MariaDB`). If you do not know it, you can leave the default value.
If you want to use a unix socket for the connection instead of a TCP connnection, you can specify the socket path in the `unix_socket` parameter.
```shell
DATABASE_URL="mysql://user:password@localhost/database?serverVersion=8.0.37&unix_socket=/var/run/mysqld/mysqld.sock"
```
### PostgreSQL
```shell
DATABASE_URL="postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=12.19&charset=utf8"
```
Here you have to replace `db_user`, `db_password` and `db_name` with the credentials of the PostgreSQL user and the database name you want to use.
The host (here 127.0.0.1) and port should also be specified according to your PostgreSQL server configuration.
In the `serverVersion` parameter you can specify the version of the PostgreSQL server you are using, in the way the server returns it
(e.g. `12.19 (Debian 12.19-1.pgdg120+1)`). If you do not know it, you can leave the default value.
The `charset` parameter specify the character set of the database. It should be set to `utf8` to ensure that all characters are stored correctly.
If you want to use a unix socket for the connection instead of a TCP connnection, you can specify the socket path in the `host` parameter.
```shell
DATABASE_URL="postgresql://db_user@localhost/db_name?serverVersion=16.6&charset=utf8&host=/var/run/postgresql"
```
## Natural Sorting
Natural sorting is the sorting of strings in a way that numbers are sorted by their numerical value, not by their ASCII value.
For example in the classical binary sorting the string `DIP-4`, `DIP-8`, `DIP-16`, `DIP-28` would be sorted as following:
* `DIP-16`
* `DIP-28`
* `DIP-4`
* `DIP-8`
In natural sorting, it would be sorted as:
* `DIP-4`
* `DIP-8`
* `DIP-16`
* `DIP-28`
Part-DB can sort names in part tables and tree views naturally. PostgreSQL and MariaDB 10.7+ support natural sorting natively,
and it is automatically used if available.
For SQLite and MySQL < 10.7 it has to be emulated if wanted, which is pretty slow. Therefore it has to be explicity enabled by setting the
`DATABASE_EMULATE_NATURAL_SORT` environment variable to `1`. If it is 0 the classical binary sorting is used, on these databases. The emulations
might have some quirks and issues, so it is recommended to use a database which supports natural sorting natively, if you want to use it.

View file

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

View file

@ -158,7 +158,7 @@ services:
container_name: partdb_database container_name: partdb_database
image: mysql:8.0 image: mysql:8.0
restart: unless-stopped restart: unless-stopped
command: --default-authentication-plugin=mysql_native_password --log-bin-trust-function-creators=1 command: --default-authentication-plugin=mysql_native_password
environment: environment:
# Change this Password # Change this Password
MYSQL_ROOT_PASSWORD: SECRET_ROOT_PASSWORD MYSQL_ROOT_PASSWORD: SECRET_ROOT_PASSWORD
@ -201,10 +201,6 @@ You also have to create the database as described above in step 4.
You can run the console commands described in README by You can run the console commands described in README by
executing `docker exec --user=www-data -it partdb bin/console [command]` executing `docker exec --user=www-data -it partdb bin/console [command]`
{: .warning }
> If you run a root console inside the container, and wanna execute commands on the webserver behalf, be sure to use `sudo -E` command (with the `-E` flag) to preserve env variables from the current shell.
> Otherwise Part-DB console might use the wrong configuration to execute commands.
## Troubleshooting ## Troubleshooting
*Login is not possible. Login page is just reloading and no error message is shown or something like "CSFR token invalid"*: *Login is not possible. Login page is just reloading and no error message is shown or something like "CSFR token invalid"*:

View file

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

View file

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

View file

@ -25,12 +25,6 @@ is named `partdb`, you can execute the command `php bin/console cache:clear` wit
docker exec --user=www-data partdb php bin/console cache:clear docker exec --user=www-data partdb php bin/console cache:clear
``` ```
{: .warning }
> If you run a root console inside the docker container, and wanna execute commands on the webserver behalf, be sure to use `sudo -E` command (with the `-E` flag) to preserve env variables from the current shell.
> Otherwise Part-DB console might use the wrong configuration to execute commands.
## Troubleshooting
## User management commands ## User management commands
* `php bin/console partdb:users:list`: List all users of this Part-DB instance * `php bin/console partdb:users:list`: List all users of this Part-DB instance
@ -70,4 +64,4 @@ docker exec --user=www-data partdb php bin/console cache:clear
## Database commands ## Database commands
* `php bin/console doctrine:migrations:migrate`: Migrate the database to the latest version * `php bin/console doctrine:migrations:migrate`: Migrate the database to the latest version
* `php bin/console doctrine:migrations:up-to-date`: Check if the database is up-to-date * `php bin/console doctrine:migrations:up-to-date`: Check if the database is up-to-date

View file

@ -107,7 +107,7 @@ The following env configuration options are available:
default: `EUR`). If an offer is only available in a certain currency, default: `EUR`). If an offer is only available in a certain currency,
Part-DB will save the prices in their native currency, and you can use Part-DB currency conversion feature to convert Part-DB will save the prices in their native currency, and you can use Part-DB currency conversion feature to convert
it to your preferred currency. it to your preferred currency.
* `PROVIDER_OCTOPART_COUNTRY`: The country you want to get prices in if available (optional, 2 letter ISO-code, * `PROVIDER_OCOTPART_COUNTRY`: The country you want to get prices in if available (optional, 2 letter ISO-code,
default: `DE`). To get the correct prices, you have to set this and the currency setting to the correct value. default: `DE`). To get the correct prices, you have to set this and the currency setting to the correct value.
* `PROVIDER_OCTOPART_SEARCH_LIMIT`: The maximum number of results to return per search (optional, default: `10`). This * `PROVIDER_OCTOPART_SEARCH_LIMIT`: The maximum number of results to return per search (optional, default: `10`). This
affects how quickly your monthly limit is used up. affects how quickly your monthly limit is used up.
@ -127,9 +127,6 @@ You must create an organization there and create a "Production app". Most settin
grant access to the "Product Information" API. grant access to the "Product Information" API.
You will get a Client ID and a Client Secret, which you have to put in the Part-DB env configuration (see below). You will get a Client ID and a Client Secret, which you have to put in the Part-DB env configuration (see below).
**Attention**: Currently only the "Product Information V3 (Deprecated)" is supported by Part-DB.
Using "Product Information V4" will not work.
The following env configuration options are available: The following env configuration options are available:
* `PROVIDER_DIGIKEY_CLIENT_ID`: The client ID you got from Digi-Key (mandatory) * `PROVIDER_DIGIKEY_CLIENT_ID`: The client ID you got from Digi-Key (mandatory)
@ -215,46 +212,6 @@ An API key is not required, it is enough to enable the provider using the follow
* `PROVIDER_LCSC_ENABLED`: Set this to `1` to enable the LCSC provider * `PROVIDER_LCSC_ENABLED`: Set this to `1` to enable the LCSC provider
* `PROVIDER_LCSC_CURRENCY`: The currency you want to get prices in (see LCSC webshop for available currencies, default: `EUR`) * `PROVIDER_LCSC_CURRENCY`: The currency you want to get prices in (see LCSC webshop for available currencies, default: `EUR`)
### OEMsecrets
The oemsecrets provider uses the [oemsecrets API](https://www.oemsecrets.com/) to search for parts and getting shopping
information from them. Similar to octopart it aggregates offers from different distributors.
You can apply for a free API key on the [oemsecrets API page](https://www.oemsecrets.com/api/) and put the key you get
in the Part-DB env configuration (see below).
The following env configuration options are available:
* `PROVIDER_OEMSECRETS_KEY`: The API key you got from oemsecrets (mandatory)
* `PROVIDER_OEMSECRETS_COUNTRY_CODE`: The two-letter code of the country you want to get the prices for
* `PROVIDER_OEMSECRETS_CURRENCY`: The currency you want to get prices in (optional, default: `EUR`)
* `PROVIDER_OEMSECRETS_ZERO_PRICE`: If set to `1`, parts with a price of 0 will be included in the search results, otherwise
they will be excluded (optional, default: `0`)
* `PROVIDER_OEMSECRETS_SET_PARAM`: If set to `1`, the provider will try to extract parameters from the part description
* `PROVIDER_OEMSECRETS_SORT_CRITERIA`: The criteria to sort the search results by. If set to 'C', it further sorts by
completeness (prioritizing items with the most detailed information). If set to 'M', it further sorts by manufacturer name.
If set to any other value, no sorting is performed.
### Reichelt
The reichelt provider uses webscraping from [reichelt.com](https://reichelt.com/) to get part information.
This is not an official API and could break at any time. So use it at your own risk.
The following env configuration options are available:
* `PROVIDER_REICHELT_ENABLED`: Set this to `1` to enable the Reichelt provider
* `PROVIDER_REICHELT_CURRENCY`: The currency you want to get prices in. Only possible for countries which use Non-EUR (optional, default: `EUR`)
* `PROVIDER_REICHELT_COUNTRY`: The country you want to get the prices for (optional, default: `DE`)
* `PROVIDER_REICHELT_LANGUAGE`: The language you want to get the descriptions in (optional, default: `en`)
* `PROVIDER_REICHELT_INCLUDE_VAT`: If set to `1`, the prices will be gross prices (including tax), otherwise net prices (optional, default: `1`)
### Pollin
The pollin provider uses webscraping from [pollin.de](https://www.pollin.de/) to get part information.
This is not an official API and could break at any time. So use it at your own risk.
The following env configuration options are available:
* `PROVIDER_POLLIN_ENABLED`: Set this to `1` to enable the Pollin provider
### Custom provider ### Custom provider
To create a custom provider, you have to create a new class implementing the `InfoProviderInterface` interface. As long To create a custom provider, you have to create a new class implementing the `InfoProviderInterface` interface. As long

View file

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

View file

@ -343,7 +343,6 @@ final class Version20240606203053 extends AbstractMultiPlatformMigration impleme
$this->addSql('ALTER TABLE webauthn_keys CHANGE public_key_credential_id public_key_credential_id LONGTEXT NOT NULL, CHANGE transports transports LONGTEXT NOT NULL, CHANGE trust_path trust_path JSON NOT NULL, CHANGE aaguid aaguid TINYTEXT NOT NULL, CHANGE credential_public_key credential_public_key LONGTEXT NOT NULL, CHANGE other_ui other_ui LONGTEXT DEFAULT NULL, CHANGE last_time_used last_time_used DATETIME DEFAULT NULL'); $this->addSql('ALTER TABLE webauthn_keys CHANGE public_key_credential_id public_key_credential_id LONGTEXT NOT NULL, CHANGE transports transports LONGTEXT NOT NULL, CHANGE trust_path trust_path JSON NOT NULL, CHANGE aaguid aaguid TINYTEXT NOT NULL, CHANGE credential_public_key credential_public_key LONGTEXT NOT NULL, CHANGE other_ui other_ui LONGTEXT DEFAULT NULL, CHANGE last_time_used last_time_used DATETIME DEFAULT NULL');
// Add the natural sort emulation function to the database (based on this stackoverflow: https://stackoverflow.com/questions/153633/natural-sort-in-mysql/58154535#58154535) // Add the natural sort emulation function to the database (based on this stackoverflow: https://stackoverflow.com/questions/153633/natural-sort-in-mysql/58154535#58154535)
//This version here is wrong, and will be replaced by the correct version in the next migration (we need to use nowdoc instead of heredoc, otherwise the slashes will be wrongly escaped!!)
$this->addSql(<<<EOD $this->addSql(<<<EOD
CREATE DEFINER=CURRENT_USER FUNCTION `NatSortKey`(`s` VARCHAR(1000) CHARSET utf8mb4, `n` INT) RETURNS varchar(3500) CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci CREATE DEFINER=CURRENT_USER FUNCTION `NatSortKey`(`s` VARCHAR(1000) CHARSET utf8mb4, `n` INT) RETURNS varchar(3500) CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci
DETERMINISTIC DETERMINISTIC
@ -412,7 +411,7 @@ final class Version20240606203053 extends AbstractMultiPlatformMigration impleme
DECLARE x,y varchar(1000); # need to be same length as input s DECLARE x,y varchar(1000); # need to be same length as input s
DECLARE r varchar(3500) DEFAULT ''; # return value: needs to be same length as return type DECLARE r varchar(3500) DEFAULT ''; # return value: needs to be same length as return type
DECLARE suf varchar(1001); # suffix for a number or version string. Must be (((inputlength+1) DIV 2)*2 + 1) chars to support version strings (e.g. '1.2.33.5'), though it's usually just 3 chars. (Max version string e.g. 1.2. ... .5 has ((length of input + 1) DIV 2) numeric components) DECLARE suf varchar(101); # suffix for a number or version string. Must be (((inputlength+1) DIV 2)*2 + 1) chars to support version strings (e.g. '1.2.33.5'), though it's usually just 3 chars. (Max version string e.g. 1.2. ... .5 has ((length of input + 1) DIV 2) numeric components)
DECLARE i,j,k int UNSIGNED; DECLARE i,j,k int UNSIGNED;
IF n<=0 THEN SET n := -1; END IF; # n<=0 means "process all numbers" IF n<=0 THEN SET n := -1; END IF; # n<=0 means "process all numbers"
LOOP LOOP

View file

@ -1,165 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use App\Migration\AbstractMultiPlatformMigration;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20240728145604 extends AbstractMultiPlatformMigration
{
public function getDescription(): string
{
return 'Update the Natural Sorting function for MySQL';
}
public function mySQLUp(Schema $schema): void
{
//Remove the old function
$this->addSql('DROP FUNCTION IF EXISTS NatSortKey');
//The difference to the original function is the correct length of the suf variable and correct escaping
//We now use heredoc syntax to avoid escaping issues with the \ (which resulted in "range out of order in character class").
$this->addSql(<<<'EOD'
CREATE DEFINER=CURRENT_USER FUNCTION `NatSortKey`(`s` VARCHAR(1000) CHARSET utf8mb4, `n` INT) RETURNS varchar(3500) CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci
DETERMINISTIC
SQL SECURITY INVOKER
BEGIN
/****
Converts numbers in the input string s into a format such that sorting results in a nat-sort.
Numbers of up to 359 digits (before the decimal point, if one is present) are supported. Sort results are undefined if the input string contains numbers longer than this.
For n>0, only the first n numbers in the input string will be converted for nat-sort (so strings that differ only after the first n numbers will not nat-sort amongst themselves).
Total sort-ordering is preserved, i.e. if s1!=s2, then NatSortKey(s1,n)!=NatSortKey(s2,n), for any given n.
Numbers may contain ',' as a thousands separator, and '.' as a decimal point. To reverse these (as appropriate for some European locales), the code would require modification.
Numbers preceded by '+' sort with numbers not preceded with either a '+' or '-' sign.
Negative numbers (preceded with '-') sort before positive numbers, but are sorted in order of ascending absolute value (so -7 sorts BEFORE -1001).
Numbers with leading zeros sort after the same number with no (or fewer) leading zeros.
Decimal-part-only numbers (like .75) are recognised, provided the decimal point is not immediately preceded by either another '.', or by a letter-type character.
Numbers with thousand separators sort after the same number without them.
Thousand separators are only recognised in numbers with no leading zeros that don't immediately follow a ',', and when they format the number correctly.
(When not recognised as a thousand separator, a ',' will instead be treated as separating two distinct numbers).
Version-number-like sequences consisting of 3 or more numbers separated by '.' are treated as distinct entities, and each component number will be nat-sorted.
The entire entity will sort after any number beginning with the first component (so e.g. 10.2.1 sorts after both 10 and 10.995, but before 11)
Note that The first number component in an entity like this is also permitted to contain thousand separators.
To achieve this, numbers within the input string are prefixed and suffixed according to the following format:
- The number is prefixed by a 2-digit base-36 number representing its length, excluding leading zeros. If there is a decimal point, this length only includes the integer part of the number.
- A 3-character suffix is appended after the number (after the decimals if present).
- The first character is a space, or a '+' sign if the number was preceded by '+'. Any preceding '+' sign is also removed from the front of the number.
- This is followed by a 2-digit base-36 number that encodes the number of leading zeros and whether the number was expressed in comma-separated form (e.g. 1,000,000.25 vs 1000000.25)
- The value of this 2-digit number is: (number of leading zeros)*2 + (1 if comma-separated, 0 otherwise)
- For version number sequences, each component number has the prefix in front of it, and the separating dots are removed.
Then there is a single suffix that consists of a ' ' or '+' character, followed by a pair base-36 digits for each number component in the sequence.
e.g. here is how some simple sample strings get converted:
'Foo055' --> 'Foo0255 02'
'Absolute zero is around -273 centigrade' --> 'Absolute zero is around -03273 00 centigrade'
'The $1,000,000 prize' --> 'The $071000000 01 prize'
'+99.74 degrees' --> '0299.74+00 degrees'
'I have 0 apples' --> 'I have 00 02 apples'
'.5 is the same value as 0000.5000' --> '00.5 00 is the same value as 00.5000 08'
'MariaDB v10.3.0018' --> 'MariaDB v02100130218 000004'
The restriction to numbers of up to 359 digits comes from the fact that the first character of the base-36 prefix MUST be a decimal digit, and so the highest permitted prefix value is '9Z' or 359 decimal.
The code could be modified to handle longer numbers by increasing the size of (both) the prefix and suffix.
A higher base could also be used (by replacing CONV() with a custom function), provided that the collation you are using sorts the "digits" of the base in the correct order, starting with 0123456789.
However, while the maximum number length may be increased this way, note that the technique this function uses is NOT applicable where strings may contain numbers of unlimited length.
The function definition does not specify the charset or collation to be used for string-type parameters or variables: The default database charset & collation at the time the function is defined will be used.
This is to make the function code more portable. However, there are some important restrictions:
- Collation is important here only when comparing (or storing) the output value from this function, but it MUST order the characters " +0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" in that order for the natural sort to work.
This is true for most collations, but not all of them, e.g. in Lithuanian 'Y' comes before 'J' (according to Wikipedia).
To adapt the function to work with such collations, replace CONV() in the function code with a custom function that emits "digits" above 9 that are characters ordered according to the collation in use.
- For efficiency, the function code uses LENGTH() rather than CHAR_LENGTH() to measure the length of strings that consist only of digits 0-9, '.', and ',' characters.
This works for any single-byte charset, as well as any charset that maps standard ASCII characters to single bytes (such as utf8 or utf8mb4).
If using a charset that maps these characters to multiple bytes (such as, e.g. utf16 or utf32), you MUST replace all instances of LENGTH() in the function definition with CHAR_LENGTH()
Length of the output:
Each number converted adds 5 characters (2 prefix + 3 suffix) to the length of the string. n is the maximum count of numbers to convert;
This parameter is provided as a means to limit the maximum output length (to input length + 5*n).
If you do not require the total-ordering property, you could edit the code to use suffixes of 1 character (space or plus) only; this would reduce the maximum output length for any given n.
Since a string of length L has at most ((L+1) DIV 2) individual numbers in it (every 2nd character a digit), for n<=0 the maximum output length is (inputlength + 5*((inputlength+1) DIV 2))
So for the current input length of 100, the maximum output length is 350.
If changing the input length, the output length must be modified according to the above formula. The DECLARE statements for x,y,r, and suf must also be modified, as the code comments indicate.
****/
DECLARE x,y varchar(1000); # need to be same length as input s
DECLARE r varchar(3500) DEFAULT ''; # return value: needs to be same length as return type
DECLARE suf varchar(1001); # suffix for a number or version string. Must be (((inputlength+1) DIV 2)*2 + 1) chars to support version strings (e.g. '1.2.33.5'), though it's usually just 3 chars. (Max version string e.g. 1.2. ... .5 has ((length of input + 1) DIV 2) numeric components)
DECLARE i,j,k int UNSIGNED;
IF n<=0 THEN SET n := -1; END IF; # n<=0 means "process all numbers"
LOOP
SET i := REGEXP_INSTR(s,'\\d'); # find position of next digit
IF i=0 OR n=0 THEN RETURN CONCAT(r,s); END IF; # no more numbers to process -> we're done
SET n := n-1, suf := ' ';
IF i>1 THEN
IF SUBSTRING(s,i-1,1)='.' AND (i=2 OR SUBSTRING(s,i-2,1) RLIKE '[^.\\p{L}\\p{N}\\p{M}\\x{608}\\x{200C}\\x{200D}\\x{2100}-\\x{214F}\\x{24B6}-\\x{24E9}\\x{1F130}-\\x{1F149}\\x{1F150}-\\x{1F169}\\x{1F170}-\\x{1F189}]') AND (SUBSTRING(s,i) NOT RLIKE '^\\d++\\.\\d') THEN SET i:=i-1; END IF; # Allow decimal number (but not version string) to begin with a '.', provided preceding char is neither another '.', nor a member of the unicode character classes: "Alphabetic", "Letter", "Block=Letterlike Symbols" "Number", "Mark", "Join_Control"
IF i>1 AND SUBSTRING(s,i-1,1)='+' THEN SET suf := '+', j := i-1; ELSE SET j := i; END IF; # move any preceding '+' into the suffix, so equal numbers with and without preceding "+" signs sort together
SET r := CONCAT(r,SUBSTRING(s,1,j-1)); SET s = SUBSTRING(s,i); # add everything before the number to r and strip it from the start of s; preceding '+' is dropped (not included in either r or s)
END IF;
SET x := REGEXP_SUBSTR(s,IF(SUBSTRING(s,1,1) IN ('0','.') OR (SUBSTRING(r,-1)=',' AND suf=' '),'^\\d*+(?:\\.\\d++)*','^(?:[1-9]\\d{0,2}(?:,\\d{3}(?!\\d))++|\\d++)(?:\\.\\d++)*+')); # capture the number + following decimals (including multiple consecutive '.<digits>' sequences)
SET s := SUBSTRING(s,CHAR_LENGTH(x)+1); # NOTE: CHAR_LENGTH() can be safely used instead of CHAR_LENGTH() here & below PROVIDED we're using a charset that represents digits, ',' and '.' characters using single bytes (e.g. latin1, utf8)
SET i := INSTR(x,'.');
IF i=0 THEN SET y := ''; ELSE SET y := SUBSTRING(x,i); SET x := SUBSTRING(x,1,i-1); END IF; # move any following decimals into y
SET i := CHAR_LENGTH(x);
SET x := REPLACE(x,',','');
SET j := CHAR_LENGTH(x);
SET x := TRIM(LEADING '0' FROM x); # strip leading zeros
SET k := CHAR_LENGTH(x);
SET suf := CONCAT(suf,LPAD(CONV(LEAST((j-k)*2,1294) + IF(i=j,0,1),10,36),2,'0')); # (j-k)*2 + IF(i=j,0,1) = (count of leading zeros)*2 + (1 if there are thousands-separators, 0 otherwise) Note the first term is bounded to <= base-36 'ZY' as it must fit within 2 characters
SET i := LOCATE('.',y,2);
IF i=0 THEN
SET r := CONCAT(r,LPAD(CONV(LEAST(k,359),10,36),2,'0'),x,y,suf); # k = count of digits in number, bounded to be <= '9Z' base-36
ELSE # encode a version number (like 3.12.707, etc)
SET r := CONCAT(r,LPAD(CONV(LEAST(k,359),10,36),2,'0'),x); # k = count of digits in number, bounded to be <= '9Z' base-36
WHILE CHAR_LENGTH(y)>0 AND n!=0 DO
IF i=0 THEN SET x := SUBSTRING(y,2); SET y := ''; ELSE SET x := SUBSTRING(y,2,i-2); SET y := SUBSTRING(y,i); SET i := LOCATE('.',y,2); END IF;
SET j := CHAR_LENGTH(x);
SET x := TRIM(LEADING '0' FROM x); # strip leading zeros
SET k := CHAR_LENGTH(x);
SET r := CONCAT(r,LPAD(CONV(LEAST(k,359),10,36),2,'0'),x); # k = count of digits in number, bounded to be <= '9Z' base-36
SET suf := CONCAT(suf,LPAD(CONV(LEAST((j-k)*2,1294),10,36),2,'0')); # (j-k)*2 = (count of leading zeros)*2, bounded to fit within 2 base-36 digits
SET n := n-1;
END WHILE;
SET r := CONCAT(r,y,suf);
END IF;
END LOOP;
END
EOD
);
}
public function mySQLDown(Schema $schema): void
{
//Not needed
}
public function sqLiteUp(Schema $schema): void
{
//Not needed
}
public function sqLiteDown(Schema $schema): void
{
//Not needed
}
public function postgreSQLUp(Schema $schema): void
{
//Not needed
}
public function postgreSQLDown(Schema $schema): void
{
//Not needed
}
}

View file

@ -1,42 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20250220215048 extends AbstractMigration
{
public function getDescription(): string
{
return 'Split $path property for attachments into $internal_path and $external_path';
}
public function up(Schema $schema): void
{
//Create the new columns as nullable (that is easier modifying them)
$this->addSql('ALTER TABLE attachments ADD internal_path VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE attachments ADD external_path VARCHAR(255) DEFAULT NULL');
//Copy the data from path to external_path and remove the path column
$this->addSql('UPDATE attachments SET external_path=path');
$this->addSql('ALTER TABLE attachments DROP path');
$this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%MEDIA#%%\' ESCAPE \'#\'');
$this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%BASE#%%\' ESCAPE \'#\'');
$this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%SECURE#%%\' ESCAPE \'#\'');
$this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%FOOTPRINTS#%%\' ESCAPE \'#\'');
$this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%FOOTPRINTS3D#%%\' ESCAPE \'#\'');
$this->addSql('UPDATE attachments SET external_path=NULL WHERE internal_path IS NOT NULL');
}
public function down(Schema $schema): void
{
$this->addSql('UPDATE attachments SET external_path=internal_path WHERE internal_path IS NOT NULL');
$this->addSql('ALTER TABLE attachments DROP internal_path');
$this->addSql('ALTER TABLE attachments RENAME COLUMN external_path TO path');
}
}

View file

@ -1,31 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250222165240 extends AbstractMigration
{
public function getDescription(): string
{
return 'Migrate the old attachment class discriminator values from legacy Part-DB to the modern format, so that there is just one unified value';
}
public function up(Schema $schema): void
{
//Change the old discriminator values to the new ones
$this->addSql("UPDATE attachments SET class_name = 'Part' WHERE class_name = 'PartDB\Part'");
$this->addSql("UPDATE attachments SET class_name = 'Device' WHERE class_name = 'PartDB\Device'");
}
public function down(Schema $schema): void
{
//No down required, as the new format can also be read by older Part-DB version
}
}

View file

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

View file

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

55
psalm.xml Normal file
View file

@ -0,0 +1,55 @@
<?xml version="1.0"?>
<psalm
errorLevel="5"
totallyTyped="false"
resolveFromConfigFile="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
>
<projectFiles>
<directory name="src"/>
<ignoreFiles>
<directory name="vendor"/>
</ignoreFiles>
</projectFiles>
<issueHandlers>
<LessSpecificReturnType errorLevel="info"/>
<!-- level 3 issues - slightly lazy code writing, but provably low false-negatives -->
<DeprecatedMethod errorLevel="info"/>
<DeprecatedProperty errorLevel="info"/>
<DeprecatedClass errorLevel="info"/>
<DeprecatedConstant errorLevel="info"/>
<DeprecatedFunction errorLevel="info"/>
<DeprecatedInterface errorLevel="info"/>
<DeprecatedTrait errorLevel="info"/>
<InternalMethod errorLevel="info"/>
<InternalProperty errorLevel="info"/>
<InternalClass errorLevel="info"/>
<MissingClosureReturnType errorLevel="info"/>
<MissingReturnType errorLevel="info"/>
<MissingPropertyType errorLevel="info"/>
<InvalidDocblock errorLevel="info"/>
<PropertyNotSetInConstructor errorLevel="info"/>
<MissingConstructor errorLevel="info"/>
<MissingClosureParamType errorLevel="info"/>
<MissingParamType errorLevel="info"/>
<RedundantCondition errorLevel="info"/>
<DocblockTypeContradiction errorLevel="info"/>
<RedundantConditionGivenDocblockType errorLevel="info"/>
<UnresolvableInclude errorLevel="info"/>
<RawObjectIteration errorLevel="info"/>
<InvalidStringClass errorLevel="info"/>
</issueHandlers>
<plugins><pluginClass class="Psalm\SymfonyPsalmPlugin\Plugin"/></plugins></psalm>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -2,17 +2,15 @@
declare(strict_types=1); declare(strict_types=1);
use Rector\CodeQuality\Rector\Identical\FlipTypeControlToUseExclusiveTypeRector;
use Rector\CodingStyle\Rector\FuncCall\CountArrayToEmptyArrayComparisonRector; use Rector\CodingStyle\Rector\FuncCall\CountArrayToEmptyArrayComparisonRector;
use Rector\Config\RectorConfig; use Rector\Config\RectorConfig;
use Rector\Doctrine\Set\DoctrineSetList; use Rector\Doctrine\Set\DoctrineSetList;
use Rector\PHPUnit\CodeQuality\Rector\Class_\PreferPHPUnitThisCallRector; use Rector\PHPUnit\Rector\ClassMethod\AddDoesNotPerformAssertionToNonAssertingTestRector;
use Rector\PHPUnit\Set\PHPUnitLevelSetList;
use Rector\PHPUnit\Set\PHPUnitSetList; use Rector\PHPUnit\Set\PHPUnitSetList;
use Rector\Set\ValueObject\LevelSetList; use Rector\Set\ValueObject\LevelSetList;
use Rector\Set\ValueObject\SetList; use Rector\Set\ValueObject\SetList;
use Rector\Symfony\CodeQuality\Rector\Class_\EventListenerToEventSubscriberRector; use Rector\Symfony\Set\SymfonyLevelSetList;
use Rector\Symfony\CodeQuality\Rector\ClassMethod\ActionSuffixRemoverRector;
use Rector\Symfony\CodeQuality\Rector\MethodCall\LiteralGetToRequestClassConstantRector;
use Rector\Symfony\Set\SymfonySetList; use Rector\Symfony\Set\SymfonySetList;
use Rector\TypeDeclaration\Rector\StmtsAwareInterface\DeclareStrictTypesRector; use Rector\TypeDeclaration\Rector\StmtsAwareInterface\DeclareStrictTypesRector;
@ -46,36 +44,20 @@ return static function (RectorConfig $rectorConfig): void {
LevelSetList::UP_TO_PHP_81, LevelSetList::UP_TO_PHP_81,
//Symfony rules //Symfony rules
SymfonyLevelSetList::UP_TO_SYMFONY_62,
SymfonySetList::SYMFONY_CODE_QUALITY, SymfonySetList::SYMFONY_CODE_QUALITY,
SymfonySetList::SYMFONY_64,
//Doctrine rules //Doctrine rules
DoctrineSetList::ANNOTATIONS_TO_ATTRIBUTES, DoctrineSetList::ANNOTATIONS_TO_ATTRIBUTES,
DoctrineSetList::DOCTRINE_CODE_QUALITY, DoctrineSetList::DOCTRINE_CODE_QUALITY,
//PHPUnit rules //PHPUnit rules
PHPUnitLevelSetList::UP_TO_PHPUNIT_90,
PHPUnitSetList::PHPUNIT_CODE_QUALITY, PHPUnitSetList::PHPUNIT_CODE_QUALITY,
PHPUnitSetList::PHPUNIT_90,
]); ]);
$rectorConfig->skip([ $rectorConfig->skip([
AddDoesNotPerformAssertionToNonAssertingTestRector::class,
CountArrayToEmptyArrayComparisonRector::class, CountArrayToEmptyArrayComparisonRector::class,
//Leave our !== null checks alone
FlipTypeControlToUseExclusiveTypeRector::class,
//Leave our PartList TableAction alone
ActionSuffixRemoverRector::class,
//We declare event listeners via attributes, therefore no need to migrate them to subscribers
EventListenerToEventSubscriberRector::class,
PreferPHPUnitThisCallRector::class,
//Do not replace 'GET' with class constant,
LiteralGetToRequestClassConstantRector::class,
]);
//Do not apply rules to Symfony own files
$rectorConfig->skip([
__DIR__ . '/public/index.php',
__DIR__ . '/src/Kernel.php',
__DIR__ . '/config/preload.php',
__DIR__ . '/config/bundles.php',
]); ]);
}; };

View file

@ -0,0 +1,116 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\ApiPlatform;
use ApiPlatform\JsonSchema\Schema;
use ApiPlatform\JsonSchema\SchemaFactoryInterface;
use ApiPlatform\Metadata\Operation;
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
/**
* This decorator adds the properties given by DocumentedAPIProperty attributes on the classes to the schema.
*/
#[AsDecorator('api_platform.json_schema.schema_factory')]
class AddDocumentedAPIPropertiesJSONSchemaFactory implements SchemaFactoryInterface
{
public function __construct(private readonly SchemaFactoryInterface $decorated)
{
}
public function buildSchema(
string $className,
string $format = 'json',
string $type = Schema::TYPE_OUTPUT,
Operation $operation = null,
Schema $schema = null,
array $serializerContext = null,
bool $forceCollection = false
): Schema {
$schema = $this->decorated->buildSchema($className, $format, $type, $operation, $schema, $serializerContext, $forceCollection);
//Check if there is are DocumentedAPIProperty attributes on the class
$reflectionClass = new \ReflectionClass($className);
$attributes = $reflectionClass->getAttributes(DocumentedAPIProperty::class);
foreach ($attributes as $attribute) {
/** @var DocumentedAPIProperty $api_property */
$api_property = $attribute->newInstance();
$this->addPropertyToSchema($schema, $api_property->schemaName, $api_property->property,
$api_property, $serializerContext ?? [], $format);
}
return $schema;
}
private function addPropertyToSchema(Schema $schema, string $definitionName, string $normalizedPropertyName, DocumentedAPIProperty $propertyMetadata, array $serializerContext, string $format): void
{
$version = $schema->getVersion();
$swagger = Schema::VERSION_SWAGGER === $version;
$propertySchema = [];
if (false === $propertyMetadata->writeable) {
$propertySchema['readOnly'] = true;
}
if (!$swagger && false === $propertyMetadata->readable) {
$propertySchema['writeOnly'] = true;
}
if (null !== $description = $propertyMetadata->description) {
$propertySchema['description'] = $description;
}
$deprecationReason = $propertyMetadata->deprecationReason;
// see https://github.com/json-schema-org/json-schema-spec/pull/737
if (!$swagger && null !== $deprecationReason) {
$propertySchema['deprecated'] = true;
}
if (!empty($default = $propertyMetadata->default)) {
if ($default instanceof \BackedEnum) {
$default = $default->value;
}
$propertySchema['default'] = $default;
}
if (!empty($example = $propertyMetadata->example)) {
$propertySchema['example'] = $example;
}
if (!isset($propertySchema['example']) && isset($propertySchema['default'])) {
$propertySchema['example'] = $propertySchema['default'];
}
$propertySchema['type'] = $propertyMetadata->type;
$propertySchema['nullable'] = $propertyMetadata->nullable;
$propertySchema = new \ArrayObject($propertySchema);
$schema->getDefinitions()[$definitionName]['properties'][$normalizedPropertyName] = $propertySchema;
}
}

View file

@ -1,73 +0,0 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2025 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\ApiPlatform\DocumentedAPIProperties;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ReflectionClass;
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
/**
* This decorator adds the virtual properties defined by the DocumentedAPIProperty attribute to the property metadata
* which then get picked up by the openapi schema generator
*/
#[AsDecorator('api_platform.metadata.property.metadata_factory')]
class PropertyMetadataFactory implements PropertyMetadataFactoryInterface
{
public function __construct(private PropertyMetadataFactoryInterface $decorated)
{
}
public function create(string $resourceClass, string $property, array $options = []): ApiProperty
{
$metadata = $this->decorated->create($resourceClass, $property, $options);
//Only become active in the context of the openapi schema generation
if (!isset($options['schema_type'])) {
return $metadata;
}
if (!class_exists($resourceClass)) {
return $metadata;
}
$refClass = new ReflectionClass($resourceClass);
$attributes = $refClass->getAttributes(DocumentedAPIProperty::class);
//Look for the DocumentedAPIProperty attribute with the given property name
foreach ($attributes as $attribute) {
/** @var DocumentedAPIProperty $api_property */
$api_property = $attribute->newInstance();
//If attribute not matches the property name, skip it
if ($api_property->property !== $property) {
continue;
}
//Return the virtual property
return $api_property->toAPIProperty();
}
return $metadata;
}
}

View file

@ -1,68 +0,0 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2025 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\ApiPlatform\DocumentedAPIProperties;
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use ApiPlatform\Metadata\Property\PropertyNameCollection;
use ReflectionClass;
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
/**
* This decorator adds the virtual property names defined by the DocumentedAPIProperty attribute to the property name collection
* which then get picked up by the openapi schema generator
*/
#[AsDecorator('api_platform.metadata.property.name_collection_factory')]
class PropertyNameCollectionFactory implements PropertyNameCollectionFactoryInterface
{
public function __construct(private readonly PropertyNameCollectionFactoryInterface $decorated)
{
}
public function create(string $resourceClass, array $options = []): PropertyNameCollection
{
// Get the default properties from the decorated service
$propertyNames = $this->decorated->create($resourceClass, $options);
//Only become active in the context of the openapi schema generation
if (!isset($options['schema_type'])) {
return $propertyNames;
}
if (!class_exists($resourceClass)) {
return $propertyNames;
}
$properties = iterator_to_array($propertyNames);
$refClass = new ReflectionClass($resourceClass);
foreach ($refClass->getAttributes(DocumentedAPIProperty::class) as $attribute) {
/** @var DocumentedAPIProperty $instance */
$instance = $attribute->newInstance();
$properties[] = $instance->property;
}
return new PropertyNameCollection($properties);
}
}

View file

@ -21,9 +21,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace App\ApiPlatform\DocumentedAPIProperties; namespace App\ApiPlatform;
use ApiPlatform\Metadata\ApiProperty;
/** /**
* When this attribute is applied to a class, an property will be added to the API documentation using the given parameters. * When this attribute is applied to a class, an property will be added to the API documentation using the given parameters.
@ -66,55 +64,4 @@ final class DocumentedAPIProperty
) )
{ {
} }
public function toAPIProperty(bool $use_swagger = false): ApiProperty
{
$openApiContext = [];
if (false === $this->writeable) {
$openApiContext['readOnly'] = true;
}
if (!$use_swagger && false === $this->readable) {
$openApiContext['writeOnly'] = true;
}
if (null !== $description = $this->description) {
$openApiContext['description'] = $description;
}
$deprecationReason = $this->deprecationReason;
// see https://github.com/json-schema-org/json-schema-spec/pull/737
if (!$use_swagger && null !== $deprecationReason) {
$openApiContext['deprecated'] = true;
}
if (!empty($default = $this->default)) {
if ($default instanceof \BackedEnum) {
$default = $default->value;
}
$openApiContext['default'] = $default;
}
if (!empty($example = $this->example)) {
$openApiContext['example'] = $example;
}
if (!isset($openApiContext['example']) && isset($openApiContext['default'])) {
$openApiContext['example'] = $openApiContext['default'];
}
$openApiContext['type'] = $this->type;
$openApiContext['nullable'] = $this->nullable;
return new ApiProperty(
description: $this->description,
readable: $this->readable,
writable: $this->writeable,
openapiContext: $openApiContext,
types: $this->type,
property: $this->property
);
}
} }

View file

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

View file

@ -81,17 +81,23 @@ class EntityFilterHelper
public function getDescription(array $properties): array public function getDescription(array $properties): array
{ {
if ($properties === []) { if (!$properties) {
return []; return [];
} }
$description = []; $description = [];
foreach (array_keys($properties) as $property) { foreach ($properties as $property => $strategy) {
$description[(string)$property] = [ $description[(string)$property] = [
'property' => $property, 'property' => $property,
'type' => Type::BUILTIN_TYPE_STRING, 'type' => Type::BUILTIN_TYPE_STRING,
'required' => false, 'required' => false,
'description' => 'Filter using a comma seperated list of element IDs. Use + to include all direct children and ++ to include all children recursively.', 'description' => 'Filter using a comma seperated list of element IDs. Use + to include all direct children and ++ to include all children recursively.',
'openapi' => [
'example' => '',
'allowReserved' => false,// if true, query parameters will be not percent-encoded
'allowEmptyValue' => true,
'explode' => false, // to be true, the type must be Type::BUILTIN_TYPE_ARRAY, ?product=blue,green will be ?product=blue&product=green
],
]; ];
} }
return $description; return $description;

View file

@ -38,7 +38,7 @@ final class LikeFilter extends AbstractFilter
QueryBuilder $queryBuilder, QueryBuilder $queryBuilder,
QueryNameGeneratorInterface $queryNameGenerator, QueryNameGeneratorInterface $queryNameGenerator,
string $resourceClass, string $resourceClass,
?Operation $operation = null, Operation $operation = null,
array $context = [] array $context = []
): void { ): void {
// Otherwise filter is applied to order and page as well // Otherwise filter is applied to order and page as well
@ -50,7 +50,7 @@ final class LikeFilter extends AbstractFilter
} }
$parameterName = $queryNameGenerator->generateParameterName($property); // Generate a unique parameter name to avoid collisions with other filters $parameterName = $queryNameGenerator->generateParameterName($property); // Generate a unique parameter name to avoid collisions with other filters
$queryBuilder $queryBuilder
->andWhere(sprintf('ILIKE(o.%s, :%s) = TRUE', $property, $parameterName)) ->andWhere(sprintf('o.%s LIKE :%s', $property, $parameterName))
->setParameter($parameterName, $value); ->setParameter($parameterName, $value);
} }
@ -61,12 +61,18 @@ final class LikeFilter extends AbstractFilter
} }
$description = []; $description = [];
foreach (array_keys($this->properties) as $property) { foreach ($this->properties as $property => $strategy) {
$description[(string)$property] = [ $description[(string)$property] = [
'property' => $property, 'property' => $property,
'type' => Type::BUILTIN_TYPE_STRING, 'type' => Type::BUILTIN_TYPE_STRING,
'required' => false, 'required' => false,
'description' => 'Filter using a LIKE SQL expression. Use % as wildcard for multiple characters and _ for single characters. For example, to search for all items containing foo, use foo. To search for all items starting with foo, use foo%. To search for all items ending with foo, use %foo', 'description' => 'Filter using a LIKE SQL expression. Use % as wildcard for multiple characters and _ for single characters. For example, to search for all items containing foo, use foo. To search for all items starting with foo, use foo%. To search for all items ending with foo, use %foo',
'openapi' => [
'example' => '',
'allowReserved' => false,// if true, query parameters will be not percent-encoded
'allowEmptyValue' => true,
'explode' => false, // to be true, the type must be Type::BUILTIN_TYPE_ARRAY, ?product=blue,green will be ?product=blue&product=green
],
]; ];
} }
return $description; return $description;

View file

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

View file

@ -1,96 +0,0 @@
<?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\ApiPlatform\Filter;
use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Metadata\Operation;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\PropertyInfo\Type;
/**
* Due to their nature, tags are stored in a single string, separated by commas, which requires some more complex search logic.
* This filter allows to easily search for tags in a part entity.
*/
final class TagFilter extends AbstractFilter
{
protected function filterProperty(
string $property,
$value,
QueryBuilder $queryBuilder,
QueryNameGeneratorInterface $queryNameGenerator,
string $resourceClass,
?Operation $operation = null,
array $context = []
): void {
// Ignore filter if property is not enabled or mapped
if (
!$this->isPropertyEnabled($property, $resourceClass) ||
!$this->isPropertyMapped($property, $resourceClass)
) {
return;
}
//Escape any %, _ or \ in the tag
$value = addcslashes($value, '%_\\');
$tag_identifier_prefix = $queryNameGenerator->generateParameterName($property);
$expr = $queryBuilder->expr();
$tmp = $expr->orX(
'ILIKE(o.'.$property.', :' . $tag_identifier_prefix . '_1) = TRUE',
'ILIKE(o.'.$property.', :' . $tag_identifier_prefix . '_2) = TRUE',
'ILIKE(o.'.$property.', :' . $tag_identifier_prefix . '_3) = TRUE',
'ILIKE(o.'.$property.', :' . $tag_identifier_prefix . '_4) = TRUE',
);
$queryBuilder->andWhere($tmp);
//Set the parameters for the LIKE expression, in each variation of the tag (so with a comma, at the end, at the beginning, and on both ends, and equaling the tag)
$queryBuilder->setParameter($tag_identifier_prefix . '_1', '%,' . $value . ',%');
$queryBuilder->setParameter($tag_identifier_prefix . '_2', '%,' . $value);
$queryBuilder->setParameter($tag_identifier_prefix . '_3', $value . ',%');
$queryBuilder->setParameter($tag_identifier_prefix . '_4', $value);
}
public function getDescription(string $resourceClass): array
{
if (!$this->properties) {
return [];
}
$description = [];
foreach (array_keys($this->properties) as $property) {
$description[(string)$property] = [
'property' => $property,
'type' => Type::BUILTIN_TYPE_STRING,
'required' => false,
'description' => 'Filter for tags of a part',
];
}
return $description;
}
}

View file

@ -46,7 +46,7 @@ final class HandleAttachmentsUploadsProcessor implements ProcessorInterface
} }
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
{ {
if ($operation instanceof DeleteOperationInterface) { if ($operation instanceof DeleteOperationInterface) {
return $this->removeProcessor->process($data, $operation, $uriVariables, $context); return $this->removeProcessor->process($data, $operation, $uriVariables, $context);

View file

@ -1,77 +0,0 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2025 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\ApiPlatform;
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use ApiPlatform\Metadata\Property\PropertyNameCollection;
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
use function Symfony\Component\String\u;
/**
* This decorator removes all camelCase property names from the property name collection, if a snake_case version exists.
* This is a fix for https://github.com/Part-DB/Part-DB-server/issues/862, as the openapi schema generator wrongly collects
* both camelCase and snake_case property names, which leads to duplicate properties in the schema.
* This seems to come from the fact that the openapi schema generator uses no serializerContext, which seems then to collect
* the getters too...
*/
#[AsDecorator('api_platform.metadata.property.name_collection_factory')]
class NormalizePropertyNameCollectionFactory implements PropertyNameCollectionFactoryInterface
{
public function __construct(private readonly PropertyNameCollectionFactoryInterface $decorated)
{
}
public function create(string $resourceClass, array $options = []): PropertyNameCollection
{
// Get the default properties from the decorated service
$propertyNames = $this->decorated->create($resourceClass, $options);
//Only become active in the context of the openapi schema generation
if (!isset($options['schema_type'])) {
return $propertyNames;
}
//If we are not in the jsonapi generator (which sets no serializer groups), return the property names as is
if (isset($options['serializer_groups'])) {
return $propertyNames;
}
//Remove all camelCase property names from the collection, if a snake_case version exists
$properties = iterator_to_array($propertyNames);
foreach ($properties as $property) {
if (str_contains($property, '_')) {
$camelized = u($property)->camel()->toString();
//If the camelized version exists, remove it from the collection
$index = array_search($camelized, $properties, true);
if ($index !== false) {
unset($properties[$index]);
}
}
}
return new PropertyNameCollection($properties);
}
}

View file

@ -52,7 +52,6 @@ class BackupCommand extends Command
$backup_attachments = $input->getOption('attachments'); $backup_attachments = $input->getOption('attachments');
$backup_config = $input->getOption('config'); $backup_config = $input->getOption('config');
$backup_full = $input->getOption('full'); $backup_full = $input->getOption('full');
$overwrite = $input->getOption('overwrite');
if ($backup_full) { if ($backup_full) {
$backup_database = true; $backup_database = true;
@ -71,9 +70,7 @@ class BackupCommand extends Command
//Check if the file already exists //Check if the file already exists
//Then ask the user, if he wants to overwrite the file //Then ask the user, if he wants to overwrite the file
if (!$overwrite if (file_exists($output_filepath) && !$io->confirm('The file '.realpath($output_filepath).' already exists. Do you want to overwrite it?', false)) {
&& file_exists($output_filepath)
&& !$io->confirm('The file '.realpath($output_filepath).' already exists. Do you want to overwrite it?', false)) {
$io->error('Backup aborted!'); $io->error('Backup aborted!');
return Command::FAILURE; return Command::FAILURE;
} }

View file

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

View file

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

View file

@ -83,19 +83,6 @@ class SetPasswordCommand extends Command
while (!$success) { while (!$success) {
$pw1 = $io->askHidden('Please enter new password:'); $pw1 = $io->askHidden('Please enter new password:');
if ($pw1 === null) {
$io->error('No password entered! Please try again.');
//If we are in non-interactive mode, we can not ask again
if (!$input->isInteractive()) {
$io->warning('Non-interactive mode detected. No password can be entered that way! If you are using docker exec, please use -it flag.');
return Command::FAILURE;
}
continue;
}
$pw2 = $io->askHidden('Please confirm:'); $pw2 = $io->askHidden('Please confirm:');
if ($pw1 !== $pw2) { if ($pw1 !== $pw2) {
$io->error('The entered password did not match! Please try again.'); $io->error('The entered password did not match! Please try again.');

View file

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

View file

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

View file

@ -35,7 +35,6 @@ use App\Entity\LabelSystem\LabelProcessMode;
use App\Entity\LabelSystem\LabelProfile; use App\Entity\LabelSystem\LabelProfile;
use App\Entity\Parameters\AbstractParameter; use App\Entity\Parameters\AbstractParameter;
use App\Exceptions\AttachmentDownloadException; use App\Exceptions\AttachmentDownloadException;
use App\Exceptions\TwigModeException;
use App\Form\AdminPages\ImportType; use App\Form\AdminPages\ImportType;
use App\Form\AdminPages\MassCreationForm; use App\Form\AdminPages\MassCreationForm;
use App\Repository\AbstractPartsContainingRepository; use App\Repository\AbstractPartsContainingRepository;
@ -53,8 +52,8 @@ use Doctrine\ORM\EntityManagerInterface;
use InvalidArgumentException; use InvalidArgumentException;
use Omines\DataTablesBundle\DataTableFactory; use Omines\DataTablesBundle\DataTableFactory;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\RedirectResponse;
@ -62,8 +61,6 @@ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Serializer\Exception\UnexpectedValueException; use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Validator\ConstraintViolationInterface;
use Symfony\Component\Validator\ConstraintViolationListInterface;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
use function Symfony\Component\Translation\t; use function Symfony\Component\Translation\t;
@ -77,10 +74,15 @@ abstract class BaseAdminController extends AbstractController
protected string $attachment_class = ''; protected string $attachment_class = '';
protected ?string $parameter_class = ''; protected ?string $parameter_class = '';
/**
* @var EventDispatcher|EventDispatcherInterface
*/
protected EventDispatcher|EventDispatcherInterface $eventDispatcher;
public function __construct(protected TranslatorInterface $translator, protected UserPasswordHasherInterface $passwordEncoder, public function __construct(protected TranslatorInterface $translator, protected UserPasswordHasherInterface $passwordEncoder,
protected AttachmentSubmitHandler $attachmentSubmitHandler, protected AttachmentSubmitHandler $attachmentSubmitHandler,
protected EventCommentHelper $commentHelper, protected HistoryHelper $historyHelper, protected TimeTravel $timeTravel, protected EventCommentHelper $commentHelper, protected HistoryHelper $historyHelper, protected TimeTravel $timeTravel,
protected DataTableFactory $dataTableFactory, protected EventDispatcherInterface $eventDispatcher, protected LabelExampleElementsGenerator $barcodeExampleGenerator, protected DataTableFactory $dataTableFactory, EventDispatcherInterface $eventDispatcher, protected LabelExampleElementsGenerator $barcodeExampleGenerator,
protected LabelGenerator $labelGenerator, protected EntityManagerInterface $entityManager) protected LabelGenerator $labelGenerator, protected EntityManagerInterface $entityManager)
{ {
if ('' === $this->entity_class || '' === $this->form_class || '' === $this->twig_template || '' === $this->route_base) { if ('' === $this->entity_class || '' === $this->form_class || '' === $this->twig_template || '' === $this->route_base) {
@ -94,6 +96,7 @@ abstract class BaseAdminController extends AbstractController
if ('' === $this->parameter_class || ($this->parameter_class && !is_a($this->parameter_class, AbstractParameter::class, true))) { if ('' === $this->parameter_class || ($this->parameter_class && !is_a($this->parameter_class, AbstractParameter::class, true))) {
throw new InvalidArgumentException('You have to override the $parameter_class value with a valid Parameter class in your subclass!'); throw new InvalidArgumentException('You have to override the $parameter_class value with a valid Parameter class in your subclass!');
} }
$this->eventDispatcher = $eventDispatcher;
} }
protected function revertElementIfNeeded(AbstractDBElement $entity, ?string $timestamp): ?DateTime protected function revertElementIfNeeded(AbstractDBElement $entity, ?string $timestamp): ?DateTime
@ -189,8 +192,10 @@ abstract class BaseAdminController extends AbstractController
} }
//Ensure that the master picture is still part of the attachments //Ensure that the master picture is still part of the attachments
if ($entity instanceof AttachmentContainingDBElement && ($entity->getMasterPictureAttachment() !== null && !$entity->getAttachments()->contains($entity->getMasterPictureAttachment()))) { if ($entity instanceof AttachmentContainingDBElement) {
$entity->setMasterPictureAttachment(null); if ($entity->getMasterPictureAttachment() !== null && !$entity->getAttachments()->contains($entity->getMasterPictureAttachment())) {
$entity->setMasterPictureAttachment(null);
}
} }
$this->commentHelper->setMessage($form['log_comment']->getData()); $this->commentHelper->setMessage($form['log_comment']->getData());
@ -213,14 +218,10 @@ abstract class BaseAdminController extends AbstractController
//Show preview for LabelProfile if needed. //Show preview for LabelProfile if needed.
if ($entity instanceof LabelProfile) { if ($entity instanceof LabelProfile) {
$example = $this->barcodeExampleGenerator->getElement($entity->getOptions()->getSupportedElement()); $example = $this->barcodeExampleGenerator->getElement($entity->getOptions()->getSupportedElement());
$pdf_data = null; $pdf_data = $this->labelGenerator->generateLabel($entity->getOptions(), $example);
try {
$pdf_data = $this->labelGenerator->generateLabel($entity->getOptions(), $example);
} catch (TwigModeException $exception) {
$form->get('options')->get('lines')->addError(new FormError($exception->getSafeMessage()));
}
} }
/** @var AbstractPartsContainingRepository $repo */
$repo = $this->entityManager->getRepository($this->entity_class); $repo = $this->entityManager->getRepository($this->entity_class);
return $this->render($this->twig_template, [ return $this->render($this->twig_template, [
@ -282,8 +283,10 @@ abstract class BaseAdminController extends AbstractController
} }
//Ensure that the master picture is still part of the attachments //Ensure that the master picture is still part of the attachments
if ($new_entity instanceof AttachmentContainingDBElement && ($new_entity->getMasterPictureAttachment() !== null && !$new_entity->getAttachments()->contains($new_entity->getMasterPictureAttachment()))) { if ($new_entity instanceof AttachmentContainingDBElement) {
$new_entity->setMasterPictureAttachment(null); if ($new_entity->getMasterPictureAttachment() !== null && !$new_entity->getAttachments()->contains($new_entity->getMasterPictureAttachment())) {
$new_entity->setMasterPictureAttachment(null);
}
} }
$this->commentHelper->setMessage($form['log_comment']->getData()); $this->commentHelper->setMessage($form['log_comment']->getData());
@ -330,8 +333,8 @@ abstract class BaseAdminController extends AbstractController
try { try {
$errors = $importer->importFileAndPersistToDB($file, $options); $errors = $importer->importFileAndPersistToDB($file, $options);
foreach ($errors as $name => ['violations' => $violations]) { foreach ($errors as $name => $error) {
foreach ($violations as $violation) { foreach ($error as $violation) {
$this->addFlash('error', $name.': '.$violation->getMessage()); $this->addFlash('error', $name.': '.$violation->getMessage());
} }
} }
@ -341,7 +344,6 @@ abstract class BaseAdminController extends AbstractController
} }
} }
ret:
//Mass creation form //Mass creation form
$mass_creation_form = $this->createForm(MassCreationForm::class, ['entity_class' => $this->entity_class]); $mass_creation_form = $this->createForm(MassCreationForm::class, ['entity_class' => $this->entity_class]);
$mass_creation_form->handleRequest($request); $mass_creation_form->handleRequest($request);
@ -354,14 +356,11 @@ abstract class BaseAdminController extends AbstractController
$results = $importer->massCreation($data['lines'], $this->entity_class, $data['parent'] ?? null, $errors); $results = $importer->massCreation($data['lines'], $this->entity_class, $data['parent'] ?? null, $errors);
//Show errors to user: //Show errors to user:
foreach ($errors as ['entity' => $new_entity, 'violations' => $violations]) { foreach ($errors as $error) {
/** @var ConstraintViolationInterface $violation */ if ($error['entity'] instanceof AbstractStructuralDBElement) {
foreach ($violations as $violation) { $this->addFlash('error', $error['entity']->getFullPath().':'.$error['violations']);
if ($new_entity instanceof AbstractStructuralDBElement) { } else { //When we don't have a structural element, we can only show the name
$this->addFlash('error', $new_entity->getFullPath().':'.$violation->getMessage()); $this->addFlash('error', $error['entity']->getName().':'.$error['violations']);
} else { //When we don't have a structural element, we can only show the name
$this->addFlash('error', $new_entity->getName().':'.$violation->getMessage());
}
} }
} }
@ -372,10 +371,11 @@ abstract class BaseAdminController extends AbstractController
$em->flush(); $em->flush();
if (count($results) > 0) { if (count($results) > 0) {
$this->addFlash('success', t('entity.mass_creation_flash', ['%COUNT%' => count($results)])); $this->addFlash('success', t('entity.mass_creation_flash', ['%COUNT%' => count($results)]));
} }
} }
ret:
return $this->render($this->twig_template, [ return $this->render($this->twig_template, [
'entity' => $new_entity, 'entity' => $new_entity,
'form' => $form, 'form' => $form,
@ -396,7 +396,7 @@ abstract class BaseAdminController extends AbstractController
{ {
if ($entity instanceof AbstractPartsContainingDBElement) { if ($entity instanceof AbstractPartsContainingDBElement) {
/** @var AbstractPartsContainingRepository $repo */ /** @var AbstractPartsContainingRepository $repo */
$repo = $this->entityManager->getRepository($this->entity_class); //@phpstan-ignore-line $repo = $this->entityManager->getRepository($this->entity_class);
if ($repo->getPartsCount($entity) > 0) { if ($repo->getPartsCount($entity) > 0) {
$this->addFlash('error', t('entity.delete.must_not_contain_parts', ['%PATH%' => $entity->getFullPath()])); $this->addFlash('error', t('entity.delete.must_not_contain_parts', ['%PATH%' => $entity->getFullPath()]));
@ -467,11 +467,6 @@ abstract class BaseAdminController extends AbstractController
$this->denyAccessUnlessGranted('read', $entity); $this->denyAccessUnlessGranted('read', $entity);
$entities = $em->getRepository($this->entity_class)->findAll(); $entities = $em->getRepository($this->entity_class)->findAll();
if (count($entities) === 0) {
$this->addFlash('error', 'entity.export.flash.error.no_entities');
return $this->redirectToRoute($this->route_base.'_new');
}
return $exporter->exportEntityFromRequest($entities, $request); return $exporter->exportEntityFromRequest($entities, $request);
} }

View file

@ -51,15 +51,15 @@ class AttachmentFileController extends AbstractController
$this->denyAccessUnlessGranted('show_private', $attachment); $this->denyAccessUnlessGranted('show_private', $attachment);
} }
if (!$attachment->hasInternal()) { if ($attachment->isExternal()) {
throw $this->createNotFoundException('The file for this attachment is external and not stored locally!'); throw new RuntimeException('You can not download external attachments!');
} }
if (!$helper->isInternalFileExisting($attachment)) { if (!$helper->isFileExisting($attachment)) {
throw $this->createNotFoundException('The file associated with the attachment is not existing!'); throw new RuntimeException('The file associated with the attachment is not existing!');
} }
$file_path = $helper->toAbsoluteInternalFilePath($attachment); $file_path = $helper->toAbsoluteFilePath($attachment);
$response = new BinaryFileResponse($file_path); $response = new BinaryFileResponse($file_path);
//Set header content disposition, so that the file will be downloaded //Set header content disposition, so that the file will be downloaded
@ -80,15 +80,15 @@ class AttachmentFileController extends AbstractController
$this->denyAccessUnlessGranted('show_private', $attachment); $this->denyAccessUnlessGranted('show_private', $attachment);
} }
if (!$attachment->hasInternal()) { if ($attachment->isExternal()) {
throw $this->createNotFoundException('The file for this attachment is external and not stored locally!'); throw new RuntimeException('You can not download external attachments!');
} }
if (!$helper->isInternalFileExisting($attachment)) { if (!$helper->isFileExisting($attachment)) {
throw $this->createNotFoundException('The file associated with the attachment is not existing!'); throw new RuntimeException('The file associated with the attachment is not existing!');
} }
$file_path = $helper->toAbsoluteInternalFilePath($attachment); $file_path = $helper->toAbsoluteFilePath($attachment);
$response = new BinaryFileResponse($file_path); $response = new BinaryFileResponse($file_path);
//Set header content disposition, so that the file will be downloaded //Set header content disposition, so that the file will be downloaded

View file

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

View file

@ -30,9 +30,6 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
/**
* @see \App\Tests\Controller\KiCadApiControllerTest
*/
#[Route('/kicad-api/v1')] #[Route('/kicad-api/v1')]
class KiCadApiController extends AbstractController class KiCadApiController extends AbstractController
{ {
@ -65,7 +62,7 @@ class KiCadApiController extends AbstractController
#[Route('/parts/category/{category}.json', name: 'kicad_api_category')] #[Route('/parts/category/{category}.json', name: 'kicad_api_category')]
public function categoryParts(?Category $category): Response public function categoryParts(?Category $category): Response
{ {
if ($category !== null) { if ($category) {
$this->denyAccessUnlessGranted('read', $category); $this->denyAccessUnlessGranted('read', $category);
} else { } else {
$this->denyAccessUnlessGranted('@categories.read'); $this->denyAccessUnlessGranted('@categories.read');

View file

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

View file

@ -151,7 +151,7 @@ class LogController extends AbstractController
if (EventUndoMode::UNDO === $mode) { if (EventUndoMode::UNDO === $mode) {
$this->undoLog($log_element); $this->undoLog($log_element);
} else { } elseif (EventUndoMode::REVERT === $mode) {
$this->revertLog($log_element); $this->revertLog($log_element);
} }

View file

@ -51,7 +51,7 @@ class OAuthClientController extends AbstractController
} }
#[Route('/{name}/check', name: 'oauth_client_check')] #[Route('/{name}/check', name: 'oauth_client_check')]
public function check(string $name): Response public function check(string $name, Request $request): Response
{ {
$this->denyAccessUnlessGranted('@system.manage_oauth_tokens'); $this->denyAccessUnlessGranted('@system.manage_oauth_tokens');

View file

@ -229,10 +229,6 @@ class PartController extends AbstractController
$dto = $infoRetriever->getDetails($providerKey, $providerId); $dto = $infoRetriever->getDetails($providerKey, $providerId);
$new_part = $infoRetriever->dtoToPart($dto); $new_part = $infoRetriever->dtoToPart($dto);
if ($new_part->getCategory() === null || $new_part->getCategory()->getID() === null) {
$this->addFlash('warning', t("part.create_from_info_provider.no_category_yet"));
}
return $this->renderPartForm('new', $request, $new_part, [ return $this->renderPartForm('new', $request, $new_part, [
'info_provider_dto' => $dto, 'info_provider_dto' => $dto,
]); ]);
@ -333,7 +329,7 @@ class PartController extends AbstractController
$this->em->flush(); $this->em->flush();
if ($mode === 'new') { if ($mode === 'new') {
$this->addFlash('success', 'part.created_flash'); $this->addFlash('success', 'part.created_flash');
} elseif ($mode === 'edit') { } else if ($mode === 'edit') {
$this->addFlash('success', 'part.edited_flash'); $this->addFlash('success', 'part.edited_flash');
} }
@ -362,11 +358,11 @@ class PartController extends AbstractController
$template = ''; $template = '';
if ($mode === 'new') { if ($mode === 'new') {
$template = 'parts/edit/new_part.html.twig'; $template = 'parts/edit/new_part.html.twig';
} elseif ($mode === 'edit') { } else if ($mode === 'edit') {
$template = 'parts/edit/edit_part_info.html.twig'; $template = 'parts/edit/edit_part_info.html.twig';
} elseif ($mode === 'merge') { } else if ($mode === 'merge') {
$template = 'parts/edit/merge_parts.html.twig'; $template = 'parts/edit/merge_parts.html.twig';
} elseif ($mode === 'update_from_ip') { } else if ($mode === 'update_from_ip') {
$template = 'parts/edit/update_from_ip.html.twig'; $template = 'parts/edit/update_from_ip.html.twig';
} }

View file

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

View file

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

View file

@ -219,7 +219,7 @@ class ProjectController extends AbstractController
'project' => $project, 'project' => $project,
'part' => $part 'part' => $part
]); ]);
if ($bom_entry !== null) { if ($bom_entry) {
$preset_data->add($bom_entry); $preset_data->add($bom_entry);
} else { //Otherwise create an empty one } else { //Otherwise create an empty one
$entry = new ProjectBOMEntry(); $entry = new ProjectBOMEntry();

View file

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

View file

@ -25,14 +25,12 @@ namespace App\Controller;
use App\Services\Attachments\AttachmentSubmitHandler; use App\Services\Attachments\AttachmentSubmitHandler;
use App\Services\Attachments\AttachmentURLGenerator; use App\Services\Attachments\AttachmentURLGenerator;
use App\Services\Attachments\BuiltinAttachmentsFinder; use App\Services\Attachments\BuiltinAttachmentsFinder;
use App\Services\Doctrine\DBInfoHelper;
use App\Services\Doctrine\NatsortDebugHelper;
use App\Services\Misc\GitVersionInfo; use App\Services\Misc\GitVersionInfo;
use App\Services\Misc\DBInfoHelper;
use App\Services\System\UpdateAvailableManager; use App\Services\System\UpdateAvailableManager;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Runtime\SymfonyRuntime;
#[Route(path: '/tools')] #[Route(path: '/tools')]
class ToolsController extends AbstractController class ToolsController extends AbstractController
@ -46,7 +44,7 @@ class ToolsController extends AbstractController
} }
#[Route(path: '/server_infos', name: 'tools_server_infos')] #[Route(path: '/server_infos', name: 'tools_server_infos')]
public function systemInfos(GitVersionInfo $versionInfo, DBInfoHelper $DBInfoHelper, NatsortDebugHelper $natsortDebugHelper, public function systemInfos(GitVersionInfo $versionInfo, DBInfoHelper $DBInfoHelper,
AttachmentSubmitHandler $attachmentSubmitHandler, UpdateAvailableManager $updateAvailableManager): Response AttachmentSubmitHandler $attachmentSubmitHandler, UpdateAvailableManager $updateAvailableManager): Response
{ {
$this->denyAccessUnlessGranted('@system.server_infos'); $this->denyAccessUnlessGranted('@system.server_infos');
@ -61,10 +59,10 @@ class ToolsController extends AbstractController
'default_theme' => $this->getParameter('partdb.global_theme'), 'default_theme' => $this->getParameter('partdb.global_theme'),
'enabled_locales' => $this->getParameter('partdb.locale_menu'), 'enabled_locales' => $this->getParameter('partdb.locale_menu'),
'demo_mode' => $this->getParameter('partdb.demo_mode'), 'demo_mode' => $this->getParameter('partdb.demo_mode'),
'gdpr_compliance' => $this->getParameter('partdb.gdpr_compliance'), 'gpdr_compliance' => $this->getParameter('partdb.gdpr_compliance'),
'use_gravatar' => $this->getParameter('partdb.users.use_gravatar'), 'use_gravatar' => $this->getParameter('partdb.users.use_gravatar'),
'email_password_reset' => $this->getParameter('partdb.users.email_pw_reset'), 'email_password_reset' => $this->getParameter('partdb.users.email_pw_reset'),
'environment' => $this->getParameter('kernel.environment'), 'enviroment' => $this->getParameter('kernel.environment'),
'is_debug' => $this->getParameter('kernel.debug'), 'is_debug' => $this->getParameter('kernel.debug'),
'email_sender' => $this->getParameter('partdb.mail.sender_email'), 'email_sender' => $this->getParameter('partdb.mail.sender_email'),
'email_sender_name' => $this->getParameter('partdb.mail.sender_name'), 'email_sender_name' => $this->getParameter('partdb.mail.sender_name'),
@ -86,7 +84,7 @@ class ToolsController extends AbstractController
'php_post_max_size' => ini_get('post_max_size'), 'php_post_max_size' => ini_get('post_max_size'),
'kernel_runtime_environment' => $this->getParameter('kernel.runtime_environment'), 'kernel_runtime_environment' => $this->getParameter('kernel.runtime_environment'),
'kernel_runtime_mode' => $this->getParameter('kernel.runtime_mode'), 'kernel_runtime_mode' => $this->getParameter('kernel.runtime_mode'),
'kernel_runtime' => $_SERVER['APP_RUNTIME'] ?? $_ENV['APP_RUNTIME'] ?? SymfonyRuntime::class, 'kernel_runtime' => $_SERVER['APP_RUNTIME'] ?? $_ENV['APP_RUNTIME'] ?? 'Symfony\\Component\\Runtime\\SymfonyRuntime',
//DB section //DB section
'db_type' => $DBInfoHelper->getDatabaseType() ?? 'Unknown', 'db_type' => $DBInfoHelper->getDatabaseType() ?? 'Unknown',
@ -94,8 +92,6 @@ class ToolsController extends AbstractController
'db_size' => $DBInfoHelper->getDatabaseSize(), 'db_size' => $DBInfoHelper->getDatabaseSize(),
'db_name' => $DBInfoHelper->getDatabaseName() ?? 'Unknown', 'db_name' => $DBInfoHelper->getDatabaseName() ?? 'Unknown',
'db_user' => $DBInfoHelper->getDatabaseUsername() ?? 'Unknown', 'db_user' => $DBInfoHelper->getDatabaseUsername() ?? 'Unknown',
'db_natsort_method' => $natsortDebugHelper->getNaturalSortMethod(),
'db_natsort_slow_allowed' => $natsortDebugHelper->isSlowNaturalSortAllowed(),
//New version section //New version section
'new_version_available' => $updateAvailableManager->isUpdateAvailable(), 'new_version_available' => $updateAvailableManager->isUpdateAvailable(),

View file

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

View file

@ -38,6 +38,7 @@ use Doctrine\ORM\EntityManagerInterface;
use RuntimeException; use RuntimeException;
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Google\GoogleAuthenticatorInterface; use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Google\GoogleAuthenticatorInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType; use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\Extension\Core\Type\EnumType; use Symfony\Component\Form\Extension\Core\Type\EnumType;
@ -58,8 +59,11 @@ use Symfony\Component\Validator\Constraints\Length;
#[Route(path: '/user')] #[Route(path: '/user')]
class UserSettingsController extends AbstractController class UserSettingsController extends AbstractController
{ {
public function __construct(protected bool $demo_mode, protected EventDispatcherInterface $eventDispatcher) protected EventDispatcher|EventDispatcherInterface $eventDispatcher;
public function __construct(protected bool $demo_mode, EventDispatcherInterface $eventDispatcher)
{ {
$this->eventDispatcher = $eventDispatcher;
} }
#[Route(path: '/2fa_backup_codes', name: 'show_backup_codes')] #[Route(path: '/2fa_backup_codes', name: 'show_backup_codes')]
@ -240,10 +244,7 @@ class UserSettingsController extends AbstractController
$page_need_reload = true; $page_need_reload = true;
} }
if (!$form instanceof Form) { /** @var Form $form We need a form implementation for the next calls */
throw new RuntimeException('Form is not an instance of Form, so we cannot retrieve the clicked button!');
}
//Remove the avatar attachment from the user if requested //Remove the avatar attachment from the user if requested
if ($form->getClickedButton() && 'remove_avatar' === $form->getClickedButton()->getName() && $user->getMasterPictureAttachment() instanceof Attachment) { if ($form->getClickedButton() && 'remove_avatar' === $form->getClickedButton()->getName() && $user->getMasterPictureAttachment() instanceof Attachment) {
$em->remove($user->getMasterPictureAttachment()); $em->remove($user->getMasterPictureAttachment());

View file

@ -41,7 +41,7 @@ class APITokenFixtures extends Fixture implements DependentFixtureInterface
public function load(ObjectManager $manager): void public function load(ObjectManager $manager): void
{ {
/** @var User $admin_user */ /** @var User $admin_user */
$admin_user = $this->getReference(UserFixtures::ADMIN, User::class); $admin_user = $this->getReference(UserFixtures::ADMIN);
$read_only_token = new ApiToken(); $read_only_token = new ApiToken();
$read_only_token->setUser($admin_user); $read_only_token->setUser($admin_user);
@ -75,7 +75,7 @@ class APITokenFixtures extends Fixture implements DependentFixtureInterface
$expired_token->setUser($admin_user); $expired_token->setUser($admin_user);
$expired_token->setLevel(ApiTokenLevel::FULL); $expired_token->setLevel(ApiTokenLevel::FULL);
$expired_token->setName('expired'); $expired_token->setName('expired');
$expired_token->setValidUntil(new \DateTimeImmutable('-1 day')); $expired_token->setValidUntil(new \DateTime('-1 day'));
$this->setTokenSecret($expired_token, self::TOKEN_EXPIRED); $this->setTokenSecret($expired_token, self::TOKEN_EXPIRED);
$manager->persist($expired_token); $manager->persist($expired_token);

View file

@ -74,37 +74,30 @@ class DataStructureFixtures extends Fixture implements DependentFixtureInterface
/** @var AbstractStructuralDBElement $node1 */ /** @var AbstractStructuralDBElement $node1 */
$node1 = new $class(); $node1 = new $class();
$node1->setName('Node 1'); $node1->setName('Node 1');
$this->addReference($class . '_1', $node1);
/** @var AbstractStructuralDBElement $node2 */ /** @var AbstractStructuralDBElement $node2 */
$node2 = new $class(); $node2 = new $class();
$node2->setName('Node 2'); $node2->setName('Node 2');
$this->addReference($class . '_2', $node2);
/** @var AbstractStructuralDBElement $node3 */ /** @var AbstractStructuralDBElement $node3 */
$node3 = new $class(); $node3 = new $class();
$node3->setName('Node 3'); $node3->setName('Node 3');
$this->addReference($class . '_3', $node3);
$node1_1 = new $class(); $node1_1 = new $class();
$node1_1->setName('Node 1.1'); $node1_1->setName('Node 1.1');
$node1_1->setParent($node1); $node1_1->setParent($node1);
$this->addReference($class . '_4', $node1_1);
$node1_2 = new $class(); $node1_2 = new $class();
$node1_2->setName('Node 1.2'); $node1_2->setName('Node 1.2');
$node1_2->setParent($node1); $node1_2->setParent($node1);
$this->addReference($class . '_5', $node1_2);
$node2_1 = new $class(); $node2_1 = new $class();
$node2_1->setName('Node 2.1'); $node2_1->setName('Node 2.1');
$node2_1->setParent($node2); $node2_1->setParent($node2);
$this->addReference($class . '_6', $node2_1);
$node1_1_1 = new $class(); $node1_1_1 = new $class();
$node1_1_1->setName('Node 1.1.1'); $node1_1_1->setName('Node 1.1.1');
$node1_1_1->setParent($node1_1); $node1_1_1->setParent($node1_1);
$this->addReference($class . '_7', $node1_1_1);
$manager->persist($node1); $manager->persist($node1);
$manager->persist($node2); $manager->persist($node2);

View file

@ -1,106 +0,0 @@
<?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\DataFixtures;
use App\Entity\LogSystem\ElementCreatedLogEntry;
use App\Entity\LogSystem\ElementDeletedLogEntry;
use App\Entity\LogSystem\ElementEditedLogEntry;
use App\Entity\Parts\Category;
use App\Entity\UserSystem\User;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
use Doctrine\Persistence\ObjectManager;
class LogEntryFixtures extends Fixture implements DependentFixtureInterface
{
public function load(ObjectManager $manager): void
{
$this->createCategoryEntries($manager);
$this->createDeletedCategory($manager);
}
public function createCategoryEntries(ObjectManager $manager): void
{
$category = $this->getReference(Category::class . '_1', Category::class);
$logEntry = new ElementCreatedLogEntry($category);
$logEntry->setTimestamp(new \DateTimeImmutable("+1 second"));
$logEntry->setUser($this->getReference(UserFixtures::ADMIN, User::class));
$logEntry->setComment('Test');
$manager->persist($logEntry);
$logEntry = new ElementEditedLogEntry($category);
$logEntry->setTimestamp(new \DateTimeImmutable("+2 second"));
$logEntry->setUser($this->getReference(UserFixtures::ADMIN, User::class));
$logEntry->setComment('Test');
$logEntry->setOldData(['name' => 'Test']);
$logEntry->setNewData(['name' => 'Node 1.1']);
$manager->persist($logEntry);
$manager->flush();
}
public function createDeletedCategory(ObjectManager $manager): void
{
//We create a fictive category to test the deletion
$category = new Category();
$category->setName('Node 100');
//Assume a category with id 100 was deleted
$reflClass = new \ReflectionClass($category);
$reflClass->getProperty('id')->setValue($category, 100);
//The whole lifecycle from creation to deletion
$logEntry = new ElementCreatedLogEntry($category);
$logEntry->setUser($this->getReference(UserFixtures::ADMIN, User::class));
$logEntry->setComment('Creation');
$manager->persist($logEntry);
$logEntry = new ElementEditedLogEntry($category);
$logEntry->setTimestamp(new \DateTimeImmutable("+1 second"));
$logEntry->setUser($this->getReference(UserFixtures::ADMIN, User::class));
$logEntry->setComment('Edit');
$logEntry->setOldData(['name' => 'Test']);
$logEntry->setNewData(['name' => 'Node 100']);
$manager->persist($logEntry);
$logEntry = new ElementDeletedLogEntry($category);
$logEntry->setTimestamp(new \DateTimeImmutable("+2 second"));
$logEntry->setUser($this->getReference(UserFixtures::ADMIN, User::class));
$logEntry->setOldData(['name' => 'Node 100', 'id' => 100, 'comment' => 'Test comment']);
$manager->persist($logEntry);
$manager->flush();
}
public function getDependencies(): array
{
return [
UserFixtures::class,
DataStructureFixtures::class
];
}
}

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