mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-02-20 16:52:41 +01:00
Compare commits
120 Commits
v1.16.0
...
settings-b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18bf68cb6a | ||
|
|
38c826713f | ||
|
|
b9c3358f7f | ||
|
|
2f2f4b2b3e | ||
|
|
423cdf6ca2 | ||
|
|
dc81a5b361 | ||
|
|
a4f2a7a7df | ||
|
|
c0c847c5be | ||
|
|
1dbcff66d1 | ||
|
|
6e28f2a74e | ||
|
|
9812671a89 | ||
|
|
8b417d6441 | ||
|
|
ff57b5b270 | ||
|
|
c00edef69c | ||
|
|
a235f05794 | ||
|
|
bd411ba13b | ||
|
|
f8bdbf1fde | ||
|
|
beea572c47 | ||
|
|
442a7aa235 | ||
|
|
2226b72d1c | ||
|
|
00a74ed96a | ||
|
|
699a5c935f | ||
|
|
b2946aee0d | ||
|
|
beb079b825 | ||
|
|
64497c4c0d | ||
|
|
70bbb47850 | ||
|
|
73253dd03c | ||
|
|
442457f11b | ||
|
|
c44535990b | ||
|
|
b8d5b83eee | ||
|
|
00da2dedc3 | ||
|
|
4ce1de079e | ||
|
|
6b9c125de4 | ||
|
|
2c4f44e808 | ||
|
|
2b694731ad | ||
|
|
7e34535e62 | ||
|
|
0bb831fe88 | ||
|
|
42a32ce142 | ||
|
|
23f58b7bf4 | ||
|
|
4e9101fded | ||
|
|
9c700c77a8 | ||
|
|
cb1f674332 | ||
|
|
6823d94ffb | ||
|
|
60ab992360 | ||
|
|
f9e769a6e3 | ||
|
|
f802c6c176 | ||
|
|
dedadf0c10 | ||
|
|
c8375def1a | ||
|
|
62ebcde2de | ||
|
|
594a5779dc | ||
|
|
c0ef64fb64 | ||
|
|
48c70c3bb4 | ||
|
|
68124a340b | ||
|
|
0b5003fcf6 | ||
|
|
956ece60af | ||
|
|
53da45d7d7 | ||
|
|
57f0432a87 | ||
|
|
fb535ec6f7 | ||
|
|
4e1b1a4ffa | ||
|
|
5b111d80f1 | ||
|
|
03e1105a8e | ||
|
|
059a9683db | ||
|
|
1daf6f01f4 | ||
|
|
d3b225771c | ||
|
|
7275db27e7 | ||
|
|
49ee9131d0 | ||
|
|
e75e0c4c0b | ||
|
|
e94d4a7752 | ||
|
|
95206f427d | ||
|
|
2d7f88522a | ||
|
|
f5c17bc7c8 | ||
|
|
63e222ed40 | ||
|
|
6963ee3b8d | ||
|
|
bb5e42bf63 | ||
|
|
c48f778648 | ||
|
|
616aad6403 | ||
|
|
bcc7547d6f | ||
|
|
5a1a6e9217 | ||
|
|
eae1fcecab | ||
|
|
b53989bb9d | ||
|
|
467d50bd31 | ||
|
|
1935258978 | ||
|
|
3d4e91fc69 | ||
|
|
97aed847b6 | ||
|
|
8750573724 | ||
|
|
1ac9641a04 | ||
|
|
ad47c8d8ed | ||
|
|
0dbf417866 | ||
|
|
a45bf22ac5 | ||
|
|
b5a6ba921c | ||
|
|
f6a2467eae | ||
|
|
79da0518c2 | ||
|
|
5e512f8935 | ||
|
|
99c10ffe85 | ||
|
|
47830dcd08 | ||
|
|
947cce78d7 | ||
|
|
74e555d25d | ||
|
|
e9973af8f4 | ||
|
|
2ab2b7f77d | ||
|
|
463812fb3d | ||
|
|
d2406726c6 | ||
|
|
1f04d1b993 | ||
|
|
2ef46cdd34 | ||
|
|
2bc50b2888 | ||
|
|
9e0f86788d | ||
|
|
2681c7ded3 | ||
|
|
5ab6a63492 | ||
|
|
6df7bc5f2a | ||
|
|
a0a7ca3c9c | ||
|
|
f88584e1ca | ||
|
|
3e657a7cac | ||
|
|
7cc67f8bb1 | ||
|
|
0772d85918 | ||
|
|
26d83af298 | ||
|
|
4876068cce | ||
|
|
08ae313dfe | ||
|
|
3967c53468 | ||
|
|
7ad077862c | ||
|
|
5a563e4f8f | ||
|
|
5a4b7c525b |
@@ -42,6 +42,48 @@ fi
|
||||
# Start PHP-FPM (the PHP_VERSION is replaced by the configured version in the Dockerfile)
|
||||
service phpPHP_VERSION-fpm start
|
||||
|
||||
|
||||
# Run migrations if automigration is enabled via env variable DB_AUTOMIGRATE
|
||||
if [ "$DB_AUTOMIGRATE" = "true" ]; then
|
||||
echo "Waiting for database to be ready..."
|
||||
ATTEMPTS_LEFT_TO_REACH_DATABASE=60
|
||||
until [ $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq 0 ] || DATABASE_ERROR=$(sudo -E -u www-data php bin/console dbal:run-sql -q "SELECT 1" 2>&1); do
|
||||
if [ $? -eq 255 ]; then
|
||||
# If the Doctrine command exits with 255, an unrecoverable error occurred
|
||||
ATTEMPTS_LEFT_TO_REACH_DATABASE=0
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
ATTEMPTS_LEFT_TO_REACH_DATABASE=$((ATTEMPTS_LEFT_TO_REACH_DATABASE - 1))
|
||||
echo "Still waiting for database to be ready... Or maybe the database is not reachable. $ATTEMPTS_LEFT_TO_REACH_DATABASE attempts left."
|
||||
done
|
||||
|
||||
if [ $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq 0 ]; then
|
||||
echo "The database is not up or not reachable:"
|
||||
echo "$DATABASE_ERROR"
|
||||
exit 1
|
||||
else
|
||||
echo "The database is now ready and reachable"
|
||||
fi
|
||||
|
||||
# Check if there are any available migrations to do, by executing doctrine:migrations:up-to-date
|
||||
# and checking if the exit code is 0 (up to date) or 1 (not up to date)
|
||||
if sudo -E -u www-data php bin/console doctrine:migrations:up-to-date --no-interaction; then
|
||||
echo "Database is up to date, no migrations necessary."
|
||||
else
|
||||
echo "Migrations available..."
|
||||
echo "Do backup of database..."
|
||||
|
||||
sudo -E -u www-data mkdir -p /var/www/html/uploads/.automigration-backup/
|
||||
# Backup the database
|
||||
sudo -E -u www-data php bin/console partdb:backup -n --database /var/www/html/uploads/.automigration-backup/backup-$(date +%Y-%m-%d_%H-%M-%S).zip
|
||||
|
||||
# Check if there are any migration files
|
||||
sudo -E -u www-data php bin/console doctrine:migrations:migrate --no-interaction
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
# 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
|
||||
set -- apache2-foreground "$@"
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
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 SHOW_PART_IMAGE_OVERLAY
|
||||
|
||||
# For most configuration files from conf-available/, which are
|
||||
# enabled or disabled at a global level, it is possible to
|
||||
|
||||
182
.env
182
.env
@@ -32,36 +32,16 @@ DATABASE_EMULATE_NATURAL_SORT=0
|
||||
###################################################################################
|
||||
|
||||
# The language to use serverwide as default (en, de, ru, etc.)
|
||||
DEFAULT_LANG="en"
|
||||
#DEFAULT_LANG="en"
|
||||
# The default timezone to use serverwide (e.g. Europe/Berlin)
|
||||
DEFAULT_TIMEZONE="Europe/Berlin"
|
||||
#DEFAULT_TIMEZONE="Europe/Berlin"
|
||||
# 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
|
||||
BASE_CURRENCY="EUR"
|
||||
# The name of this installation. This will be shown as title in the browser and in the header of the website
|
||||
INSTANCE_NAME="Part-DB"
|
||||
# Allow users to download attachments to the server by providing an URL
|
||||
# This could be a potential security issue, as the user can retrieve any file the server has access to (via internet)
|
||||
ALLOW_ATTACHMENT_DOWNLOADS=0
|
||||
# Set this to 1, if the "download external files" checkbox should be checked by default for new attachments
|
||||
ATTACHMENT_DOWNLOAD_BY_DEFAULT=0
|
||||
# Use gravatars for user avatars, when user has no own avatar defined
|
||||
USE_GRAVATAR=0
|
||||
# The maximum allowed size for attachment files in bytes (you can use M for megabytes and G for gigabytes)
|
||||
# Please note that the php.ini setting upload_max_filesize also limits the maximum size of uploaded files
|
||||
MAX_ATTACHMENT_FILE_SIZE="100M"
|
||||
#BASE_CURRENCY="EUR"
|
||||
|
||||
# The public reachable URL of this Part-DB installation. This is used for generating links in SAML and email templates
|
||||
# This must end with a slash!
|
||||
DEFAULT_URI="https://partdb.changeme.invalid/"
|
||||
|
||||
# With this option you can configure, where users are enforced to give a change reason, which will be logged
|
||||
# This is a comma separated list of values, see documentation for available values
|
||||
# Leave this empty, to make all change reasons optional
|
||||
ENFORCE_CHANGE_COMMENTS_FOR=""
|
||||
|
||||
# Disable that if you do not want that Part-DB connects to GitHub to check for available updates, or if your server can not connect to the internet
|
||||
CHECK_FOR_UPDATES=1
|
||||
|
||||
###################################################################################
|
||||
# Email settings
|
||||
###################################################################################
|
||||
@@ -78,21 +58,6 @@ EMAIL_SENDER_NAME="Part-DB Mailer"
|
||||
# Set this to 1 to allow reset of a password per email
|
||||
ALLOW_EMAIL_PW_RESET=0
|
||||
|
||||
######################################################################################
|
||||
# History/Eventlog settings
|
||||
######################################################################################
|
||||
# If you want to use full timetrave functionality all values below have to be set to 1
|
||||
|
||||
# Save which fields were changed in a ElementEdited log entry
|
||||
HISTORY_SAVE_CHANGED_FIELDS=1
|
||||
# Save the old data in the ElementEdited log entry (warning this could increase the database size in short time)
|
||||
HISTORY_SAVE_CHANGED_DATA=1
|
||||
# Save the data of an element that gets removed into log entry. This allows to undelete an element
|
||||
HISTORY_SAVE_REMOVED_DATA=1
|
||||
# Save the new data of an element that gets changed or added. This allows an easy comparison of the old and new data on the detail page
|
||||
# This option only becomes active when HISTORY_SAVE_CHANGED_DATA is set to 1
|
||||
HISTORY_SAVE_NEW_DATA=1
|
||||
|
||||
###################################################################################
|
||||
# Error pages settings
|
||||
###################################################################################
|
||||
@@ -102,140 +67,6 @@ ERROR_PAGE_ADMIN_EMAIL=''
|
||||
# If this is set to true, solutions to common problems are shown on error pages. Disable this, if you do not want your users to see them...
|
||||
ERROR_PAGE_SHOW_HELP=1
|
||||
|
||||
##################################################################################
|
||||
# Part table settings
|
||||
##################################################################################
|
||||
|
||||
# The default page size for the part table (set to -1 to show all parts on one page)
|
||||
TABLE_DEFAULT_PAGE_SIZE=50
|
||||
# Configure which columns will be visible by default in the parts table (and in which order).
|
||||
# This is a comma separated list of column names. See documentation for available values.
|
||||
TABLE_PARTS_DEFAULT_COLUMNS=name,description,category,footprint,manufacturer,storage_location,amount
|
||||
|
||||
##################################################################################
|
||||
# Info provider settings
|
||||
##################################################################################
|
||||
|
||||
# Digikey Provider:
|
||||
# You can get your client id and secret from https://developer.digikey.com/
|
||||
PROVIDER_DIGIKEY_CLIENT_ID=
|
||||
PROVIDER_DIGIKEY_SECRET=
|
||||
# The currency to get prices in
|
||||
PROVIDER_DIGIKEY_CURRENCY=EUR
|
||||
# The language to get results in (en, de, fr, it, es, zh, ja, ko)
|
||||
PROVIDER_DIGIKEY_LANGUAGE=en
|
||||
# The country to get results for
|
||||
PROVIDER_DIGIKEY_COUNTRY=DE
|
||||
|
||||
# Farnell Provider:
|
||||
# You can get your API key from https://partner.element14.com/
|
||||
PROVIDER_ELEMENT14_KEY=
|
||||
# Configure the store domain you want to use. This decides the language and currency of results. You can get a list of available stores from https://partner.element14.com/docs/Product_Search_API_REST__Description
|
||||
PROVIDER_ELEMENT14_STORE_ID=de.farnell.com
|
||||
|
||||
# TME Provider:
|
||||
# You can get your API key from https://developers.tme.eu/en/
|
||||
PROVIDER_TME_KEY=
|
||||
PROVIDER_TME_SECRET=
|
||||
# The currency to get prices in
|
||||
PROVIDER_TME_CURRENCY=EUR
|
||||
# The language to get results in (en, de, pl)
|
||||
PROVIDER_TME_LANGUAGE=en
|
||||
# The country to get results for
|
||||
PROVIDER_TME_COUNTRY=DE
|
||||
# [DEPRECATED] Set this to 1 to get gross prices (including VAT) instead of net prices
|
||||
# With private API keys, this option cannot be used anymore is ignored by Part-DB. The VAT inclusion depends on your TME account settings.
|
||||
PROVIDER_TME_GET_GROSS_PRICES=1
|
||||
|
||||
# Octopart / Nexar Provider:
|
||||
# You can get your API key from https://nexar.com/api
|
||||
PROVIDER_OCTOPART_CLIENT_ID=
|
||||
PROVIDER_OCTOPART_SECRET=
|
||||
# The currency and country to get prices for (you have to set both to get meaningful results)
|
||||
# 3 letter ISO currency code (e.g. EUR, USD, GBP)
|
||||
PROVIDER_OCTOPART_CURRENCY=EUR
|
||||
# 2 letter ISO country code (e.g. DE, US, GB)
|
||||
PROVIDER_OCTOPART_COUNTRY=DE
|
||||
# The number of results to get from Octopart while searching (please note that this counts towards your API limits)
|
||||
PROVIDER_OCTOPART_SEARCH_LIMIT=10
|
||||
# Set to false to include non authorized offers in the results
|
||||
PROVIDER_OCTOPART_ONLY_AUTHORIZED_SELLERS=1
|
||||
|
||||
# Mouser Provider API V2:
|
||||
# You can get your API key from https://www.mouser.it/api-hub/
|
||||
PROVIDER_MOUSER_KEY=
|
||||
# Filter search results by RoHS compliance and stock availability:
|
||||
# Available options: None | Rohs | InStock | RohsAndInStock
|
||||
PROVIDER_MOUSER_SEARCH_OPTION='None'
|
||||
# The number of results to get from Mouser while searching (please note that this value is max 50)
|
||||
PROVIDER_MOUSER_SEARCH_LIMIT=50
|
||||
# It is recommended to leave this set to 'true'. The option is not really good doumented by Mouser:
|
||||
# Used when searching for keywords in the language specified when you signed up for Search API.
|
||||
PROVIDER_MOUSER_SEARCH_WITH_SIGNUP_LANGUAGE='true'
|
||||
|
||||
# LCSC Provider:
|
||||
# LCSC does not provide an offical API, so this used the API LCSC uses to render their webshop.
|
||||
# LCSC did not intended the use of this API and it could break any time, so use it at your own risk.
|
||||
|
||||
# We dont require an API key for LCSC, just set this to 1 to enable LCSC support
|
||||
PROVIDER_LCSC_ENABLED=0
|
||||
# The currency to get prices in (e.g. EUR, USD, etc.)
|
||||
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
|
||||
@@ -243,8 +74,8 @@ PROVIDER_POLLIN_ENABLED=0
|
||||
|
||||
# This value determines the depth of the category tree, that is visible inside KiCad
|
||||
# 0 means that only the top level categories are visible. Set to a value > 0 to show more levels.
|
||||
# Set to -1, to show all parts of Part-DB inside a single category in KiCad
|
||||
EDA_KICAD_CATEGORY_DEPTH=0
|
||||
# Set to -1, to show all parts of Part-DB inside a sigle cnategory in KiCad
|
||||
#EDA_KICAD_CATEGORY_DEPTH=0
|
||||
|
||||
###################################################################################
|
||||
# SAML Single sign on-settings
|
||||
@@ -298,9 +129,6 @@ NO_URL_REWRITE_AVAILABLE=0
|
||||
# Set to 1, if Part-DB should redirect all HTTP requests to HTTPS. You dont need to configure this, if your webserver already does this.
|
||||
REDIRECT_TO_HTTPS=0
|
||||
|
||||
# If you want to use fixer.io for currency conversion, you have to set this to your API key
|
||||
FIXER_API_KEY=CHANGEME
|
||||
|
||||
# Override value if you want to show to show a given text on homepage.
|
||||
# When this is empty the content of config/banner.md is used as banner
|
||||
BANNER=""
|
||||
|
||||
@@ -10,4 +10,6 @@ DATABASE_URL="sqlite:///%kernel.project_dir%/var/app_test.db"
|
||||
#DATABASE_URL=mysql://root:@127.0.0.1:3306/part-db
|
||||
|
||||
# Disable update checks, as tests would fail, when github is not reachable
|
||||
CHECK_FOR_UPDATES=0
|
||||
CHECK_FOR_UPDATES=0
|
||||
|
||||
INSTANCE_NAME="Part-DB"
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,6 +2,7 @@
|
||||
/.env.local
|
||||
/.env.local.php
|
||||
/.env.*.local
|
||||
/.env.local.bak
|
||||
/config/secrets/prod/prod.decrypt.private.php
|
||||
/public/bundles/
|
||||
/var/
|
||||
|
||||
@@ -128,6 +128,8 @@ const PLACEHOLDERS = [
|
||||
['[[BARCODE_QR]]', 'QR code linking to this element'],
|
||||
['[[BARCODE_C128]]', 'Code 128 barcode linking to this element'],
|
||||
['[[BARCODE_C39]]', 'Code 39 barcode linking to this element'],
|
||||
['[[BARCODE_C93]]', 'Code 93 barcode linking to this element'],
|
||||
['[[BARCODE_DATAMATRIX]]', 'Datamatrix code linking to this element'],
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -69,6 +69,8 @@ Object.assign( window.CKEDITOR_TRANSLATIONS[ 'de' ].dictionary, {
|
||||
'QR code linking to this element': 'QR Code verknüpft mit diesem Element',
|
||||
'Code 128 barcode linking to this element': 'Code 128 Barcode verknüpft mit diesem Element',
|
||||
'Code 39 barcode linking to this element': 'Code 39 Barcode verknüpft mit diesem Element',
|
||||
'Code 93 barcode linking to this element': 'Code 93 Barcode verknüpft mit diesem Element',
|
||||
'Datamatrix code linking to this element': 'Datamatrix Code verknüpft mit diesem Element',
|
||||
|
||||
'Location ID': 'Lagerort ID',
|
||||
'Name': 'Name',
|
||||
|
||||
@@ -40,6 +40,7 @@ export default class extends Controller {
|
||||
|
||||
|
||||
let settings = {
|
||||
plugins: ["clear_button"],
|
||||
allowEmptyOption: true,
|
||||
selectOnTab: true,
|
||||
maxOptions: null,
|
||||
@@ -50,7 +51,24 @@ export default class extends Controller {
|
||||
}
|
||||
};
|
||||
|
||||
//Load the drag_drop plugin if the select is ordered
|
||||
if (this.element.dataset.orderedValue) {
|
||||
settings.plugins.push('drag_drop');
|
||||
settings.plugins.push("caret_position");
|
||||
}
|
||||
|
||||
//If multiple items can be selected, enable the remove_button plugin
|
||||
if (this.element.multiple) {
|
||||
settings.plugins.push('remove_button');
|
||||
}
|
||||
|
||||
this._tomSelect = new TomSelect(this.element, settings);
|
||||
|
||||
//If the select is ordered, we need to update the value field (with the decoded value from the orderedValue field)
|
||||
if (this.element.dataset.orderedValue) {
|
||||
const data = JSON.parse(this.element.dataset.orderedValue);
|
||||
this._tomSelect.setValue(data);
|
||||
}
|
||||
}
|
||||
|
||||
getTomSelect() {
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
import {Controller} from "@hotwired/stimulus";
|
||||
import TomSelect from "tom-select";
|
||||
|
||||
// TODO: Merge with select_controller.js
|
||||
|
||||
export default class extends Controller {
|
||||
_tomSelect;
|
||||
|
||||
|
||||
@@ -25,9 +25,23 @@ import "katex/dist/katex.css";
|
||||
export default class extends Controller {
|
||||
static targets = ["input", "preview"];
|
||||
|
||||
static values = {
|
||||
unit: {type: Boolean, default: false} //Render as upstanding (non-italic) text, useful for units
|
||||
}
|
||||
|
||||
updatePreview()
|
||||
{
|
||||
katex.render(this.inputTarget.value, this.previewTarget, {
|
||||
let value = "";
|
||||
if (this.unitValue) {
|
||||
//Escape percentage signs
|
||||
value = this.inputTarget.value.replace(/%/g, '\\%');
|
||||
|
||||
value = "\\mathrm{" + value + "}";
|
||||
} else {
|
||||
value = this.inputTarget.value;
|
||||
}
|
||||
|
||||
katex.render(value, this.previewTarget, {
|
||||
throwOnError: false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -85,7 +85,9 @@ export default class extends Controller
|
||||
tmp += '<span>' + katex.renderToString(data.symbol) + '</span>'
|
||||
}
|
||||
if (data.unit) {
|
||||
tmp += '<span class="ms-2">' + katex.renderToString('[' + data.unit + ']') + '</span>'
|
||||
let unit = data.unit.replace(/%/g, '\\%');
|
||||
unit = "\\mathrm{" + unit + "}";
|
||||
tmp += '<span class="ms-2">' + katex.renderToString('[' + unit + ']') + '</span>'
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -111,4 +111,11 @@ ul.structural_link li a:hover {
|
||||
.permission-checkbox:checked {
|
||||
background-color: var(--bs-success);
|
||||
border-color: var(--bs-success);
|
||||
}
|
||||
|
||||
/***********************************************
|
||||
* Katex rendering with same height as text
|
||||
***********************************************/
|
||||
.katex-same-height-as-text .katex {
|
||||
font-size: 1.0em;
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
"api-platform/core": "^3.1",
|
||||
"beberlei/doctrineextensions": "^1.2",
|
||||
"brick/math": "0.12.1 as 0.11.0",
|
||||
"composer/ca-bundle": "^1.3",
|
||||
"composer/ca-bundle": "^1.5",
|
||||
"composer/package-versions-deprecated": "^1.11.99.5",
|
||||
"doctrine/data-fixtures": "^2.0.0",
|
||||
"doctrine/dbal": "^4.0.0",
|
||||
@@ -30,6 +30,7 @@
|
||||
"hshn/base64-encoded-file": "^5.0",
|
||||
"jbtronics/2fa-webauthn": "^v2.2.0",
|
||||
"jbtronics/dompdf-font-loader-bundle": "^1.0.0",
|
||||
"jbtronics/settings-bundle": "^v2.6.0",
|
||||
"jfcherng/php-diff": "^6.14",
|
||||
"knpuniversity/oauth2-client-bundle": "^2.15",
|
||||
"league/csv": "^9.8.0",
|
||||
@@ -43,6 +44,7 @@
|
||||
"omines/datatables-bundle": "^0.9.1",
|
||||
"paragonie/sodium_compat": "^1.21",
|
||||
"part-db/label-fonts": "^1.0",
|
||||
"rhukster/dom-sanitizer": "^1.0",
|
||||
"runtime/frankenphp-symfony": "^0.2.0",
|
||||
"s9e/text-formatter": "^2.1",
|
||||
"scheb/2fa-backup-code": "^6.8.0",
|
||||
@@ -73,7 +75,7 @@
|
||||
"symfony/runtime": "6.4.*",
|
||||
"symfony/security-bundle": "6.4.*",
|
||||
"symfony/serializer": "6.4.*",
|
||||
"symfony/string": "6.4.*",
|
||||
"symfony/string": "6.4.8",
|
||||
"symfony/translation": "6.4.*",
|
||||
"symfony/twig-bundle": "6.4.*",
|
||||
"symfony/ux-translator": "^2.10",
|
||||
|
||||
2147
composer.lock
generated
2147
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -31,5 +31,6 @@ return [
|
||||
KnpU\OAuth2ClientBundle\KnpUOAuth2ClientBundle::class => ['all' => true],
|
||||
Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
|
||||
ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
|
||||
Jbtronics\SettingsBundle\JbtronicsSettingsBundle::class => ['all' => true],
|
||||
Jbtronics\TranslationEditorBundle\JbtronicsTranslationEditorBundle::class => ['dev' => true],
|
||||
];
|
||||
|
||||
@@ -9,7 +9,8 @@ datatables:
|
||||
# Set options, as documented at https://datatables.net/reference/option/
|
||||
options:
|
||||
lengthMenu : [[10, 25, 50, 100], [10, 25, 50, 100]] # We add the "All" option, when part tables are generated
|
||||
pageLength: '%partdb.table.default_page_size%' # Set to -1 to disable pagination (i.e. show all rows) by default
|
||||
#pageLength: '%partdb.table.default_page_size%' # Set to -1 to disable pagination (i.e. show all rows) by default
|
||||
pageLength: 50 #TODO
|
||||
dom: " <'row' <'col mb-2 input-group flex-nowrap' B l > <'col-auto mb-2' < p >>>
|
||||
<'card'
|
||||
rt
|
||||
|
||||
@@ -6,8 +6,8 @@ knpu_oauth2_client:
|
||||
type: generic
|
||||
provider_class: '\League\OAuth2\Client\Provider\GenericProvider'
|
||||
|
||||
client_id: '%env(PROVIDER_DIGIKEY_CLIENT_ID)%'
|
||||
client_secret: '%env(PROVIDER_DIGIKEY_SECRET)%'
|
||||
client_id: '%env(settings:digikey:clientId)%'
|
||||
client_secret: '%env(settings:digikey:secret)%'
|
||||
|
||||
redirect_route: 'oauth_client_check'
|
||||
redirect_params: {name: 'ip_digikey_oauth'}
|
||||
@@ -26,8 +26,8 @@ knpu_oauth2_client:
|
||||
type: generic
|
||||
provider_class: '\League\OAuth2\Client\Provider\GenericProvider'
|
||||
|
||||
client_id: '%env(PROVIDER_OCTOPART_CLIENT_ID)%'
|
||||
client_secret: '%env(PROVIDER_OCTOPART_SECRET)%'
|
||||
client_id: '%env(settings:octopart:clientId)%'
|
||||
client_secret: '%env(settings:octopart:secret)%'
|
||||
|
||||
redirect_route: 'oauth_client_check'
|
||||
redirect_params: { name: 'ip_octopart_oauth' }
|
||||
|
||||
8
config/packages/settings.yaml
Normal file
8
config/packages/settings.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
jbtronics_settings:
|
||||
default_storage_adapter: Jbtronics\SettingsBundle\Storage\ORMStorageAdapter
|
||||
|
||||
cache:
|
||||
default_cacheable: true
|
||||
|
||||
orm_storage:
|
||||
default_entity_class: App\Entity\SettingsEntry
|
||||
@@ -6,5 +6,5 @@ florianv_swap:
|
||||
providers:
|
||||
european_central_bank: ~ # European Central Bank (only works for EUR base currency)
|
||||
fixer: # Fixer.io (needs an API key)
|
||||
access_key: "%env(FIXER_API_KEY)%"
|
||||
access_key: "%env(string:default:settings:exchange_rate:fixerApiKey:INVALID)%"
|
||||
#exchange_rates_api: ~
|
||||
@@ -1,11 +1,10 @@
|
||||
framework:
|
||||
default_locale: '%partdb.locale%'
|
||||
default_locale: 'en'
|
||||
# Just enable the locales we need for performance reasons.
|
||||
enabled_locale: '%partdb.locale_menu%'
|
||||
translator:
|
||||
default_path: '%kernel.project_dir%/translations'
|
||||
fallbacks:
|
||||
- '%partdb.locale%'
|
||||
- 'en'
|
||||
providers:
|
||||
# crowdin:
|
||||
|
||||
@@ -6,16 +6,12 @@ twig:
|
||||
'%kernel.project_dir%/assets/css': css
|
||||
|
||||
globals:
|
||||
partdb_title: '%partdb.title%'
|
||||
default_currency: '%partdb.default_currency%'
|
||||
global_theme: '%partdb.global_theme%'
|
||||
allow_email_pw_reset: '%partdb.users.email_pw_reset%'
|
||||
locale_menu: '%partdb.locale_menu%'
|
||||
attachment_manager: '@App\Services\Attachments\AttachmentManager'
|
||||
label_profile_dropdown_helper: '@App\Services\LabelSystem\LabelProfileDropdownHelper'
|
||||
error_page_admin_email: '%partdb.error_pages.admin_email%'
|
||||
error_page_show_help: '%partdb.error_pages.show_help%'
|
||||
sidebar_items: '%partdb.sidebar.items%'
|
||||
sidebar_tree_updater: '@App\Services\Trees\SidebarTreeUpdater'
|
||||
avatar_helper: '@App\Services\UserSystem\UserAvatarHelper'
|
||||
available_themes: '%partdb.available_themes%'
|
||||
|
||||
@@ -5,14 +5,10 @@ parameters:
|
||||
######################################################################################################################
|
||||
# Common
|
||||
######################################################################################################################
|
||||
partdb.locale: '%env(string:DEFAULT_LANG)%' # The default language to use serverwide
|
||||
partdb.timezone: '%env(string:DEFAULT_TIMEZONE)%' # The default timezone
|
||||
partdb.title: '%env(trim:string:INSTANCE_NAME)%' # The title shown inside of Part-DB (e.g. in the navbar and on homepage)
|
||||
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.global_theme: '' # The theme to use globally (see public/build/themes/ for choices, use name without .css). Set to '' for default bootstrap theme
|
||||
|
||||
# This is used as workaround for places where we can not access the settings directly (like the 2FA application names)
|
||||
partdb.title: '%env(string:settings:customization:instanceName)%' # The title shown inside of Part-DB (e.g. in the navbar and on homepage)
|
||||
partdb.locale_menu: ['en', 'de', 'it', 'fr', 'ru', 'ja', 'cs', 'da', 'zh', 'pl'] # 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.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
|
||||
|
||||
@@ -22,11 +18,8 @@ parameters:
|
||||
# Users and Privacy
|
||||
######################################################################################################################
|
||||
partdb.gdpr_compliance: true # If this option is activated, IP addresses are anonymized to be GDPR compliant
|
||||
partdb.users.use_gravatar: '%env(bool:USE_GRAVATAR)%' # Set to false, if no Gravatar images should be used for user profiles.
|
||||
partdb.users.email_pw_reset: '%env(bool:ALLOW_EMAIL_PW_RESET)%' # Config if users are able, to reset their password by email. By default this enabled, when a mail server is configured.
|
||||
|
||||
partdb.check_for_updates: '%env(bool:CHECK_FOR_UPDATES)' # Set to false, if Part-DB should not contact the GitHub API to check for updates
|
||||
|
||||
######################################################################################################################
|
||||
# Mail settings
|
||||
######################################################################################################################
|
||||
@@ -36,11 +29,8 @@ parameters:
|
||||
######################################################################################################################
|
||||
# Attachments and files
|
||||
######################################################################################################################
|
||||
partdb.attachments.allow_downloads: '%env(bool:ALLOW_ATTACHMENT_DOWNLOADS)%' # Allow users to download attachments to server. Warning: This can be dangerous, because via that feature attackers maybe can access ressources on your intranet!
|
||||
partdb.attachments.download_by_default: '%env(bool:ATTACHMENT_DOWNLOAD_BY_DEFAULT)%' # If this is set the 'download external files' checkbox is set by default for new attachments (only if allow_downloads is set to true)
|
||||
partdb.attachments.dir.media: 'public/media/' # The folder where uploaded attachment files are saved (must be in public folder)
|
||||
partdb.attachments.dir.secure: 'uploads/' # The folder where secured attachment files are saved (must not be in public/)
|
||||
partdb.attachments.max_file_size: '%env(string:MAX_ATTACHMENT_FILE_SIZE)%' # The maximum size of an attachment file (in bytes, you can use M for megabytes and G for gigabytes)
|
||||
|
||||
######################################################################################################################
|
||||
# Error pages
|
||||
@@ -53,22 +43,6 @@ parameters:
|
||||
######################################################################################################################
|
||||
partdb.saml.enabled: '%env(bool:SAML_ENABLED)%' # If this is set to true, SAML authentication is enabled
|
||||
|
||||
######################################################################################################################
|
||||
# Table settings
|
||||
######################################################################################################################
|
||||
partdb.table.default_page_size: '%env(int:TABLE_DEFAULT_PAGE_SIZE)%' # The default number of entries shown per page in tables
|
||||
partdb.table.parts.default_columns: '%env(trim:string:TABLE_PARTS_DEFAULT_COLUMNS)%' # The default columns in part tables and their order
|
||||
|
||||
######################################################################################################################
|
||||
# Sidebar
|
||||
######################################################################################################################
|
||||
# You can configures the default shown tree items in the sidebar here. You can add or remove entries here, to change the number of trees in the sidebar. The possible entries are: categories, locations, footprints, manufacturers, suppliers, devices, tools
|
||||
partdb.sidebar.items:
|
||||
- categories
|
||||
- devices
|
||||
- tools
|
||||
partdb.sidebar.root_expanded: true # If this is set to true, the root node of the sidebar is expanded by default
|
||||
partdb.sidebar.root_node_enable: true # Put all entities below a root node in the sidebar
|
||||
|
||||
######################################################################################################################
|
||||
# Miscellaneous
|
||||
@@ -110,17 +84,8 @@ parameters:
|
||||
# Env default values
|
||||
######################################################################################################################
|
||||
|
||||
env(DEFAULT_LANG): 'en'
|
||||
env(DEFAULT_TIMEZONE): 'Europe/Berlin'
|
||||
env(INSTANCE_NAME): 'Part-DB'
|
||||
env(BASE_CURRENCY): 'EUR'
|
||||
env(USE_GRAVATAR): '0'
|
||||
env(MAX_ATTACHMENT_FILE_SIZE): '100M'
|
||||
|
||||
env(REDIRECT_TO_HTTPS): 0
|
||||
|
||||
env(ENFORCE_CHANGE_COMMENTS_FOR): ''
|
||||
|
||||
env(ERROR_PAGE_ADMIN_EMAIL): ''
|
||||
env(ERROR_PAGE_SHOW_HELP): 1
|
||||
|
||||
@@ -132,8 +97,6 @@ parameters:
|
||||
env(EMAIL_SENDER_NAME): 'Part-DB Mailer'
|
||||
env(ALLOW_EMAIL_PW_RESET): 0
|
||||
|
||||
env(TABLE_DEFAULT_PAGE_SIZE): 50
|
||||
|
||||
env(TRUSTED_PROXIES): '127.0.0.1' #By default trust only our own server
|
||||
env(TRUSTED_HOSTS): '' # Trust all host names by default
|
||||
|
||||
@@ -141,11 +104,4 @@ parameters:
|
||||
|
||||
env(SAML_ROLE_MAPPING): '{}'
|
||||
|
||||
env(HISTORY_SAVE_CHANGED_DATA): 1
|
||||
env(HISTORY_SAVE_CHANGED_FIELDS): 1
|
||||
env(HISTORY_SAVE_REMOVED_DATA): 1
|
||||
env(HISTORY_SAVE_NEW_DATA): 1
|
||||
|
||||
env(EDA_KICAD_CATEGORY_DEPTH): 0
|
||||
|
||||
env(DATABASE_EMULATE_NATURAL_SORT): 0
|
||||
|
||||
@@ -265,17 +265,13 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co
|
||||
# label: "perm.database.write_db_settings"
|
||||
# alsoSet: ['read_db_settings', 'see_status']
|
||||
|
||||
#config:
|
||||
# label: "perm.config"
|
||||
# group: "system"
|
||||
# operations:
|
||||
# read_config:
|
||||
# label: "perm.config.read_config"
|
||||
# edit_config:
|
||||
# label: "perm.config.edit_config"
|
||||
# alsoSet: 'read_config'
|
||||
# server_info:
|
||||
# label: "perm.config.server_info"
|
||||
config:
|
||||
label: "perm.config"
|
||||
group: "system"
|
||||
operations:
|
||||
change_system_settings:
|
||||
label: "perm.config.change_system_settings"
|
||||
apiTokenRole: ROLE_API_ADMIN
|
||||
|
||||
system:
|
||||
label: "perm.system"
|
||||
|
||||
@@ -17,8 +17,6 @@ services:
|
||||
bool $gdpr_compliance: '%partdb.gdpr_compliance%'
|
||||
bool $kernel_debug_enabled: '%kernel.debug%'
|
||||
string $kernel_cache_dir: '%kernel.cache_dir%'
|
||||
string $partdb_title: '%partdb.title%'
|
||||
string $base_currency: '%partdb.default_currency%'
|
||||
|
||||
_instanceof:
|
||||
App\Services\LabelSystem\PlaceholderProviders\PlaceholderProviderInterface:
|
||||
@@ -76,28 +74,10 @@ services:
|
||||
# Only the event classes specified here are saved to DB (set to []) to log all events
|
||||
$whitelist: []
|
||||
|
||||
App\EventListener\LogSystem\EventLoggerListener:
|
||||
arguments:
|
||||
$save_changed_fields: '%env(bool:HISTORY_SAVE_CHANGED_FIELDS)%'
|
||||
$save_changed_data: '%env(bool:HISTORY_SAVE_CHANGED_DATA)%'
|
||||
$save_removed_data: '%env(bool:HISTORY_SAVE_REMOVED_DATA)%'
|
||||
$save_new_data: '%env(bool:HISTORY_SAVE_NEW_DATA)%'
|
||||
|
||||
App\Form\AttachmentFormType:
|
||||
arguments:
|
||||
$allow_attachments_download: '%partdb.attachments.allow_downloads%'
|
||||
$max_file_size: '%partdb.attachments.max_file_size%'
|
||||
$download_by_default: '%partdb.attachments.download_by_default%'
|
||||
|
||||
App\Services\Attachments\AttachmentSubmitHandler:
|
||||
arguments:
|
||||
$allow_attachments_downloads: '%partdb.attachments.allow_downloads%'
|
||||
$mimeTypes: '@mime_types'
|
||||
$max_upload_size: '%partdb.attachments.max_file_size%'
|
||||
|
||||
App\Services\LogSystem\EventCommentNeededHelper:
|
||||
arguments:
|
||||
$enforce_change_comments_for: '%partdb.enforce_change_comments_for%'
|
||||
|
||||
####################################################################################################################
|
||||
# Attachment system
|
||||
@@ -156,29 +136,6 @@ services:
|
||||
tags:
|
||||
- { name: doctrine.orm.entity_listener }
|
||||
|
||||
####################################################################################################################
|
||||
# Price system
|
||||
####################################################################################################################
|
||||
App\Command\Currencies\UpdateExchangeRatesCommand:
|
||||
arguments:
|
||||
$base_current: '%partdb.default_currency%'
|
||||
|
||||
App\Form\Type\CurrencyEntityType:
|
||||
arguments:
|
||||
$base_currency: '%partdb.default_currency%'
|
||||
|
||||
App\Services\Parts\PricedetailHelper:
|
||||
arguments:
|
||||
$base_currency: '%partdb.default_currency%'
|
||||
|
||||
App\Services\Formatters\MoneyFormatter:
|
||||
arguments:
|
||||
$base_currency: '%partdb.default_currency%'
|
||||
|
||||
App\Services\Tools\ExchangeRateUpdater:
|
||||
arguments:
|
||||
$base_currency: '%partdb.default_currency%'
|
||||
|
||||
###################################################################################################################
|
||||
# User system
|
||||
####################################################################################################################
|
||||
@@ -186,10 +143,6 @@ services:
|
||||
arguments:
|
||||
$demo_mode: '%partdb.demo_mode%'
|
||||
|
||||
App\EventSubscriber\UserSystem\SetUserTimezoneSubscriber:
|
||||
arguments:
|
||||
$default_timezone: '%partdb.timezone%'
|
||||
|
||||
App\Controller\SecurityController:
|
||||
arguments:
|
||||
$allow_email_pw_reset: '%partdb.users.email_pw_reset%'
|
||||
@@ -203,10 +156,6 @@ services:
|
||||
tags:
|
||||
- { name: 'translation.extractor', alias: 'permissionExtractor'}
|
||||
|
||||
App\Services\UserSystem\UserAvatarHelper:
|
||||
arguments:
|
||||
$use_gravatar: '%partdb.users.use_gravatar%'
|
||||
|
||||
App\Form\Type\ThemeChoiceType:
|
||||
arguments:
|
||||
$available_themes: '%partdb.available_themes%'
|
||||
@@ -222,9 +171,6 @@ services:
|
||||
####################################################################################################################
|
||||
# Table settings
|
||||
####################################################################################################################
|
||||
App\DataTables\PartsDataTable:
|
||||
arguments:
|
||||
$visible_columns: '%partdb.table.parts.default_columns%'
|
||||
|
||||
App\DataTables\Helpers\ColumnSortHelper:
|
||||
shared: false # Service has a state so not share it between different tables
|
||||
@@ -246,14 +192,6 @@ services:
|
||||
$fontDirectory: '%kernel.project_dir%/var/dompdf/fonts/'
|
||||
$tmpDirectory: '%kernel.project_dir%/var/dompdf/tmp/'
|
||||
|
||||
####################################################################################################################
|
||||
# Trees
|
||||
####################################################################################################################
|
||||
App\Services\Trees\TreeViewGenerator:
|
||||
arguments:
|
||||
$rootNodeExpandedByDefault: '%partdb.sidebar.root_expanded%'
|
||||
$rootNodeEnabled: '%partdb.sidebar.root_node_enable%'
|
||||
|
||||
####################################################################################################################
|
||||
# Part info provider system
|
||||
####################################################################################################################
|
||||
@@ -261,76 +199,12 @@ services:
|
||||
arguments:
|
||||
$providers: !tagged_iterator 'app.info_provider'
|
||||
|
||||
App\Services\InfoProviderSystem\Providers\Element14Provider:
|
||||
arguments:
|
||||
$api_key: '%env(string:PROVIDER_ELEMENT14_KEY)%'
|
||||
$store_id: '%env(string:PROVIDER_ELEMENT14_STORE_ID)%'
|
||||
|
||||
App\Services\InfoProviderSystem\Providers\DigikeyProvider:
|
||||
arguments:
|
||||
$clientId: '%env(string:PROVIDER_DIGIKEY_CLIENT_ID)%'
|
||||
$currency: '%env(string:PROVIDER_DIGIKEY_CURRENCY)%'
|
||||
$language: '%env(string:PROVIDER_DIGIKEY_LANGUAGE)%'
|
||||
$country: '%env(string:PROVIDER_DIGIKEY_COUNTRY)%'
|
||||
|
||||
App\Services\InfoProviderSystem\Providers\TMEClient:
|
||||
arguments:
|
||||
$secret: '%env(string:PROVIDER_TME_SECRET)%'
|
||||
$token: '%env(string:PROVIDER_TME_KEY)%'
|
||||
|
||||
App\Services\InfoProviderSystem\Providers\TMEProvider:
|
||||
arguments:
|
||||
$currency: '%env(string:PROVIDER_TME_CURRENCY)%'
|
||||
$country: '%env(string:PROVIDER_TME_COUNTRY)%'
|
||||
$language: '%env(string:PROVIDER_TME_LANGUAGE)%'
|
||||
$get_gross_prices: '%env(bool:PROVIDER_TME_GET_GROSS_PRICES)%'
|
||||
|
||||
App\Services\InfoProviderSystem\Providers\OctopartProvider:
|
||||
arguments:
|
||||
$clientId: '&env(string:PROVIDER_OCTOPART_CLIENT_ID)%'
|
||||
$secret: '%env(string:PROVIDER_OCTOPART_SECRET)%'
|
||||
$country: '%env(string:PROVIDER_OCTOPART_COUNTRY)%'
|
||||
$currency: '%env(string:PROVIDER_OCTOPART_CURRENCY)%'
|
||||
$search_limit: '%env(int:PROVIDER_OCTOPART_SEARCH_LIMIT)%'
|
||||
$onlyAuthorizedSellers: '%env(bool:PROVIDER_OCTOPART_ONLY_AUTHORIZED_SELLERS)%'
|
||||
|
||||
App\Services\InfoProviderSystem\Providers\MouserProvider:
|
||||
arguments:
|
||||
$api_key: '%env(string:PROVIDER_MOUSER_KEY)%'
|
||||
$language: '%env(string:PROVIDER_MOUSER_SEARCH_WITH_SIGNUP_LANGUAGE)%'
|
||||
$options: '%env(string:PROVIDER_MOUSER_SEARCH_OPTION)%'
|
||||
$search_limit: '%env(int:PROVIDER_MOUSER_SEARCH_LIMIT)%'
|
||||
|
||||
App\Services\InfoProviderSystem\Providers\LCSCProvider:
|
||||
arguments:
|
||||
$enabled: '%env(bool:PROVIDER_LCSC_ENABLED)%'
|
||||
$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
|
||||
####################################################################################################################
|
||||
App\State\PartDBInfoProvider:
|
||||
arguments:
|
||||
$default_uri: '%partdb.default_uri%'
|
||||
$global_locale: '%partdb.locale%'
|
||||
$global_timezone: '%partdb.timezone%'
|
||||
|
||||
####################################################################################################################
|
||||
# EDA system
|
||||
####################################################################################################################
|
||||
App\Services\EDA\KiCadHelper:
|
||||
arguments:
|
||||
$category_depth: '%env(int:EDA_KICAD_CATEGORY_DEPTH)%'
|
||||
|
||||
####################################################################################################################
|
||||
# Symfony overrides
|
||||
@@ -355,7 +229,6 @@ services:
|
||||
####################################################################################################################
|
||||
App\Controller\RedirectController:
|
||||
arguments:
|
||||
$default_locale: '%partdb.locale%'
|
||||
$enforce_index_php: '%env(bool:NO_URL_REWRITE_AVAILABLE)%'
|
||||
|
||||
App\Doctrine\Purger\ResetAutoIncrementPurgerFactory:
|
||||
@@ -370,14 +243,6 @@ services:
|
||||
arguments:
|
||||
$project_dir: '%kernel.project_dir%'
|
||||
|
||||
App\Services\System\UpdateAvailableManager:
|
||||
arguments:
|
||||
$check_for_updates: '%partdb.check_for_updates%'
|
||||
|
||||
App\Services\System\BannerHelper:
|
||||
arguments:
|
||||
$partdb_banner: '%partdb.banner%'
|
||||
$project_dir: '%kernel.project_dir%'
|
||||
|
||||
App\Doctrine\Middleware\MySQLSSLConnectionMiddlewareWrapper:
|
||||
arguments:
|
||||
|
||||
@@ -95,6 +95,8 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept
|
||||
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.
|
||||
* `SHOW_PART_IMAGE_OVERLAY`: Set to 0 to disable the part image overlay, which appears if you hover over an image in the
|
||||
part image gallery
|
||||
|
||||
### E-Mail settings
|
||||
|
||||
|
||||
@@ -47,6 +47,12 @@ services:
|
||||
- DATABASE_URL=sqlite:///%kernel.project_dir%/var/db/app.db
|
||||
# In docker env logs will be redirected to stderr
|
||||
- APP_ENV=docker
|
||||
|
||||
# Uncomment this, if you want to use the automatic database migration feature. With this you have you do not have to
|
||||
# run the doctrine:migrations:migrate commands on installation or upgrade. A database backup is written to the uploads/
|
||||
# folder (under .automigration-backup), so you can restore it, if the migration fails.
|
||||
# This feature is currently experimental, so use it at your own risk!
|
||||
# - DB_AUTOMIGRATE=true
|
||||
|
||||
# You can configure Part-DB using environment variables
|
||||
# Below you can find the most essential ones predefined
|
||||
@@ -130,6 +136,12 @@ services:
|
||||
# In docker env logs will be redirected to stderr
|
||||
- APP_ENV=docker
|
||||
|
||||
# Uncomment this, if you want to use the automatic database migration feature. With this you have you do not have to
|
||||
# run the doctrine:migrations:migrate commands on installation or upgrade. A database backup is written to the uploads/
|
||||
# folder (under .automigration-backup), so you can restore it, if the migration fails.
|
||||
# This feature is currently experimental, so use it at your own risk!
|
||||
# - DB_AUTOMIGRATE=true
|
||||
|
||||
# You can configure Part-DB using environment variables
|
||||
# Below you can find the most essential ones predefined
|
||||
# However you can add add any other environment configuration you want here
|
||||
@@ -201,6 +213,10 @@ You also have to create the database as described above in step 4.
|
||||
You can run the console commands described in README by
|
||||
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
|
||||
|
||||
*Login is not possible. Login page is just reloading and no error message is shown or something like "CSFR token invalid"*:
|
||||
|
||||
@@ -52,6 +52,11 @@ server {
|
||||
location ~ \.php$ {
|
||||
return 404;
|
||||
}
|
||||
|
||||
# Set Content-Security-Policy for svg files, to block embedded javascript in there
|
||||
location ~* \.svg$ {
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none';";
|
||||
}
|
||||
|
||||
error_log /var/log/nginx/parts.error.log;
|
||||
access_log /var/log/nginx/parts.access.log;
|
||||
|
||||
@@ -25,6 +25,12 @@ 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
|
||||
```
|
||||
|
||||
{: .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
|
||||
|
||||
* `php bin/console partdb:users:list`: List all users of this Part-DB instance
|
||||
@@ -64,4 +70,10 @@ docker exec --user=www-data partdb php bin/console cache:clear
|
||||
## Database commands
|
||||
|
||||
* `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
|
||||
|
||||
## Attachment commands
|
||||
|
||||
* `php bin/console partdb:attachments:download`: Download all attachments, which are not already downloaded, to the
|
||||
local filesystem. This is useful to create local backups of the attachments, no matter what happens on the remote and
|
||||
also makes pictures thumbnails available for the frontend for them
|
||||
@@ -127,9 +127,6 @@ You must create an organization there and create a "Production app". Most settin
|
||||
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).
|
||||
|
||||
**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:
|
||||
|
||||
* `PROVIDER_DIGIKEY_CLIENT_ID`: The client ID you got from Digi-Key (mandatory)
|
||||
|
||||
@@ -4,24 +4,25 @@ declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use App\Migration\AbstractMultiPlatformMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20250220215048 extends AbstractMultiPlatformMigration
|
||||
final class Version20250220215048 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Split $path property for attachments into $internal_path and $external_path';
|
||||
}
|
||||
|
||||
public function mySQLUp(Schema $schema): void
|
||||
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, ADD external_path VARCHAR(255) DEFAULT NULL');
|
||||
$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('ALTER TABLE attachments DROP COLUMN path');
|
||||
|
||||
|
||||
$this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%MEDIA#%%\' ESCAPE \'#\'');
|
||||
@@ -32,52 +33,10 @@ final class Version20250220215048 extends AbstractMultiPlatformMigration
|
||||
$this->addSql('UPDATE attachments SET external_path=NULL WHERE internal_path IS NOT NULL');
|
||||
}
|
||||
|
||||
public function mySQLDown(Schema $schema): void
|
||||
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 DROP COLUMN internal_path');
|
||||
$this->addSql('ALTER TABLE attachments RENAME COLUMN external_path TO path');
|
||||
}
|
||||
|
||||
public function postgreSQLUp(Schema $schema): void
|
||||
{
|
||||
//We can use the same SQL for PostgreSQL as for MySQL
|
||||
$this->mySQLUp($schema);
|
||||
}
|
||||
|
||||
public function postgreSQLDown(Schema $schema): void
|
||||
{
|
||||
//We can use the same SQL for PostgreSQL as for MySQL
|
||||
$this->mySQLDown($schema);
|
||||
}
|
||||
|
||||
public function sqLiteUp(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE TEMPORARY TABLE __temp__attachments AS SELECT id, type_id, original_filename, show_in_table, name, last_modified, datetime_added, class_name, element_id, path FROM attachments');
|
||||
$this->addSql('DROP TABLE attachments');
|
||||
$this->addSql('CREATE TABLE attachments (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, type_id INTEGER NOT NULL, original_filename VARCHAR(255) DEFAULT NULL, show_in_table BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, class_name VARCHAR(255) NOT NULL, element_id INTEGER NOT NULL, internal_path VARCHAR(255) DEFAULT NULL, external_path VARCHAR(255) DEFAULT NULL, CONSTRAINT FK_47C4FAD6C54C8C93 FOREIGN KEY (type_id) REFERENCES attachment_types (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE)');
|
||||
$this->addSql('INSERT INTO attachments (id, type_id, original_filename, show_in_table, name, last_modified, datetime_added, class_name, element_id, external_path) SELECT id, type_id, original_filename, show_in_table, name, last_modified, datetime_added, class_name, element_id, path FROM __temp__attachments');
|
||||
$this->addSql('DROP TABLE __temp__attachments');
|
||||
$this->addSql('CREATE INDEX attachment_element_idx ON attachments (class_name, element_id)');
|
||||
$this->addSql('CREATE INDEX attachment_name_idx ON attachments (name)');
|
||||
$this->addSql('CREATE INDEX attachments_idx_class_name_id ON attachments (class_name, id)');
|
||||
$this->addSql('CREATE INDEX attachments_idx_id_element_id_class_name ON attachments (id, element_id, class_name)');
|
||||
$this->addSql('CREATE INDEX IDX_47C4FAD6C54C8C93 ON attachments (type_id)');
|
||||
$this->addSql('CREATE INDEX IDX_47C4FAD61F1F2A24 ON attachments (element_id)');
|
||||
|
||||
$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 sqLiteDown(Schema $schema): void
|
||||
{
|
||||
//Reuse the MySQL down migration:
|
||||
$this->mySQLDown($schema);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
49
migrations/Version20250706201121.php
Normal file
49
migrations/Version20250706201121.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use App\Migration\AbstractMultiPlatformMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
|
||||
final class Version20250706201121 extends AbstractMultiPlatformMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add settings_entry table for storing settings';
|
||||
}
|
||||
|
||||
public function mySQLUp(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE TABLE settings_entry (`key` VARCHAR(255) NOT NULL, `data` JSON DEFAULT NULL, id INT AUTO_INCREMENT NOT NULL, UNIQUE INDEX UNIQ_93F8DB394E645A7E (`key`), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci`');
|
||||
|
||||
}
|
||||
|
||||
public function mySQLDown(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP TABLE settings_entry');
|
||||
}
|
||||
|
||||
public function sqLiteUp(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE TABLE settings_entry ("key" VARCHAR(255) NOT NULL, "data" CLOB DEFAULT NULL, id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)');
|
||||
$this->addSql('CREATE UNIQUE INDEX UNIQ_93F8DB39F48571EB ON settings_entry ("key")');
|
||||
}
|
||||
|
||||
public function sqLiteDown(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP TABLE settings_entry');
|
||||
}
|
||||
|
||||
public function postgreSQLUp(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE TABLE settings_entry ("key" VARCHAR(255) NOT NULL, "data" JSON DEFAULT NULL, id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, PRIMARY KEY(id))');
|
||||
$this->addSql('CREATE UNIQUE INDEX UNIQ_93F8DB39F48571EB ON settings_entry ("key")');
|
||||
}
|
||||
|
||||
public function postgreSQLDown(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP TABLE settings_entry');
|
||||
}
|
||||
}
|
||||
@@ -118,3 +118,10 @@ DirectoryIndex index.php
|
||||
# RedirectTemp cannot be used instead
|
||||
</IfModule>
|
||||
</IfModule>
|
||||
|
||||
# Set Content-Security-Policy for svg files (and compressed variants), to block embedded javascript in there
|
||||
<IfModule mod_headers.c>
|
||||
<FilesMatch "\.(svg|svg\.gz|svg\.br)$">
|
||||
Header set Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none';"
|
||||
</FilesMatch>
|
||||
</IfModule>
|
||||
@@ -65,7 +65,7 @@ class NormalizePropertyNameCollectionFactory implements PropertyNameCollectionFa
|
||||
$camelized = u($property)->camel()->toString();
|
||||
|
||||
//If the camelized version exists, remove it from the collection
|
||||
$index = array_search($camelized, $properties);
|
||||
$index = array_search($camelized, $properties, true);
|
||||
if ($index !== false) {
|
||||
unset($properties[$index]);
|
||||
}
|
||||
|
||||
@@ -73,6 +73,9 @@ class CleanAttachmentsCommand extends Command
|
||||
//Ignore image cache folder
|
||||
$finder->exclude('cache');
|
||||
|
||||
//Ignore automigration folder
|
||||
$finder->exclude('.automigration-backup');
|
||||
|
||||
$fs = new Filesystem();
|
||||
|
||||
$file_list = [];
|
||||
|
||||
136
src/Command/Attachments/DownloadAttachmentsCommand.php
Normal file
136
src/Command/Attachments/DownloadAttachmentsCommand.php
Normal file
@@ -0,0 +1,136 @@
|
||||
<?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\Command\Attachments;
|
||||
|
||||
use App\Entity\Attachments\Attachment;
|
||||
use App\Entity\Attachments\AttachmentUpload;
|
||||
use App\Exceptions\AttachmentDownloadException;
|
||||
use App\Services\Attachments\AttachmentManager;
|
||||
use App\Services\Attachments\AttachmentSubmitHandler;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand('partdb:attachments:download', "Downloads all attachments which have only an external URL to the local filesystem.")]
|
||||
class DownloadAttachmentsCommand extends Command
|
||||
{
|
||||
public function __construct(private readonly AttachmentSubmitHandler $attachmentSubmitHandler,
|
||||
private EntityManagerInterface $entityManager)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function configure(): void
|
||||
{
|
||||
$this->setHelp('This command downloads all attachments, which only have an external URL, to the local filesystem, so that you have an offline copy of the attachments.');
|
||||
$this->addOption('--private', null, null, 'If set, the attachments will be downloaded to the private storage.');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$qb = $this->entityManager->createQueryBuilder();
|
||||
$qb->select('attachment')
|
||||
->from(Attachment::class, 'attachment')
|
||||
->where('attachment.external_path IS NOT NULL')
|
||||
->andWhere('attachment.external_path != \'\'')
|
||||
->andWhere('attachment.internal_path IS NULL');
|
||||
|
||||
$query = $qb->getQuery();
|
||||
$attachments = $query->getResult();
|
||||
|
||||
if (count($attachments) === 0) {
|
||||
$io->success('No attachments with external URL found.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$io->note('Found ' . count($attachments) . ' attachments with external URL, that will be downloaded.');
|
||||
|
||||
//If the option --private is set, the attachments will be downloaded to the private storage.
|
||||
$private = $input->getOption('private');
|
||||
if ($private) {
|
||||
if (!$io->confirm('Attachments will be downloaded to the private storage. Continue?')) {
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
} else {
|
||||
if (!$io->confirm('Attachments will be downloaded to the public storage, where everybody knowing the correct URL can access it. Continue?')){
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
$progressBar = $io->createProgressBar(count($attachments));
|
||||
$progressBar->setFormat("%current%/%max% [%bar%] %percent:3s%% %elapsed:16s%/%estimated:-16s% \n%message%");
|
||||
|
||||
$progressBar->setMessage('Starting download...');
|
||||
$progressBar->start();
|
||||
|
||||
|
||||
$errors = [];
|
||||
|
||||
foreach ($attachments as $attachment) {
|
||||
/** @var Attachment $attachment */
|
||||
$progressBar->setMessage(sprintf('%s (ID: %s) from %s', $attachment->getName(), $attachment->getID(), $attachment->getHost()));
|
||||
$progressBar->advance();
|
||||
|
||||
try {
|
||||
$attachmentUpload = new AttachmentUpload(file: null, downloadUrl: true, private: $private);
|
||||
$this->attachmentSubmitHandler->handleUpload($attachment, $attachmentUpload);
|
||||
|
||||
//Write changes to the database
|
||||
$this->entityManager->flush();
|
||||
} catch (AttachmentDownloadException $e) {
|
||||
$errors[] = [
|
||||
'attachment' => $attachment,
|
||||
'error' => $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$progressBar->finish();
|
||||
|
||||
//Fix the line break after the progress bar
|
||||
$io->newLine();
|
||||
$io->newLine();
|
||||
|
||||
if (count($errors) > 0) {
|
||||
$io->warning('Some attachments could not be downloaded:');
|
||||
foreach ($errors as $error) {
|
||||
$io->warning(sprintf("Attachment %s (ID %s) could not be downloaded from %s:\n%s",
|
||||
$error['attachment']->getName(),
|
||||
$error['attachment']->getID(),
|
||||
$error['attachment']->getExternalPath(),
|
||||
$error['error'])
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$io->success('All attachments downloaded successfully.');
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
90
src/Command/Attachments/SanitizeSVGAttachmentsCommand.php
Normal file
90
src/Command/Attachments/SanitizeSVGAttachmentsCommand.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?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\Command\Attachments;
|
||||
|
||||
use App\Entity\Attachments\Attachment;
|
||||
use App\Services\Attachments\AttachmentSubmitHandler;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand('partdb:attachments:sanitize-svg', "Sanitize uploaded SVG files.")]
|
||||
class SanitizeSVGAttachmentsCommand extends Command
|
||||
{
|
||||
public function __construct(private readonly EntityManagerInterface $entityManager, private readonly AttachmentSubmitHandler $attachmentSubmitHandler, ?string $name = null)
|
||||
{
|
||||
parent::__construct($name);
|
||||
}
|
||||
|
||||
public function configure(): void
|
||||
{
|
||||
$this->setHelp('This command allows to sanitize SVG files uploaded via attachments. This happens automatically since version 1.17.1, this command is intended to be used for older files.');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$io->info('This command will sanitize all uploaded SVG files. This is only required if you have uploaded (untrusted) SVG files before version 1.17.1. If you are running a newer version, you don\'t need to run this command (again).');
|
||||
if (!$io->confirm('Do you want to continue?', false)) {
|
||||
$io->success('Command aborted.');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$io->info('Sanitizing SVG files...');
|
||||
|
||||
//Finding all attachments with svg files
|
||||
$qb = $this->entityManager->createQueryBuilder();
|
||||
$qb->select('a')
|
||||
->from(Attachment::class, 'a')
|
||||
->where('a.internal_path LIKE :pattern ESCAPE \'#\'')
|
||||
->orWhere('a.original_filename LIKE :pattern ESCAPE \'#\'')
|
||||
->setParameter('pattern', '%.svg');
|
||||
|
||||
$attachments = $qb->getQuery()->getResult();
|
||||
$io->note('Found '.count($attachments).' attachments with SVG files.');
|
||||
|
||||
if (count($attachments) === 0) {
|
||||
$io->success('No SVG files found.');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$io->info('Sanitizing SVG files...');
|
||||
$io->progressStart(count($attachments));
|
||||
foreach ($attachments as $attachment) {
|
||||
/** @var Attachment $attachment */
|
||||
$io->note('Sanitizing attachment '.$attachment->getId().' ('.($attachment->getFilename() ?? '???').')');
|
||||
$this->attachmentSubmitHandler->sanitizeSVGAttachment($attachment);
|
||||
$io->progressAdvance();
|
||||
|
||||
}
|
||||
$io->progressFinish();
|
||||
|
||||
$io->success('Sanitization finished. All SVG files have been sanitized.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Command\Currencies;
|
||||
|
||||
use App\Settings\SystemSettings\LocalizationSettings;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use App\Entity\PriceInformations\Currency;
|
||||
use App\Services\Tools\ExchangeRateUpdater;
|
||||
@@ -39,7 +40,7 @@ use function strlen;
|
||||
#[AsCommand('partdb:currencies:update-exchange-rates|partdb:update-exchange-rates|app:update-exchange-rates', 'Updates the currency exchange rates.')]
|
||||
class UpdateExchangeRatesCommand extends Command
|
||||
{
|
||||
public function __construct(protected string $base_current, protected EntityManagerInterface $em, protected ExchangeRateUpdater $exchangeRateUpdater)
|
||||
public function __construct(protected EntityManagerInterface $em, protected ExchangeRateUpdater $exchangeRateUpdater, private readonly LocalizationSettings $localizationSettings)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
@@ -54,13 +55,13 @@ class UpdateExchangeRatesCommand extends Command
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
//Check for valid base current
|
||||
if (3 !== strlen($this->base_current)) {
|
||||
if (3 !== strlen($this->localizationSettings->baseCurrency)) {
|
||||
$io->error('Chosen Base current is not valid. Check your settings!');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$io->note('Update currency exchange rates with base currency: '.$this->base_current);
|
||||
$io->note('Update currency exchange rates with base currency: '.$this->localizationSettings->baseCurrency);
|
||||
|
||||
//Check what currencies we need to update:
|
||||
$iso_code = $input->getArgument('iso_code');
|
||||
|
||||
@@ -28,6 +28,7 @@ use App\Entity\Attachments\Attachment;
|
||||
use App\Form\Filters\AttachmentFilterType;
|
||||
use App\Services\Attachments\AttachmentManager;
|
||||
use App\Services\Trees\NodesListBuilder;
|
||||
use App\Settings\BehaviorSettings\TableSettings;
|
||||
use Omines\DataTablesBundle\DataTableFactory;
|
||||
use RuntimeException;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
@@ -98,7 +99,8 @@ class AttachmentFileController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/attachment/list', name: 'attachment_list')]
|
||||
public function attachmentsTable(Request $request, DataTableFactory $dataTableFactory, NodesListBuilder $nodesListBuilder): Response
|
||||
public function attachmentsTable(Request $request, DataTableFactory $dataTableFactory, NodesListBuilder $nodesListBuilder,
|
||||
TableSettings $tableSettings): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('@attachments.list_attachments');
|
||||
|
||||
@@ -110,7 +112,7 @@ class AttachmentFileController extends AbstractController
|
||||
|
||||
$filterForm->handleRequest($formRequest);
|
||||
|
||||
$table = $dataTableFactory->createFromType(AttachmentDataTable::class, ['filter' => $filter])
|
||||
$table = $dataTableFactory->createFromType(AttachmentDataTable::class, ['filter' => $filter], ['pageLength' => $tableSettings->fullDefaultPageSize])
|
||||
->handleRequest($request);
|
||||
|
||||
if ($table->isCallback()) {
|
||||
|
||||
@@ -38,6 +38,7 @@ use App\Services\LogSystem\LogEntryExtraFormatter;
|
||||
use App\Services\LogSystem\LogLevelHelper;
|
||||
use App\Services\LogSystem\LogTargetHelper;
|
||||
use App\Services\LogSystem\TimeTravel;
|
||||
use App\Settings\BehaviorSettings\TableSettings;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use InvalidArgumentException;
|
||||
use Omines\DataTablesBundle\DataTableFactory;
|
||||
@@ -58,7 +59,7 @@ class LogController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/', name: 'log_view')]
|
||||
public function showLogs(Request $request, DataTableFactory $dataTable): Response
|
||||
public function showLogs(Request $request, DataTableFactory $dataTable, TableSettings $tableSettings): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('@system.show_logs');
|
||||
|
||||
@@ -72,7 +73,7 @@ class LogController extends AbstractController
|
||||
|
||||
$table = $dataTable->createFromType(LogDataTable::class, [
|
||||
'filter' => $filter,
|
||||
])
|
||||
], ['pageLength' => $tableSettings->fullDefaultPageSize])
|
||||
->handleRequest($request);
|
||||
|
||||
if ($table->isCallback()) {
|
||||
|
||||
@@ -29,12 +29,14 @@ use App\DataTables\PartsDataTable;
|
||||
use App\Entity\Parts\Category;
|
||||
use App\Entity\Parts\Footprint;
|
||||
use App\Entity\Parts\Manufacturer;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Entity\Parts\StorageLocation;
|
||||
use App\Entity\Parts\Supplier;
|
||||
use App\Exceptions\InvalidRegexException;
|
||||
use App\Form\Filters\PartFilterType;
|
||||
use App\Services\Parts\PartsTableActionHandler;
|
||||
use App\Services\Trees\NodesListBuilder;
|
||||
use App\Settings\BehaviorSettings\TableSettings;
|
||||
use Doctrine\DBAL\Exception\DriverException;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Omines\DataTablesBundle\DataTableFactory;
|
||||
@@ -43,11 +45,19 @@ use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Translation\TranslatableMessage;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
use function Symfony\Component\Translation\t;
|
||||
|
||||
class PartListsController extends AbstractController
|
||||
{
|
||||
public function __construct(private readonly EntityManagerInterface $entityManager, private readonly NodesListBuilder $nodesListBuilder, private readonly DataTableFactory $dataTableFactory, private readonly TranslatorInterface $translator)
|
||||
public function __construct(private readonly EntityManagerInterface $entityManager,
|
||||
private readonly NodesListBuilder $nodesListBuilder,
|
||||
private readonly DataTableFactory $dataTableFactory,
|
||||
private readonly TranslatorInterface $translator,
|
||||
private readonly TableSettings $tableSettings
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -71,13 +81,32 @@ class PartListsController extends AbstractController
|
||||
if (null === $action || null === $ids) {
|
||||
$this->addFlash('error', 'part.table.actions.no_params_given');
|
||||
} else {
|
||||
$errors = [];
|
||||
|
||||
$parts = $actionHandler->idStringToArray($ids);
|
||||
$redirectResponse = $actionHandler->handleAction($action, $parts, $target ? (int) $target : null, $redirect);
|
||||
$redirectResponse = $actionHandler->handleAction($action, $parts, $target ? (int) $target : null, $redirect, $errors);
|
||||
|
||||
//Save changes
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->addFlash('success', 'part.table.actions.success');
|
||||
if (count($errors) === 0) {
|
||||
$this->addFlash('success', 'part.table.actions.success');
|
||||
} else {
|
||||
$this->addFlash('error', t('part.table.actions.error', ['%count%' => count($errors)]));
|
||||
//Create a flash message for each error
|
||||
foreach ($errors as $error) {
|
||||
/** @var Part $part */
|
||||
$part = $error['part'];
|
||||
|
||||
$this->addFlash('error',
|
||||
t('part.table.actions.error_detail', [
|
||||
'%part_name%' => $part->getName(),
|
||||
'%part_id%' => $part->getID(),
|
||||
'%message%' => $error['message']
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//If the action handler returned a response, we use it, otherwise we redirect back to the previous page.
|
||||
@@ -132,11 +161,7 @@ class PartListsController extends AbstractController
|
||||
|
||||
$filterForm->handleRequest($formRequest);
|
||||
|
||||
$table = $this->dataTableFactory->createFromType(
|
||||
PartsDataTable::class,
|
||||
array_merge(['filter' => $filter], $additional_table_vars),
|
||||
['lengthMenu' => PartsDataTable::LENGTH_MENU]
|
||||
)
|
||||
$table = $this->dataTableFactory->createFromType(PartsDataTable::class, array_merge(['filter' => $filter], $additional_table_vars), ['pageLength' => $this->tableSettings->fullDefaultPageSize])
|
||||
->handleRequest($request);
|
||||
|
||||
if ($table->isCallback()) {
|
||||
|
||||
@@ -31,6 +31,7 @@ use App\Form\ProjectSystem\ProjectBuildType;
|
||||
use App\Helpers\Projects\ProjectBuildRequest;
|
||||
use App\Services\ImportExportSystem\BOMImporter;
|
||||
use App\Services\ProjectSystem\ProjectBuildHelper;
|
||||
use App\Settings\BehaviorSettings\TableSettings;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use League\Csv\SyntaxError;
|
||||
@@ -55,11 +56,12 @@ class ProjectController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/info', name: 'project_info', requirements: ['id' => '\d+'])]
|
||||
public function info(Project $project, Request $request, ProjectBuildHelper $buildHelper): Response
|
||||
public function info(Project $project, Request $request, ProjectBuildHelper $buildHelper, TableSettings $tableSettings): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('read', $project);
|
||||
|
||||
$table = $this->dataTableFactory->createFromType(ProjectBomEntriesDataTable::class, ['project' => $project])
|
||||
$table = $this->dataTableFactory->createFromType(ProjectBomEntriesDataTable::class, ['project' => $project],
|
||||
['pageLength' => $tableSettings->fullDefaultPageSize])
|
||||
->handleRequest($request);
|
||||
|
||||
if ($table->isCallback()) {
|
||||
|
||||
@@ -23,6 +23,7 @@ declare(strict_types=1);
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\UserSystem\User;
|
||||
use App\Settings\SystemSettings\LocalizationSettings;
|
||||
use function function_exists;
|
||||
use function in_array;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
@@ -35,7 +36,7 @@ use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
*/
|
||||
class RedirectController extends AbstractController
|
||||
{
|
||||
public function __construct(protected string $default_locale, protected TranslatorInterface $translator, protected bool $enforce_index_php)
|
||||
public function __construct(private readonly LocalizationSettings $localizationSettings, protected TranslatorInterface $translator, protected bool $enforce_index_php)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -46,7 +47,7 @@ class RedirectController extends AbstractController
|
||||
public function addLocalePart(Request $request): RedirectResponse
|
||||
{
|
||||
//By default, we use the global default locale
|
||||
$locale = $this->default_locale;
|
||||
$locale = $this->localizationSettings->locale;
|
||||
|
||||
//Check if a user has set a preferred language setting:
|
||||
$user = $this->getUser();
|
||||
|
||||
@@ -29,6 +29,7 @@ use App\Entity\Parts\Category;
|
||||
use App\Entity\Parts\Footprint;
|
||||
use App\Entity\Parts\Manufacturer;
|
||||
use App\Entity\Parts\MeasurementUnit;
|
||||
use App\Entity\Parts\StorageLocation;
|
||||
use App\Entity\ProjectSystem\Project;
|
||||
use App\Form\Type\Helper\StructuralEntityChoiceHelper;
|
||||
use App\Services\Trees\NodesListBuilder;
|
||||
@@ -78,6 +79,12 @@ class SelectAPIController extends AbstractController
|
||||
return $this->getResponseForClass(Project::class, false);
|
||||
}
|
||||
|
||||
#[Route(path: '/storage_location', name: 'select_storage_location')]
|
||||
public function locations(): Response
|
||||
{
|
||||
return $this->getResponseForClass(StorageLocation::class, true);
|
||||
}
|
||||
|
||||
#[Route(path: '/export_level', name: 'select_export_level')]
|
||||
public function exportLevel(): Response
|
||||
{
|
||||
|
||||
72
src/Controller/SettingsController.php
Normal file
72
src/Controller/SettingsController.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?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\Controller;
|
||||
|
||||
use App\Settings\AppSettings;
|
||||
use Jbtronics\SettingsBundle\Form\SettingsFormFactoryInterface;
|
||||
use Jbtronics\SettingsBundle\Manager\SettingsManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Contracts\Cache\TagAwareCacheInterface;
|
||||
|
||||
class SettingsController extends AbstractController
|
||||
{
|
||||
public function __construct(private readonly SettingsManagerInterface $settingsManager, private readonly SettingsFormFactoryInterface $settingsFormFactory)
|
||||
{}
|
||||
|
||||
#[Route("/settings", name: "system_settings")]
|
||||
public function systemSettings(Request $request, TagAwareCacheInterface $cache): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('@config.change_system_settings');
|
||||
|
||||
//Create a clone of the settings object
|
||||
$settings = $this->settingsManager->createTemporaryCopy(AppSettings::class);
|
||||
|
||||
//Create a form builder for the settings object
|
||||
$builder = $this->settingsFormFactory->createSettingsFormBuilder($settings);
|
||||
|
||||
//Add a submit button to the form
|
||||
$builder->add('submit', \Symfony\Component\Form\Extension\Core\Type\SubmitType::class, ['label' => 'save']);
|
||||
|
||||
//Create the form
|
||||
$form = $builder->getForm();
|
||||
$form->handleRequest($request);
|
||||
|
||||
//If the form was submitted and is valid, save the settings
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$this->settingsManager->mergeTemporaryCopy($settings);
|
||||
$this->settingsManager->save($settings);
|
||||
|
||||
//It might be possible, that the tree settings have changed, so clear the cache
|
||||
$cache->invalidateTags(['tree_treeview', 'sidebar_tree_update']);
|
||||
}
|
||||
|
||||
//Render the form
|
||||
return $this->render('settings/settings.html.twig', [
|
||||
'form' => $form
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ use App\Services\Doctrine\DBInfoHelper;
|
||||
use App\Services\Doctrine\NatsortDebugHelper;
|
||||
use App\Services\Misc\GitVersionInfo;
|
||||
use App\Services\System\UpdateAvailableManager;
|
||||
use App\Settings\AppSettings;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
@@ -47,7 +48,8 @@ class ToolsController extends AbstractController
|
||||
|
||||
#[Route(path: '/server_infos', name: 'tools_server_infos')]
|
||||
public function systemInfos(GitVersionInfo $versionInfo, DBInfoHelper $DBInfoHelper, NatsortDebugHelper $natsortDebugHelper,
|
||||
AttachmentSubmitHandler $attachmentSubmitHandler, UpdateAvailableManager $updateAvailableManager): Response
|
||||
AttachmentSubmitHandler $attachmentSubmitHandler, UpdateAvailableManager $updateAvailableManager,
|
||||
AppSettings $settings): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('@system.server_infos');
|
||||
|
||||
@@ -55,23 +57,23 @@ class ToolsController extends AbstractController
|
||||
//Part-DB section
|
||||
'git_branch' => $versionInfo->getGitBranchName(),
|
||||
'git_commit' => $versionInfo->getGitCommitHash(),
|
||||
'default_locale' => $this->getParameter('partdb.locale'),
|
||||
'default_timezone' => $this->getParameter('partdb.timezone'),
|
||||
'default_currency' => $this->getParameter('partdb.default_currency'),
|
||||
'default_theme' => $this->getParameter('partdb.global_theme'),
|
||||
'default_locale' => $settings->system->localization->locale,
|
||||
'default_timezone' => $settings->system->localization->timezone,
|
||||
'default_currency' => $settings->system->localization->baseCurrency,
|
||||
'default_theme' => $settings->system->customization->theme,
|
||||
'enabled_locales' => $this->getParameter('partdb.locale_menu'),
|
||||
'demo_mode' => $this->getParameter('partdb.demo_mode'),
|
||||
'use_gravatar' => $settings->system->privacy->useGravatar,
|
||||
'gdpr_compliance' => $this->getParameter('partdb.gdpr_compliance'),
|
||||
'use_gravatar' => $this->getParameter('partdb.users.use_gravatar'),
|
||||
'email_password_reset' => $this->getParameter('partdb.users.email_pw_reset'),
|
||||
'environment' => $this->getParameter('kernel.environment'),
|
||||
'is_debug' => $this->getParameter('kernel.debug'),
|
||||
'email_sender' => $this->getParameter('partdb.mail.sender_email'),
|
||||
'email_sender_name' => $this->getParameter('partdb.mail.sender_name'),
|
||||
'allow_attachments_downloads' => $this->getParameter('partdb.attachments.allow_downloads'),
|
||||
'allow_attachments_downloads' => $settings->system->attachments->allowDownloads,
|
||||
'detailed_error_pages' => $this->getParameter('partdb.error_pages.show_help'),
|
||||
'error_page_admin_email' => $this->getParameter('partdb.error_pages.admin_email'),
|
||||
'configured_max_file_size' => $this->getParameter('partdb.attachments.max_file_size'),
|
||||
'configured_max_file_size' => $settings->system->attachments->maxFileSize,
|
||||
'effective_max_file_size' => $attachmentSubmitHandler->getMaximumAllowedUploadSize(),
|
||||
'saml_enabled' => $this->getParameter('partdb.saml.enabled'),
|
||||
|
||||
|
||||
@@ -72,7 +72,8 @@ class ColumnSortHelper
|
||||
* Apply the visibility configuration to the given DataTable and configure the columns.
|
||||
* @param DataTable $dataTable
|
||||
* @param string|array $visible_columns Either a list or a comma separated string of column names, which should
|
||||
* be visible by default. If a column is not listed here, it will be hidden by default.
|
||||
* be visible by default. If a column is not listed here, it will be hidden by default. If an array of enum values are passed,
|
||||
* their value will be used as the column name.
|
||||
* @return void
|
||||
*/
|
||||
public function applyVisibilityAndConfigureColumns(DataTable $dataTable, string|array $visible_columns,
|
||||
@@ -83,6 +84,14 @@ class ColumnSortHelper
|
||||
$visible_columns = array_map(trim(...), explode(",", $visible_columns));
|
||||
}
|
||||
|
||||
//If $visible_columns is a list of enum values, convert them to the column names
|
||||
foreach ($visible_columns as &$value) {
|
||||
if ($value instanceof \BackedEnum) {
|
||||
$value = $value->value;
|
||||
}
|
||||
}
|
||||
unset ($value);
|
||||
|
||||
$processed_columns = [];
|
||||
|
||||
//First add all columns which visibility is not configurable
|
||||
|
||||
@@ -45,6 +45,7 @@ use App\Entity\Parts\PartLot;
|
||||
use App\Entity\ProjectSystem\Project;
|
||||
use App\Services\EntityURLGenerator;
|
||||
use App\Services\Formatters\AmountFormatter;
|
||||
use App\Settings\BehaviorSettings\TableSettings;
|
||||
use Doctrine\ORM\AbstractQuery;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Omines\DataTablesBundle\Adapter\Doctrine\ORM\SearchCriteriaProvider;
|
||||
@@ -65,8 +66,8 @@ final class PartsDataTable implements DataTableTypeInterface
|
||||
private readonly AmountFormatter $amountFormatter,
|
||||
private readonly PartDataTableHelper $partDataTableHelper,
|
||||
private readonly Security $security,
|
||||
private readonly string $visible_columns,
|
||||
private readonly ColumnSortHelper $csh,
|
||||
private readonly TableSettings $tableSettings,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -246,7 +247,7 @@ final class PartsDataTable implements DataTableTypeInterface
|
||||
]);
|
||||
|
||||
//Apply the user configured order and visibility and add the columns to the table
|
||||
$this->csh->applyVisibilityAndConfigureColumns($dataTable, $this->visible_columns,
|
||||
$this->csh->applyVisibilityAndConfigureColumns($dataTable, $this->tableSettings->partsDefaultColumns,
|
||||
"TABLE_PARTS_DEFAULT_COLUMNS");
|
||||
|
||||
$dataTable->addOrderBy('name')
|
||||
|
||||
@@ -318,6 +318,7 @@ abstract class AbstractStructuralDBElement extends AttachmentContainingDBElement
|
||||
return new ArrayCollection();
|
||||
}
|
||||
|
||||
//@phpstan-ignore-next-line
|
||||
return $this->children ?? new ArrayCollection();
|
||||
}
|
||||
|
||||
|
||||
@@ -208,7 +208,7 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu
|
||||
*/
|
||||
#[Groups(['parameter:read', 'full'])]
|
||||
#[SerializedName('formatted')]
|
||||
public function getFormattedValue(): string
|
||||
public function getFormattedValue(bool $latex_formatted = false): string
|
||||
{
|
||||
//If we just only have text value, return early
|
||||
if (null === $this->value_typical && null === $this->value_min && null === $this->value_max) {
|
||||
@@ -217,20 +217,20 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu
|
||||
|
||||
$str = '';
|
||||
$bracket_opened = false;
|
||||
if ($this->value_typical) {
|
||||
$str .= $this->getValueTypicalWithUnit();
|
||||
if ($this->value_typical !== null) {
|
||||
$str .= $this->getValueTypicalWithUnit($latex_formatted);
|
||||
if ($this->value_min || $this->value_max) {
|
||||
$bracket_opened = true;
|
||||
$str .= ' (';
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->value_max && $this->value_min) {
|
||||
$str .= $this->getValueMinWithUnit().' ... '.$this->getValueMaxWithUnit();
|
||||
} elseif ($this->value_max) {
|
||||
$str .= 'max. '.$this->getValueMaxWithUnit();
|
||||
} elseif ($this->value_min) {
|
||||
$str .= 'min. '.$this->getValueMinWithUnit();
|
||||
if ($this->value_max !== null && $this->value_min !== null) {
|
||||
$str .= $this->getValueMinWithUnit($latex_formatted).' ... '.$this->getValueMaxWithUnit($latex_formatted);
|
||||
} elseif ($this->value_max !== null) {
|
||||
$str .= 'max. '.$this->getValueMaxWithUnit($latex_formatted);
|
||||
} elseif ($this->value_min !== null) {
|
||||
$str .= 'min. '.$this->getValueMinWithUnit($latex_formatted);
|
||||
}
|
||||
|
||||
//Add closing bracket
|
||||
@@ -344,25 +344,25 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu
|
||||
/**
|
||||
* Return a formatted version with the minimum value with the unit of this parameter.
|
||||
*/
|
||||
public function getValueTypicalWithUnit(): string
|
||||
public function getValueTypicalWithUnit(bool $with_latex = false): string
|
||||
{
|
||||
return $this->formatWithUnit($this->value_typical);
|
||||
return $this->formatWithUnit($this->value_typical, with_latex: $with_latex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a formatted version with the maximum value with the unit of this parameter.
|
||||
*/
|
||||
public function getValueMaxWithUnit(): string
|
||||
public function getValueMaxWithUnit(bool $with_latex = false): string
|
||||
{
|
||||
return $this->formatWithUnit($this->value_max);
|
||||
return $this->formatWithUnit($this->value_max, with_latex: $with_latex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a formatted version with the typical value with the unit of this parameter.
|
||||
*/
|
||||
public function getValueMinWithUnit(): string
|
||||
public function getValueMinWithUnit(bool $with_latex = false): string
|
||||
{
|
||||
return $this->formatWithUnit($this->value_min);
|
||||
return $this->formatWithUnit($this->value_min, with_latex: $with_latex);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -441,16 +441,26 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu
|
||||
/**
|
||||
* Return a string representation and (if possible) with its unit.
|
||||
*/
|
||||
protected function formatWithUnit(float $value, string $format = '%g'): string
|
||||
protected function formatWithUnit(float $value, string $format = '%g', bool $with_latex = false): string
|
||||
{
|
||||
$str = sprintf($format, $value);
|
||||
if ($this->unit !== '') {
|
||||
return $str.' '.$this->unit;
|
||||
|
||||
if (!$with_latex) {
|
||||
$unit = $this->unit;
|
||||
} else {
|
||||
//Escape the percentage sign for convenience (as latex uses it as comment and it is often used in units)
|
||||
$escaped = preg_replace('/\\\\?%/', "\\\\%", $this->unit);
|
||||
|
||||
$unit = '$\mathrm{'.$escaped.'}$';
|
||||
}
|
||||
|
||||
return $str.' '.$unit;
|
||||
}
|
||||
|
||||
return $str;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the class of the element that is allowed to be associated with this attachment.
|
||||
* @return string
|
||||
|
||||
35
src/Entity/SettingsEntry.php
Normal file
35
src/Entity/SettingsEntry.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?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\Entity;
|
||||
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Jbtronics\SettingsBundle\Entity\AbstractSettingsORMEntry;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity]
|
||||
class SettingsEntry extends AbstractSettingsORMEntry
|
||||
{
|
||||
#[ORM\Id, ORM\GeneratedValue, ORM\Column(type: Types::INTEGER)]
|
||||
protected int $id;
|
||||
}
|
||||
@@ -197,7 +197,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
|
||||
/**
|
||||
* @var string|null The language/locale the user prefers
|
||||
*/
|
||||
#[Assert\Language]
|
||||
#[Assert\Locale]
|
||||
#[Groups(['full', 'import', 'user:read'])]
|
||||
#[ORM\Column(name: 'config_language', type: Types::STRING, nullable: true)]
|
||||
protected ?string $language = '';
|
||||
|
||||
@@ -39,6 +39,8 @@ use App\Services\LogSystem\EventCommentHelper;
|
||||
use App\Services\LogSystem\EventLogger;
|
||||
use App\Services\LogSystem\EventUndoHelper;
|
||||
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
|
||||
use App\Settings\SystemSettings\HistorySettings;
|
||||
use Doctrine\Common\EventSubscriber;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Event\OnFlushEventArgs;
|
||||
use Doctrine\ORM\Event\PostFlushEventArgs;
|
||||
@@ -74,14 +76,15 @@ class EventLoggerListener
|
||||
];
|
||||
|
||||
protected const MAX_STRING_LENGTH = 2000;
|
||||
protected bool $save_new_data;
|
||||
|
||||
public function __construct(protected EventLogger $logger, protected SerializerInterface $serializer, protected EventCommentHelper $eventCommentHelper,
|
||||
protected bool $save_changed_fields, protected bool $save_changed_data, protected bool $save_removed_data, bool $save_new_data,
|
||||
protected PropertyAccessorInterface $propertyAccessor, protected EventUndoHelper $eventUndoHelper)
|
||||
public function __construct(
|
||||
protected EventLogger $logger,
|
||||
protected SerializerInterface $serializer,
|
||||
protected EventCommentHelper $eventCommentHelper,
|
||||
private readonly HistorySettings $settings,
|
||||
protected PropertyAccessorInterface $propertyAccessor,
|
||||
protected EventUndoHelper $eventUndoHelper)
|
||||
{
|
||||
//This option only makes sense if save_changed_data is true
|
||||
$this->save_new_data = $save_new_data && $save_changed_data;
|
||||
}
|
||||
|
||||
public function onFlush(OnFlushEventArgs $eventArgs): void
|
||||
@@ -200,14 +203,14 @@ class EventLoggerListener
|
||||
if ($this->eventUndoHelper->isUndo()) {
|
||||
$log->setUndoneEvent($this->eventUndoHelper->getUndoneEvent(), $this->eventUndoHelper->getMode());
|
||||
}
|
||||
if ($this->save_removed_data) {
|
||||
if ($this->settings->saveRemovedData) {
|
||||
//The 4th param is important here, as we delete the element...
|
||||
$this->saveChangeSet($entity, $log, $em, true);
|
||||
}
|
||||
$this->logger->logFromOnFlush($log);
|
||||
|
||||
//Check if we have to log CollectionElementDeleted entries
|
||||
if ($this->save_changed_data) {
|
||||
if ($this->settings->saveOldData) {
|
||||
$metadata = $em->getClassMetadata($entity::class);
|
||||
$mappings = $metadata->getAssociationMappings();
|
||||
//Check if class is whitelisted for CollectionElementDeleted entry
|
||||
@@ -243,9 +246,9 @@ class EventLoggerListener
|
||||
}
|
||||
|
||||
$log = new ElementEditedLogEntry($entity);
|
||||
if ($this->save_changed_data) {
|
||||
if ($this->settings->saveOldData) {
|
||||
$this->saveChangeSet($entity, $log, $em);
|
||||
} elseif ($this->save_changed_fields) {
|
||||
} elseif ($this->settings->saveChangedFields) {
|
||||
$changed_fields = array_keys($uow->getEntityChangeSet($entity));
|
||||
//Remove lastModified field, as this is always changed (gives us no additional info)
|
||||
$changed_fields = array_diff($changed_fields, ['lastModified']);
|
||||
@@ -313,7 +316,7 @@ class EventLoggerListener
|
||||
$changeSet = $uow->getEntityChangeSet($entity);
|
||||
$old_data = array_combine(array_keys($changeSet), array_column($changeSet, 0));
|
||||
//If save_new_data is enabled, we extract it from the change set
|
||||
if ($this->save_new_data) {
|
||||
if ($this->settings->saveNewData && $this->settings->saveOldData) { //Only useful if we save old data too
|
||||
$new_data = array_combine(array_keys($changeSet), array_column($changeSet, 1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ declare(strict_types=1);
|
||||
namespace App\EventSubscriber\UserSystem;
|
||||
|
||||
use App\Entity\UserSystem\User;
|
||||
use App\Settings\SystemSettings\LocalizationSettings;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpKernel\Event\ControllerEvent;
|
||||
@@ -33,7 +34,7 @@ use Symfony\Component\HttpKernel\KernelEvents;
|
||||
*/
|
||||
final class SetUserTimezoneSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(private readonly string $default_timezone, private readonly Security $security)
|
||||
public function __construct(private readonly LocalizationSettings $localizationSettings, private readonly Security $security)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -48,8 +49,8 @@ final class SetUserTimezoneSubscriber implements EventSubscriberInterface
|
||||
}
|
||||
|
||||
//Fill with default value if needed
|
||||
if (null === $timezone && $this->default_timezone !== '') {
|
||||
$timezone = $this->default_timezone;
|
||||
if (null === $timezone && $this->localizationSettings->timezone !== '') {
|
||||
$timezone = $this->localizationSettings->timezone;
|
||||
}
|
||||
|
||||
//If timezone was configured anywhere set it, otherwise just use the one from php.ini
|
||||
|
||||
@@ -25,6 +25,7 @@ namespace App\Form\AdminPages;
|
||||
use App\Entity\PriceInformations\Currency;
|
||||
use App\Entity\ProjectSystem\Project;
|
||||
use App\Entity\UserSystem\Group;
|
||||
use App\Services\LogSystem\EventCommentType;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use App\Entity\Base\AbstractNamedDBElement;
|
||||
use App\Entity\Base\AbstractStructuralDBElement;
|
||||
@@ -152,7 +153,7 @@ class BaseEntityAdminForm extends AbstractType
|
||||
$builder->add('log_comment', TextType::class, [
|
||||
'label' => 'edit.log_comment',
|
||||
'mapped' => false,
|
||||
'required' => $this->eventCommentNeededHelper->isCommentNeeded($is_new ? 'datastructure_create': 'datastructure_edit'),
|
||||
'required' => $this->eventCommentNeededHelper->isCommentNeeded($is_new ? EventCommentType::DATASTRUCTURE_CREATE: EventCommentType::DATASTRUCTURE_EDIT),
|
||||
'empty_data' => null,
|
||||
]);
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Form\AdminPages;
|
||||
|
||||
use App\Settings\SystemSettings\LocalizationSettings;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use App\Entity\Base\AbstractNamedDBElement;
|
||||
use App\Form\Type\BigDecimalMoneyType;
|
||||
@@ -32,7 +33,7 @@ use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class CurrencyAdminForm extends BaseEntityAdminForm
|
||||
{
|
||||
public function __construct(Security $security, EventCommentNeededHelper $eventCommentNeededHelper, private readonly string $base_currency)
|
||||
public function __construct(Security $security, EventCommentNeededHelper $eventCommentNeededHelper, private readonly LocalizationSettings $localizationSettings)
|
||||
{
|
||||
parent::__construct($security, $eventCommentNeededHelper);
|
||||
}
|
||||
@@ -51,7 +52,7 @@ class CurrencyAdminForm extends BaseEntityAdminForm
|
||||
$builder->add('exchange_rate', BigDecimalMoneyType::class, [
|
||||
'required' => false,
|
||||
'label' => 'currency.edit.exchange_rate',
|
||||
'currency' => $this->base_currency,
|
||||
'currency' => $this->localizationSettings->baseCurrency,
|
||||
'scale' => 6,
|
||||
'disabled' => !$this->security->isGranted($is_new ? 'create' : 'edit', $entity),
|
||||
]);
|
||||
|
||||
@@ -22,6 +22,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Form\AdminPages;
|
||||
|
||||
use App\Settings\SystemSettings\LocalizationSettings;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use App\Entity\Base\AbstractNamedDBElement;
|
||||
use App\Entity\PriceInformations\Currency;
|
||||
@@ -32,7 +33,7 @@ use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class SupplierForm extends CompanyForm
|
||||
{
|
||||
public function __construct(Security $security, EventCommentNeededHelper $eventCommentNeededHelper, protected string $base_currency)
|
||||
public function __construct(Security $security, EventCommentNeededHelper $eventCommentNeededHelper, private readonly LocalizationSettings $localizationSettings)
|
||||
{
|
||||
parent::__construct($security, $eventCommentNeededHelper);
|
||||
}
|
||||
@@ -53,7 +54,7 @@ class SupplierForm extends CompanyForm
|
||||
|
||||
$builder->add('shipping_costs', BigDecimalMoneyType::class, [
|
||||
'required' => false,
|
||||
'currency' => $this->base_currency,
|
||||
'currency' => $this->localizationSettings->baseCurrency,
|
||||
'scale' => 3,
|
||||
'label' => 'supplier.shipping_costs.label',
|
||||
'disabled' => !$this->security->isGranted($is_new ? 'create' : 'edit', $entity),
|
||||
|
||||
@@ -22,6 +22,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Settings\SystemSettings\AttachmentsSettings;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use App\Entity\Attachments\Attachment;
|
||||
use App\Entity\Attachments\AttachmentType;
|
||||
@@ -54,9 +55,7 @@ class AttachmentFormType extends AbstractType
|
||||
protected Security $security,
|
||||
protected AttachmentSubmitHandler $submitHandler,
|
||||
protected TranslatorInterface $translator,
|
||||
protected bool $allow_attachments_download,
|
||||
protected bool $download_by_default,
|
||||
protected string $max_file_size
|
||||
protected AttachmentsSettings $settings,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -108,7 +107,7 @@ class AttachmentFormType extends AbstractType
|
||||
'required' => false,
|
||||
'label' => 'attachment.edit.download_url',
|
||||
'mapped' => false,
|
||||
'disabled' => !$this->allow_attachments_download,
|
||||
'disabled' => !$this->settings->allowDownloads,
|
||||
]);
|
||||
|
||||
$builder->add('file', FileType::class, [
|
||||
@@ -177,7 +176,7 @@ class AttachmentFormType extends AbstractType
|
||||
|
||||
//If the attachment should be downloaded by default (and is download allowed at all), register a listener,
|
||||
// which sets the downloadURL checkbox to true for new attachments
|
||||
if ($this->download_by_default && $this->allow_attachments_download) {
|
||||
if ($this->settings->downloadByDefault && $this->settings->allowDownloads) {
|
||||
$builder->addEventListener(FormEvents::POST_SET_DATA, function (FormEvent $event): void {
|
||||
$form = $event->getForm();
|
||||
$attachment = $form->getData();
|
||||
@@ -204,7 +203,7 @@ class AttachmentFormType extends AbstractType
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Attachment::class,
|
||||
'max_file_size' => $this->max_file_size,
|
||||
'max_file_size' => $this->settings->maxFileSize,
|
||||
'allow_builtins' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
49
src/Form/History/EnforceEventCommentTypesType.php
Normal file
49
src/Form/History/EnforceEventCommentTypesType.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2024 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace App\Form\History;
|
||||
|
||||
use App\Services\LogSystem\EventCommentType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EnumType;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* The type for the "enforceComments" setting in the HistorySettings.
|
||||
*/
|
||||
class EnforceEventCommentTypesType extends AbstractType
|
||||
{
|
||||
public function getParent(): string
|
||||
{
|
||||
return EnumType::class;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'multiple' => true,
|
||||
'class' => EventCommentType::class,
|
||||
'empty_data' => [],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@ use App\Form\Type\SIUnitType;
|
||||
use App\Form\Type\StructuralEntityType;
|
||||
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
|
||||
use App\Services\LogSystem\EventCommentNeededHelper;
|
||||
use App\Services\LogSystem\EventCommentType;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
@@ -265,7 +266,7 @@ class PartBaseType extends AbstractType
|
||||
$builder->add('log_comment', TextType::class, [
|
||||
'label' => 'edit.log_comment',
|
||||
'mapped' => false,
|
||||
'required' => $this->event_comment_needed_helper->isCommentNeeded($new_part ? 'part_create' : 'part_edit'),
|
||||
'required' => $this->event_comment_needed_helper->isCommentNeeded($new_part ? EventCommentType::PART_CREATE : EventCommentType::PART_EDIT),
|
||||
'empty_data' => null,
|
||||
]);
|
||||
|
||||
|
||||
60
src/Form/SelectTypeOrderExtension.php
Normal file
60
src/Form/SelectTypeOrderExtension.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?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\Form;
|
||||
|
||||
use Symfony\Component\Form\AbstractTypeExtension;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EnumType;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\OptionsResolver\Options;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class SelectTypeOrderExtension extends AbstractTypeExtension
|
||||
{
|
||||
public static function getExtendedTypes(): iterable
|
||||
{
|
||||
return [
|
||||
ChoiceType::class,
|
||||
EnumType::class
|
||||
];
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefault('ordered', false);
|
||||
$resolver->setDefault('by_reference', function (Options $options) {
|
||||
//Disable by_reference if the field is ordered (otherwise the order will be lost)
|
||||
return !$options['ordered'];
|
||||
});
|
||||
}
|
||||
|
||||
public function buildView(FormView $view, FormInterface $form, array $options)
|
||||
{
|
||||
//Pass the data in ordered form to the frontend controller, so it can make the items appear in the correct order.
|
||||
if ($options['ordered']) {
|
||||
$view->vars['attr']['data-ordered-value'] = json_encode($form->getViewData(), JSON_THROW_ON_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ namespace App\Form\Type;
|
||||
use App\Entity\PriceInformations\Currency;
|
||||
use App\Form\Type\Helper\StructuralEntityChoiceHelper;
|
||||
use App\Services\Trees\NodesListBuilder;
|
||||
use App\Settings\SystemSettings\LocalizationSettings;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Intl\Currencies;
|
||||
use Symfony\Component\OptionsResolver\Options;
|
||||
@@ -36,7 +37,7 @@ use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
*/
|
||||
class CurrencyEntityType extends StructuralEntityType
|
||||
{
|
||||
public function __construct(EntityManagerInterface $em, NodesListBuilder $builder, TranslatorInterface $translator, StructuralEntityChoiceHelper $choiceHelper, protected ?string $base_currency)
|
||||
public function __construct(EntityManagerInterface $em, NodesListBuilder $builder, TranslatorInterface $translator, StructuralEntityChoiceHelper $choiceHelper, private readonly LocalizationSettings $localizationSettings)
|
||||
{
|
||||
parent::__construct($em, $builder, $translator, $choiceHelper);
|
||||
}
|
||||
@@ -57,7 +58,7 @@ class CurrencyEntityType extends StructuralEntityType
|
||||
|
||||
$resolver->setDefault('empty_message', function (Options $options) {
|
||||
//By default, we use the global base currency:
|
||||
$iso_code = $this->base_currency;
|
||||
$iso_code = $this->localizationSettings->baseCurrency;
|
||||
|
||||
if ($options['base_currency']) { //Allow to override it
|
||||
$iso_code = $options['base_currency'];
|
||||
|
||||
53
src/Form/Type/LocaleSelectType.php
Normal file
53
src/Form/Type/LocaleSelectType.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?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\Form\Type;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\LocaleType;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* A locale select field that uses the preferred languages from the configuration.
|
||||
|
||||
*/
|
||||
class LocaleSelectType extends AbstractType
|
||||
{
|
||||
|
||||
public function __construct(#[Autowire(param: 'partdb.locale_menu')] private readonly array $preferred_languages)
|
||||
{
|
||||
|
||||
}
|
||||
public function getParent(): string
|
||||
{
|
||||
return LocaleType::class;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'preferred_choices' => $this->preferred_languages,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Form\Type\LocaleSelectType;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use App\Entity\Base\AbstractNamedDBElement;
|
||||
use App\Entity\UserSystem\Group;
|
||||
@@ -35,7 +36,6 @@ use App\Form\Type\ThemeChoiceType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\LanguageType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ResetType;
|
||||
@@ -140,11 +140,10 @@ class UserAdminForm extends AbstractType
|
||||
])
|
||||
|
||||
//Config section
|
||||
->add('language', LanguageType::class, [
|
||||
->add('language', LocaleSelectType::class, [
|
||||
'required' => false,
|
||||
'placeholder' => 'user_settings.language.placeholder',
|
||||
'label' => 'user.language_select',
|
||||
'preferred_choices' => ['en', 'de'],
|
||||
'disabled' => !$this->security->isGranted('change_user_settings', $entity),
|
||||
])
|
||||
->add('timezone', TimezoneType::class, [
|
||||
|
||||
@@ -22,6 +22,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Form\Type\LocaleSelectType;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use App\Entity\UserSystem\User;
|
||||
use App\Form\Type\CurrencyEntityType;
|
||||
@@ -33,7 +34,6 @@ use Symfony\Component\Form\Event\PreSetDataEvent;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FileType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\LanguageType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ResetType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
@@ -47,7 +47,7 @@ class UserSettingsType extends AbstractType
|
||||
{
|
||||
public function __construct(protected Security $security,
|
||||
protected bool $demo_mode,
|
||||
#[Autowire(param: 'partdb.locale_menu')] private readonly array $preferred_languages)
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -107,12 +107,11 @@ class UserSettingsType extends AbstractType
|
||||
'mode' => 'markdown-full',
|
||||
'disabled' => !$this->security->isGranted('edit_infos', $options['data']) || $this->demo_mode,
|
||||
])
|
||||
->add('language', LanguageType::class, [
|
||||
->add('language', LocaleSelectType::class, [
|
||||
'disabled' => $this->demo_mode,
|
||||
'required' => false,
|
||||
'placeholder' => 'user_settings.language.placeholder',
|
||||
'label' => 'user.language_select',
|
||||
'preferred_choices' => $this->preferred_languages,
|
||||
])
|
||||
->add('timezone', TimezoneType::class, [
|
||||
'disabled' => $this->demo_mode,
|
||||
|
||||
@@ -66,7 +66,7 @@ class AttachmentRepository extends DBElementRepository
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the count of all external attachments (attachments containing an external path).
|
||||
* Gets the count of all external attachments (attachments containing only an external path).
|
||||
*
|
||||
* @throws NoResultException
|
||||
* @throws NonUniqueResultException
|
||||
@@ -75,8 +75,9 @@ class AttachmentRepository extends DBElementRepository
|
||||
{
|
||||
$qb = $this->createQueryBuilder('attachment');
|
||||
$qb->select('COUNT(attachment)')
|
||||
->andWhere('attaachment.internal_path IS NULL')
|
||||
->where('attachment.external_path IS NOT NULL');
|
||||
->where('attachment.external_path IS NOT NULL')
|
||||
->andWhere('attachment.internal_path IS NULL');
|
||||
|
||||
$query = $qb->getQuery();
|
||||
|
||||
return (int) $query->getSingleScalarResult();
|
||||
|
||||
@@ -40,6 +40,7 @@ use App\Entity\Attachments\StorageLocationAttachment;
|
||||
use App\Entity\Attachments\SupplierAttachment;
|
||||
use App\Entity\Attachments\UserAttachment;
|
||||
use App\Exceptions\AttachmentDownloadException;
|
||||
use App\Settings\SystemSettings\AttachmentsSettings;
|
||||
use Hshn\Base64EncodedFile\HttpFoundation\File\Base64EncodedFile;
|
||||
use Hshn\Base64EncodedFile\HttpFoundation\File\UploadedBase64EncodedFile;
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
@@ -64,12 +65,14 @@ class AttachmentSubmitHandler
|
||||
'asp', 'cgi', 'py', 'pl', 'exe', 'aspx', 'js', 'mjs', 'jsp', 'css', 'jar', 'html', 'htm', 'shtm', 'shtml', 'htaccess',
|
||||
'htpasswd', ''];
|
||||
|
||||
public function __construct(protected AttachmentPathResolver $pathResolver, protected bool $allow_attachments_downloads,
|
||||
protected HttpClientInterface $httpClient, protected MimeTypesInterface $mimeTypes,
|
||||
protected FileTypeFilterTools $filterTools, /**
|
||||
* @var string The user configured maximum upload size. This is a string like "10M" or "1G" and will be converted to
|
||||
*/
|
||||
protected string $max_upload_size)
|
||||
public function __construct(
|
||||
protected AttachmentPathResolver $pathResolver,
|
||||
protected HttpClientInterface $httpClient,
|
||||
protected MimeTypesInterface $mimeTypes,
|
||||
protected FileTypeFilterTools $filterTools,
|
||||
protected AttachmentsSettings $settings,
|
||||
protected readonly SVGSanitizer $SVGSanitizer,
|
||||
)
|
||||
{
|
||||
//The mapping used to determine which folder will be used for an attachment type
|
||||
$this->folder_mapping = [
|
||||
@@ -214,6 +217,9 @@ class AttachmentSubmitHandler
|
||||
//Move the attachment files to secure location (and back) if needed
|
||||
$this->moveFile($attachment, $secure_attachment);
|
||||
|
||||
//Sanitize the SVG if needed
|
||||
$this->sanitizeSVGAttachment($attachment);
|
||||
|
||||
//Rename blacklisted (unsecure) files to a better extension
|
||||
$this->renameBlacklistedExtensions($attachment);
|
||||
|
||||
@@ -334,7 +340,7 @@ class AttachmentSubmitHandler
|
||||
protected function downloadURL(Attachment $attachment, bool $secureAttachment): Attachment
|
||||
{
|
||||
//Check if we are allowed to download files
|
||||
if (!$this->allow_attachments_downloads) {
|
||||
if (!$this->settings->allowDownloads) {
|
||||
throw new RuntimeException('Download of attachments is not allowed!');
|
||||
}
|
||||
|
||||
@@ -345,9 +351,28 @@ class AttachmentSubmitHandler
|
||||
$tmp_path = $attachment_folder.DIRECTORY_SEPARATOR.$this->generateAttachmentFilename($attachment, 'tmp');
|
||||
|
||||
try {
|
||||
$response = $this->httpClient->request('GET', $url, [
|
||||
$opts = [
|
||||
'buffer' => false,
|
||||
]);
|
||||
//Use user-agent and other headers to make the server think we are a browser
|
||||
'headers' => [
|
||||
"sec-ch-ua" => "\"Not(A:Brand\";v=\"99\", \"Google Chrome\";v=\"133\", \"Chromium\";v=\"133\"",
|
||||
"sec-ch-ua-mobile" => "?0",
|
||||
"sec-ch-ua-platform" => "\"Windows\"",
|
||||
"user-agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36",
|
||||
"sec-fetch-site" => "none",
|
||||
"sec-fetch-mode" => "navigate",
|
||||
],
|
||||
|
||||
];
|
||||
$response = $this->httpClient->request('GET', $url, $opts);
|
||||
//Digikey wants TLSv1.3, so try again with that if we get a 403
|
||||
if ($response->getStatusCode() === 403) {
|
||||
$opts['crypto_method'] = STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT;
|
||||
$response = $this->httpClient->request('GET', $url, $opts);
|
||||
}
|
||||
# if you have these changes and downloads still fail, check if it's due to an unknown certificate. Curl by
|
||||
# default uses the systems ca store and that doesn't contain all the intermediate certificates needed to
|
||||
# verify the leafs
|
||||
|
||||
if (200 !== $response->getStatusCode()) {
|
||||
throw new AttachmentDownloadException('Status code: '.$response->getStatusCode());
|
||||
@@ -474,9 +499,37 @@ class AttachmentSubmitHandler
|
||||
$this->max_upload_size_bytes = min(
|
||||
$this->parseFileSizeString(ini_get('post_max_size')),
|
||||
$this->parseFileSizeString(ini_get('upload_max_filesize')),
|
||||
$this->parseFileSizeString($this->max_upload_size),
|
||||
$this->parseFileSizeString($this->settings->maxFileSize)
|
||||
);
|
||||
|
||||
return $this->max_upload_size_bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes the given SVG file, if the attachment is an internal SVG file.
|
||||
* @param Attachment $attachment
|
||||
* @return Attachment
|
||||
*/
|
||||
public function sanitizeSVGAttachment(Attachment $attachment): Attachment
|
||||
{
|
||||
//We can not do anything on builtins or external ressources
|
||||
if ($attachment->isBuiltIn() || !$attachment->hasInternal()) {
|
||||
return $attachment;
|
||||
}
|
||||
|
||||
//Resolve the path to the file
|
||||
$path = $this->pathResolver->placeholderToRealPath($attachment->getInternalPath());
|
||||
|
||||
//Check if the file exists
|
||||
if (!file_exists($path)) {
|
||||
return $attachment;
|
||||
}
|
||||
|
||||
//Check if the file is an SVG
|
||||
if ($attachment->getExtension() === "svg") {
|
||||
$this->SVGSanitizer->sanitizeFile($path);
|
||||
}
|
||||
|
||||
return $attachment;
|
||||
}
|
||||
}
|
||||
|
||||
58
src/Services/Attachments/SVGSanitizer.php
Normal file
58
src/Services/Attachments/SVGSanitizer.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?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\Services\Attachments;
|
||||
|
||||
use Rhukster\DomSanitizer\DOMSanitizer;
|
||||
|
||||
class SVGSanitizer
|
||||
{
|
||||
|
||||
/**
|
||||
* Sanitizes the given SVG string by removing any potentially harmful content (like inline scripts).
|
||||
* @param string $input
|
||||
* @return string
|
||||
*/
|
||||
public function sanitizeString(string $input): string
|
||||
{
|
||||
return (new DOMSanitizer(DOMSanitizer::SVG))->sanitize($input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes the given SVG file by removing any potentially harmful content (like inline scripts).
|
||||
* The sanitized content is written back to the file.
|
||||
* @param string $filepath
|
||||
*/
|
||||
public function sanitizeFile(string $filepath): void
|
||||
{
|
||||
//Open the file and read the content
|
||||
$content = file_get_contents($filepath);
|
||||
if ($content === false) {
|
||||
throw new \RuntimeException('Could not read file: ' . $filepath);
|
||||
}
|
||||
//Sanitize the content
|
||||
$sanitizedContent = $this->sanitizeString($content);
|
||||
//Write the sanitized content back to the file
|
||||
file_put_contents($filepath, $sanitizedContent);
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,9 @@ use App\Entity\Parts\Category;
|
||||
use App\Entity\Parts\Footprint;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Services\Cache\ElementCacheTagGenerator;
|
||||
use App\Services\EntityURLGenerator;
|
||||
use App\Services\Trees\NodesListBuilder;
|
||||
use App\Settings\MiscSettings\KiCadEDASettings;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
@@ -38,16 +40,20 @@ use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
class KiCadHelper
|
||||
{
|
||||
|
||||
/** @var int The maximum level of the shown categories. 0 Means only the top level categories are shown. -1 means only a single one containing */
|
||||
private readonly int $category_depth;
|
||||
|
||||
public function __construct(
|
||||
private readonly NodesListBuilder $nodesListBuilder,
|
||||
private readonly TagAwareCacheInterface $kicadCache,
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly ElementCacheTagGenerator $tagGenerator,
|
||||
private readonly UrlGeneratorInterface $urlGenerator,
|
||||
private readonly EntityURLGenerator $entityURLGenerator,
|
||||
private readonly TranslatorInterface $translator,
|
||||
/** The maximum level of the shown categories. 0 Means only the top level categories are shown. -1 means only a single one containing */
|
||||
private readonly int $category_depth,
|
||||
KiCadEDASettings $kiCadEDASettings,
|
||||
) {
|
||||
$this->category_depth = $kiCadEDASettings->categoryDepth;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,6 +70,10 @@ class KiCadHelper
|
||||
$secure_class_name = $this->tagGenerator->getElementTypeCacheTag(Category::class);
|
||||
$item->tag($secure_class_name);
|
||||
|
||||
//Invalidate the cache on part changes (as the visibility depends on parts, and the parts can change)
|
||||
$secure_class_name = $this->tagGenerator->getElementTypeCacheTag(Part::class);
|
||||
$item->tag($secure_class_name);
|
||||
|
||||
//If the category depth is smaller than 0, create only one dummy category
|
||||
if ($this->category_depth < 0) {
|
||||
return [
|
||||
@@ -108,6 +118,8 @@ class KiCadHelper
|
||||
$result[] = [
|
||||
'id' => (string)$category->getId(),
|
||||
'name' => $category->getFullPath('/'),
|
||||
//Show the category link as the category description, this also fixes an segfault in KiCad see issue #878
|
||||
'description' => $this->entityURLGenerator->listPartsURL($category),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -229,6 +241,49 @@ class KiCadHelper
|
||||
$result["fields"]["Part-DB IPN"] = $this->createField($part->getIpn());
|
||||
}
|
||||
|
||||
// Add supplier information from orderdetails (include obsolete orderdetails)
|
||||
if ($part->getOrderdetails(false)->count() > 0) {
|
||||
$supplierCounts = [];
|
||||
|
||||
foreach ($part->getOrderdetails(false) as $orderdetail) {
|
||||
if ($orderdetail->getSupplier() !== null && $orderdetail->getSupplierPartNr() !== '') {
|
||||
$supplierName = $orderdetail->getSupplier()->getName();
|
||||
|
||||
$supplierName .= " SPN"; // Append "SPN" to the supplier name to indicate Supplier Part Number
|
||||
|
||||
if (!isset($supplierCounts[$supplierName])) {
|
||||
$supplierCounts[$supplierName] = 0;
|
||||
}
|
||||
$supplierCounts[$supplierName]++;
|
||||
|
||||
// Create field name with sequential number if more than one from same supplier (e.g. "Mouser", "Mouser 2", etc.)
|
||||
$fieldName = $supplierCounts[$supplierName] > 1
|
||||
? $supplierName . ' ' . $supplierCounts[$supplierName]
|
||||
: $supplierName;
|
||||
|
||||
$result["fields"][$fieldName] = $this->createField($orderdetail->getSupplierPartNr());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Add fields for KiCost:
|
||||
if ($part->getManufacturer() !== null) {
|
||||
$result["fields"]["manf"] = $this->createField($part->getManufacturer()->getName());
|
||||
}
|
||||
if ($part->getManufacturerProductNumber() !== "") {
|
||||
$result['fields']['manf#'] = $this->createField($part->getManufacturerProductNumber());
|
||||
}
|
||||
|
||||
//For each supplier, add a field with the supplier name and the supplier part number for KiCost
|
||||
if ($part->getOrderdetails(false)->count() > 0) {
|
||||
foreach ($part->getOrderdetails(false) as $orderdetail) {
|
||||
if ($orderdetail->getSupplier() !== null && $orderdetail->getSupplierPartNr() !== '') {
|
||||
$fieldName = mb_strtolower($orderdetail->getSupplier()->getName()) . '#';
|
||||
|
||||
$result["fields"][$fieldName] = $this->createField($orderdetail->getSupplierPartNr());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
@@ -156,8 +156,10 @@ class EntityURLGenerator
|
||||
|
||||
public function viewURL(Attachment $entity): string
|
||||
{
|
||||
if ($entity->hasInternal()) {
|
||||
return $this->attachmentURLGenerator->getInternalViewURL($entity);
|
||||
//If the underlying file path is invalid, null gets returned, which is not allowed here.
|
||||
//We still have the chance to use an external path, if it is set.
|
||||
if ($entity->hasInternal() && ($url = $this->attachmentURLGenerator->getInternalViewURL($entity)) !== null) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
if($entity->hasExternal()) {
|
||||
|
||||
@@ -23,6 +23,7 @@ declare(strict_types=1);
|
||||
namespace App\Services\Formatters;
|
||||
|
||||
use App\Entity\PriceInformations\Currency;
|
||||
use App\Settings\SystemSettings\LocalizationSettings;
|
||||
use Locale;
|
||||
use NumberFormatter;
|
||||
|
||||
@@ -30,7 +31,7 @@ class MoneyFormatter
|
||||
{
|
||||
protected string $locale;
|
||||
|
||||
public function __construct(protected string $base_currency)
|
||||
public function __construct(private readonly LocalizationSettings $localizationSettings)
|
||||
{
|
||||
$this->locale = Locale::getDefault();
|
||||
}
|
||||
@@ -45,7 +46,7 @@ class MoneyFormatter
|
||||
*/
|
||||
public function format(string|float $value, ?Currency $currency = null, int $decimals = 5, bool $show_all_digits = false): string
|
||||
{
|
||||
$iso_code = $this->base_currency;
|
||||
$iso_code = $this->localizationSettings->baseCurrency;
|
||||
if ($currency instanceof Currency && ($currency->getIsoCode() !== '')) {
|
||||
$iso_code = $currency->getIsoCode();
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ use App\Entity\Parts\Supplier;
|
||||
use App\Entity\PriceInformations\Currency;
|
||||
use App\Entity\PriceInformations\Orderdetail;
|
||||
use App\Entity\PriceInformations\Pricedetail;
|
||||
use App\Settings\SystemSettings\LocalizationSettings;
|
||||
use Brick\Math\BigDecimal;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Intl\Currencies;
|
||||
@@ -47,7 +48,7 @@ class PKPartImporter
|
||||
{
|
||||
use PKImportHelperTrait;
|
||||
|
||||
public function __construct(EntityManagerInterface $em, PropertyAccessorInterface $propertyAccessor, private readonly string $base_currency)
|
||||
public function __construct(EntityManagerInterface $em, PropertyAccessorInterface $propertyAccessor, private readonly LocalizationSettings $localizationSettings)
|
||||
{
|
||||
$this->em = $em;
|
||||
$this->propertyAccessor = $propertyAccessor;
|
||||
@@ -210,7 +211,7 @@ class PKPartImporter
|
||||
$currency_iso_code = strtoupper($currency_iso_code);
|
||||
|
||||
//We do not have a currency for the base currency to be consistent with prices without currencies
|
||||
if ($currency_iso_code === $this->base_currency) {
|
||||
if ($currency_iso_code === $this->localizationSettings->baseCurrency) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PriceDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
|
||||
use App\Settings\SystemSettings\LocalizationSettings;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
/**
|
||||
@@ -54,8 +55,11 @@ final class DTOtoEntityConverter
|
||||
private const TYPE_DATASHEETS_NAME = 'Datasheet';
|
||||
private const TYPE_IMAGE_NAME = 'Image';
|
||||
|
||||
public function __construct(private readonly EntityManagerInterface $em, private readonly string $base_currency)
|
||||
private readonly string $base_currency;
|
||||
|
||||
public function __construct(private readonly EntityManagerInterface $em, LocalizationSettings $localizationSettings)
|
||||
{
|
||||
$this->base_currency = $localizationSettings->baseCurrency;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,6 +31,7 @@ use App\Services\InfoProviderSystem\DTOs\PriceDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
|
||||
use App\Services\OAuth\OAuthTokenManager;
|
||||
use App\Settings\InfoProviderSystem\DigikeySettings;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class DigikeyProvider implements InfoProviderInterface
|
||||
@@ -55,17 +56,16 @@ class DigikeyProvider implements InfoProviderInterface
|
||||
];
|
||||
|
||||
public function __construct(HttpClientInterface $httpClient, private readonly OAuthTokenManager $authTokenManager,
|
||||
private readonly string $currency, private readonly string $clientId,
|
||||
private readonly string $language, private readonly string $country)
|
||||
private readonly DigikeySettings $settings,)
|
||||
{
|
||||
//Create the HTTP client with some default options
|
||||
$this->digikeyClient = $httpClient->withOptions([
|
||||
"base_uri" => self::BASE_URI,
|
||||
"headers" => [
|
||||
"X-DIGIKEY-Client-Id" => $clientId,
|
||||
"X-DIGIKEY-Locale-Site" => $this->country,
|
||||
"X-DIGIKEY-Locale-Language" => $this->language,
|
||||
"X-DIGIKEY-Locale-Currency" => $this->currency,
|
||||
"X-DIGIKEY-Client-Id" => $this->settings->clientId,
|
||||
"X-DIGIKEY-Locale-Site" => $this->settings->country,
|
||||
"X-DIGIKEY-Locale-Language" => $this->settings->language,
|
||||
"X-DIGIKEY-Locale-Currency" => $this->settings->currency,
|
||||
"X-DIGIKEY-Customer-Id" => 0,
|
||||
]
|
||||
]);
|
||||
@@ -101,19 +101,22 @@ class DigikeyProvider implements InfoProviderInterface
|
||||
public function isActive(): bool
|
||||
{
|
||||
//The client ID has to be set and a token has to be available (user clicked connect)
|
||||
return $this->clientId !== '' && $this->authTokenManager->hasToken(self::OAUTH_APP_NAME);
|
||||
return $this->settings->clientId !== '' && $this->authTokenManager->hasToken(self::OAUTH_APP_NAME);
|
||||
}
|
||||
|
||||
public function searchByKeyword(string $keyword): array
|
||||
{
|
||||
$request = [
|
||||
'Keywords' => $keyword,
|
||||
'RecordCount' => 50,
|
||||
'RecordStartPosition' => 0,
|
||||
'ExcludeMarketPlaceProducts' => 'true',
|
||||
'Limit' => 50,
|
||||
'Offset' => 0,
|
||||
'FilterOptionsRequest' => [
|
||||
'MarketPlaceFilter' => 'ExcludeMarketPlace',
|
||||
],
|
||||
];
|
||||
|
||||
$response = $this->digikeyClient->request('POST', '/Search/v3/Products/Keyword', [
|
||||
//$response = $this->digikeyClient->request('POST', '/Search/v3/Products/Keyword', [
|
||||
$response = $this->digikeyClient->request('POST', '/products/v4/search/keyword', [
|
||||
'json' => $request,
|
||||
'auth_bearer' => $this->authTokenManager->getAlwaysValidTokenString(self::OAUTH_APP_NAME)
|
||||
]);
|
||||
@@ -124,18 +127,21 @@ class DigikeyProvider implements InfoProviderInterface
|
||||
$result = [];
|
||||
$products = $response_array['Products'];
|
||||
foreach ($products as $product) {
|
||||
$result[] = new SearchResultDTO(
|
||||
provider_key: $this->getProviderKey(),
|
||||
provider_id: $product['DigiKeyPartNumber'],
|
||||
name: $product['ManufacturerPartNumber'],
|
||||
description: $product['DetailedDescription'] ?? $product['ProductDescription'],
|
||||
category: $this->getCategoryString($product),
|
||||
manufacturer: $product['Manufacturer']['Value'] ?? null,
|
||||
mpn: $product['ManufacturerPartNumber'],
|
||||
preview_image_url: $product['PrimaryPhoto'] ?? null,
|
||||
manufacturing_status: $this->productStatusToManufacturingStatus($product['ProductStatus']),
|
||||
provider_url: $product['ProductUrl'],
|
||||
);
|
||||
foreach ($product['ProductVariations'] as $variation) {
|
||||
$result[] = new SearchResultDTO(
|
||||
provider_key: $this->getProviderKey(),
|
||||
provider_id: $variation['DigiKeyProductNumber'],
|
||||
name: $product['ManufacturerProductNumber'],
|
||||
description: $product['Description']['DetailedDescription'] ?? $product['Description']['ProductDescription'],
|
||||
category: $this->getCategoryString($product),
|
||||
manufacturer: $product['Manufacturer']['Name'] ?? null,
|
||||
mpn: $product['ManufacturerProductNumber'],
|
||||
preview_image_url: $product['PhotoUrl'] ?? null,
|
||||
manufacturing_status: $this->productStatusToManufacturingStatus($product['ProductStatus']['Id']),
|
||||
provider_url: $product['ProductUrl'],
|
||||
footprint: $variation['PackageType']['Name'], //Use the footprint field, to show the user the package type (Tape & Reel, etc., as digikey has many different package types)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
@@ -143,62 +149,79 @@ class DigikeyProvider implements InfoProviderInterface
|
||||
|
||||
public function getDetails(string $id): PartDetailDTO
|
||||
{
|
||||
$response = $this->digikeyClient->request('GET', '/Search/v3/Products/' . urlencode($id), [
|
||||
$response = $this->digikeyClient->request('GET', '/products/v4/search/' . urlencode($id) . '/productdetails', [
|
||||
'auth_bearer' => $this->authTokenManager->getAlwaysValidTokenString(self::OAUTH_APP_NAME)
|
||||
]);
|
||||
|
||||
$product = $response->toArray();
|
||||
$response_array = $response->toArray();
|
||||
$product = $response_array['Product'];
|
||||
|
||||
$footprint = null;
|
||||
$parameters = $this->parametersToDTOs($product['Parameters'] ?? [], $footprint);
|
||||
$media = $this->mediaToDTOs($product['MediaLinks']);
|
||||
$media = $this->mediaToDTOs($id);
|
||||
|
||||
// Get the price_breaks of the selected variation
|
||||
$price_breaks = [];
|
||||
foreach ($product['ProductVariations'] as $variation) {
|
||||
if ($variation['DigiKeyProductNumber'] == $id) {
|
||||
$price_breaks = $variation['StandardPricing'] ?? [];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new PartDetailDTO(
|
||||
provider_key: $this->getProviderKey(),
|
||||
provider_id: $product['DigiKeyPartNumber'],
|
||||
name: $product['ManufacturerPartNumber'],
|
||||
description: $product['DetailedDescription'] ?? $product['ProductDescription'],
|
||||
provider_id: $id,
|
||||
name: $product['ManufacturerProductNumber'],
|
||||
description: $product['Description']['DetailedDescription'] ?? $product['Description']['ProductDescription'],
|
||||
category: $this->getCategoryString($product),
|
||||
manufacturer: $product['Manufacturer']['Value'] ?? null,
|
||||
mpn: $product['ManufacturerPartNumber'],
|
||||
preview_image_url: $product['PrimaryPhoto'] ?? null,
|
||||
manufacturing_status: $this->productStatusToManufacturingStatus($product['ProductStatus']),
|
||||
manufacturer: $product['Manufacturer']['Name'] ?? null,
|
||||
mpn: $product['ManufacturerProductNumber'],
|
||||
preview_image_url: $product['PhotoUrl'] ?? null,
|
||||
manufacturing_status: $this->productStatusToManufacturingStatus($product['ProductStatus']['Id']),
|
||||
provider_url: $product['ProductUrl'],
|
||||
footprint: $footprint,
|
||||
datasheets: $media['datasheets'],
|
||||
images: $media['images'],
|
||||
parameters: $parameters,
|
||||
vendor_infos: $this->pricingToDTOs($product['StandardPricing'] ?? [], $product['DigiKeyPartNumber'], $product['ProductUrl']),
|
||||
vendor_infos: $this->pricingToDTOs($price_breaks, $id, $product['ProductUrl']),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the product status from the Digikey API to the manufacturing status used in Part-DB
|
||||
* @param string|null $dk_status
|
||||
* @param int|null $dk_status
|
||||
* @return ManufacturingStatus|null
|
||||
*/
|
||||
private function productStatusToManufacturingStatus(?string $dk_status): ?ManufacturingStatus
|
||||
private function productStatusToManufacturingStatus(?int $dk_status): ?ManufacturingStatus
|
||||
{
|
||||
// The V4 can use strings to get the status, but if you have changed the PROVIDER_DIGIKEY_LANGUAGE it will not match.
|
||||
// Using the Id instead which should be fixed.
|
||||
//
|
||||
// The API is not well documented and the ID are not there yet, so were extracted using "trial and error".
|
||||
// The 'Preliminary' id was not found in several categories so I was unable to extract it. Disabled for now.
|
||||
return match ($dk_status) {
|
||||
null => null,
|
||||
'Active' => ManufacturingStatus::ACTIVE,
|
||||
'Obsolete' => ManufacturingStatus::DISCONTINUED,
|
||||
'Discontinued at Digi-Key', 'Last Time Buy' => ManufacturingStatus::EOL,
|
||||
'Not For New Designs' => ManufacturingStatus::NRFND,
|
||||
'Preliminary' => ManufacturingStatus::ANNOUNCED,
|
||||
0 => ManufacturingStatus::ACTIVE,
|
||||
1 => ManufacturingStatus::DISCONTINUED,
|
||||
2, 4 => ManufacturingStatus::EOL,
|
||||
7 => ManufacturingStatus::NRFND,
|
||||
//'Preliminary' => ManufacturingStatus::ANNOUNCED,
|
||||
default => ManufacturingStatus::NOT_SET,
|
||||
};
|
||||
}
|
||||
|
||||
private function getCategoryString(array $product): string
|
||||
{
|
||||
$category = $product['Category']['Value'];
|
||||
$sub_category = $product['Family']['Value'];
|
||||
$category = $product['Category']['Name'];
|
||||
$sub_category = current($product['Category']['ChildCategories']);
|
||||
|
||||
//Replace the ' - ' category separator with ' -> '
|
||||
$sub_category = str_replace(' - ', ' -> ', $sub_category);
|
||||
if ($sub_category) {
|
||||
//Replace the ' - ' category separator with ' -> '
|
||||
$category = $category . ' -> ' . str_replace(' - ', ' -> ', $sub_category["Name"]);
|
||||
}
|
||||
|
||||
return $category . ' -> ' . $sub_category;
|
||||
return $category;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -215,18 +238,18 @@ class DigikeyProvider implements InfoProviderInterface
|
||||
|
||||
foreach ($parameters as $parameter) {
|
||||
if ($parameter['ParameterId'] === 1291) { //Meaning "Manufacturer given footprint"
|
||||
$footprint_name = $parameter['Value'];
|
||||
$footprint_name = $parameter['ValueText'];
|
||||
}
|
||||
|
||||
if (in_array(trim((string) $parameter['Value']), ['', '-'], true)) {
|
||||
if (in_array(trim((string) $parameter['ValueText']), ['', '-'], true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
//If the parameter was marked as text only, then we do not try to parse it as a numerical value
|
||||
if (in_array($parameter['ParameterId'], self::TEXT_ONLY_PARAMETERS, true)) {
|
||||
$results[] = new ParameterDTO(name: $parameter['Parameter'], value_text: $parameter['Value']);
|
||||
$results[] = new ParameterDTO(name: $parameter['ParameterText'], value_text: $parameter['ValueText']);
|
||||
} else { //Otherwise try to parse it as a numerical value
|
||||
$results[] = ParameterDTO::parseValueIncludingUnit($parameter['Parameter'], $parameter['Value']);
|
||||
$results[] = ParameterDTO::parseValueIncludingUnit($parameter['ParameterText'], $parameter['ValueText']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,7 +268,7 @@ class DigikeyProvider implements InfoProviderInterface
|
||||
$prices = [];
|
||||
|
||||
foreach ($price_breaks as $price_break) {
|
||||
$prices[] = new PriceDTO(minimum_discount_amount: $price_break['BreakQuantity'], price: (string) $price_break['UnitPrice'], currency_iso_code: $this->currency);
|
||||
$prices[] = new PriceDTO(minimum_discount_amount: $price_break['BreakQuantity'], price: (string) $price_break['UnitPrice'], currency_iso_code: $this->settings->currency);
|
||||
}
|
||||
|
||||
return [
|
||||
@@ -254,16 +277,22 @@ class DigikeyProvider implements InfoProviderInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $media_links
|
||||
* @param string $id The Digikey product number, to get the media for
|
||||
* @return FileDTO[][]
|
||||
* @phpstan-return array<string, FileDTO[]>
|
||||
*/
|
||||
private function mediaToDTOs(array $media_links): array
|
||||
private function mediaToDTOs(string $id): array
|
||||
{
|
||||
$datasheets = [];
|
||||
$images = [];
|
||||
|
||||
foreach ($media_links as $media_link) {
|
||||
$response = $this->digikeyClient->request('GET', '/products/v4/search/' . urlencode($id) . '/media', [
|
||||
'auth_bearer' => $this->authTokenManager->getAlwaysValidTokenString(self::OAUTH_APP_NAME)
|
||||
]);
|
||||
|
||||
$media_array = $response->toArray();
|
||||
|
||||
foreach ($media_array['MediaLinks'] as $media_link) {
|
||||
$file = new FileDTO(url: $media_link['Url'], name: $media_link['Title']);
|
||||
|
||||
switch ($media_link['MediaType']) {
|
||||
|
||||
@@ -29,6 +29,8 @@ use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PriceDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
|
||||
use App\Settings\InfoProviderSystem\Element14Settings;
|
||||
use Composer\CaBundle\CaBundle;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class Element14Provider implements InfoProviderInterface
|
||||
@@ -43,9 +45,19 @@ class Element14Provider implements InfoProviderInterface
|
||||
private const COMPLIANCE_ATTRIBUTES = ['euEccn', 'hazardous', 'MSL', 'productTraceability', 'rohsCompliant',
|
||||
'rohsPhthalatesCompliant', 'SVHC', 'tariffCode', 'usEccn', 'hazardCode'];
|
||||
|
||||
public function __construct(private readonly HttpClientInterface $element14Client, private readonly string $api_key, private readonly string $store_id)
|
||||
private readonly HttpClientInterface $element14Client;
|
||||
|
||||
public function __construct(HttpClientInterface $element14Client, private readonly Element14Settings $settings)
|
||||
{
|
||||
|
||||
/* We use the mozilla CA from the composer ca bundle directly, as some debian systems seems to have problems
|
||||
* with the SSL.COM CA, element14 uses. See https://github.com/Part-DB/Part-DB-server/issues/866
|
||||
*
|
||||
* This is a workaround until the issue is resolved in debian (or never).
|
||||
* As this only affects this provider, this should have no negative impact and the CA bundle is still secure.
|
||||
*/
|
||||
$this->element14Client = $element14Client->withOptions([
|
||||
'cafile' => CaBundle::getBundledCaBundlePath(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function getProviderInfo(): array
|
||||
@@ -65,7 +77,7 @@ class Element14Provider implements InfoProviderInterface
|
||||
|
||||
public function isActive(): bool
|
||||
{
|
||||
return $this->api_key !== '';
|
||||
return trim($this->settings->apiKey) !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,11 +89,11 @@ class Element14Provider implements InfoProviderInterface
|
||||
$response = $this->element14Client->request('GET', self::ENDPOINT_URL, [
|
||||
'query' => [
|
||||
'term' => $term,
|
||||
'storeInfo.id' => $this->store_id,
|
||||
'storeInfo.id' => $this->settings->storeId,
|
||||
'resultsSettings.offset' => 0,
|
||||
'resultsSettings.numberOfResults' => self::NUMBER_OF_RESULTS,
|
||||
'resultsSettings.responseGroup' => 'large',
|
||||
'callInfo.apiKey' => $this->api_key,
|
||||
'callInfo.apiKey' => $this->settings->apiKey,
|
||||
'callInfo.responseDataFormat' => 'json',
|
||||
'versionNumber' => self::API_VERSION_NUMBER,
|
||||
],
|
||||
@@ -149,7 +161,7 @@ class Element14Provider implements InfoProviderInterface
|
||||
$locale = 'en_US';
|
||||
}
|
||||
|
||||
return 'https://' . $this->store_id . '/productimages/standard/' . $locale . $image['baseName'];
|
||||
return 'https://' . $this->settings->storeId . '/productimages/standard/' . $locale . $image['baseName'];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -184,7 +196,7 @@ class Element14Provider implements InfoProviderInterface
|
||||
public function getUsedCurrency(): string
|
||||
{
|
||||
//Decide based on the shop ID
|
||||
return match ($this->store_id) {
|
||||
return match ($this->settings->storeId) {
|
||||
'bg.farnell.com', 'at.farnell.com', 'si.farnell.com', 'sk.farnell.com', 'ro.farnell.com', 'pt.farnell.com', 'nl.farnell.com', 'be.farnell.com', 'lv.farnell.com', 'lt.farnell.com', 'it.farnell.com', 'fr.farnell.com', 'fi.farnell.com', 'ee.farnell.com', 'es.farnell.com', 'ie.farnell.com', 'cpcireland.farnell.com', 'de.farnell.com' => 'EUR',
|
||||
'cz.farnell.com' => 'CZK',
|
||||
'dk.farnell.com' => 'DKK',
|
||||
@@ -211,7 +223,7 @@ class Element14Provider implements InfoProviderInterface
|
||||
'tw.element14.com' => 'TWD',
|
||||
'kr.element14.com' => 'KRW',
|
||||
'vn.element14.com' => 'VND',
|
||||
default => throw new \RuntimeException('Unknown store ID: ' . $this->store_id)
|
||||
default => throw new \RuntimeException('Unknown store ID: ' . $this->settings->storeId)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PriceDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
|
||||
use App\Settings\InfoProviderSystem\LCSCSettings;
|
||||
use Symfony\Component\HttpFoundation\Cookie;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
@@ -39,7 +40,7 @@ class LCSCProvider implements InfoProviderInterface
|
||||
|
||||
public const DISTRIBUTOR_NAME = 'LCSC';
|
||||
|
||||
public function __construct(private readonly HttpClientInterface $lcscClient, private readonly string $currency, private readonly bool $enabled = true)
|
||||
public function __construct(private readonly HttpClientInterface $lcscClient, private readonly LCSCSettings $settings)
|
||||
{
|
||||
|
||||
}
|
||||
@@ -62,7 +63,7 @@ class LCSCProvider implements InfoProviderInterface
|
||||
// This provider is always active
|
||||
public function isActive(): bool
|
||||
{
|
||||
return $this->enabled;
|
||||
return $this->settings->enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,10 +74,10 @@ class LCSCProvider implements InfoProviderInterface
|
||||
{
|
||||
$response = $this->lcscClient->request('GET', self::ENDPOINT_URL . "/product/detail", [
|
||||
'headers' => [
|
||||
'Cookie' => new Cookie('currencyCode', $this->currency)
|
||||
'Cookie' => new Cookie('currencyCode', $this->settings->currency)
|
||||
],
|
||||
'query' => [
|
||||
'prductCode' => $id,
|
||||
'productCode' => $id,
|
||||
],
|
||||
]);
|
||||
|
||||
@@ -123,7 +124,7 @@ class LCSCProvider implements InfoProviderInterface
|
||||
{
|
||||
$response = $this->lcscClient->request('GET', self::ENDPOINT_URL . "/search/global", [
|
||||
'headers' => [
|
||||
'Cookie' => new Cookie('currencyCode', $this->currency)
|
||||
'Cookie' => new Cookie('currencyCode', $this->settings->currency)
|
||||
],
|
||||
'query' => [
|
||||
'keyword' => $term,
|
||||
@@ -273,7 +274,7 @@ class LCSCProvider implements InfoProviderInterface
|
||||
'kr.' => 'DKK',
|
||||
'₹' => 'INR',
|
||||
//Fallback to the configured currency
|
||||
default => $this->currency,
|
||||
default => $this->settings->currency,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ use App\Services\InfoProviderSystem\DTOs\FileDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PriceDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
|
||||
use App\Settings\InfoProviderSystem\MouserSettings;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
|
||||
@@ -50,10 +51,7 @@ class MouserProvider implements InfoProviderInterface
|
||||
|
||||
public function __construct(
|
||||
private readonly HttpClientInterface $mouserClient,
|
||||
private readonly string $api_key,
|
||||
private readonly string $language,
|
||||
private readonly string $options,
|
||||
private readonly int $search_limit
|
||||
private readonly MouserSettings $settings,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -74,7 +72,7 @@ class MouserProvider implements InfoProviderInterface
|
||||
|
||||
public function isActive(): bool
|
||||
{
|
||||
return $this->api_key !== '';
|
||||
return $this->settings->apiKey !== '' && $this->settings->apiKey !== null;
|
||||
}
|
||||
|
||||
public function searchByKeyword(string $keyword): array
|
||||
@@ -94,6 +92,7 @@ class MouserProvider implements InfoProviderInterface
|
||||
From the startingRecord, the number of records specified will be returned up to the end of the recordset.
|
||||
This is useful for paging through the complete recordset of parts matching keyword.
|
||||
|
||||
|
||||
searchOptions string
|
||||
Optional.
|
||||
If not provided, the default is None.
|
||||
@@ -119,15 +118,15 @@ class MouserProvider implements InfoProviderInterface
|
||||
|
||||
$response = $this->mouserClient->request('POST', self::ENDPOINT_URL."/keyword", [
|
||||
'query' => [
|
||||
'apiKey' => $this->api_key,
|
||||
'apiKey' => $this->settings->apiKey
|
||||
],
|
||||
'json' => [
|
||||
'SearchByKeywordRequest' => [
|
||||
'keyword' => $keyword,
|
||||
'records' => $this->search_limit, //self::NUMBER_OF_RESULTS,
|
||||
'records' => $this->settings->searchLimit, //self::NUMBER_OF_RESULTS,
|
||||
'startingRecord' => 0,
|
||||
'searchOptions' => $this->options,
|
||||
'searchWithYourSignUpLanguage' => $this->language,
|
||||
'searchOptions' => $this->settings->searchOption->value,
|
||||
'searchWithYourSignUpLanguage' => $this->settings->searchWithSignUpLanguage ? 'true' : 'false',
|
||||
]
|
||||
],
|
||||
]);
|
||||
@@ -160,7 +159,7 @@ class MouserProvider implements InfoProviderInterface
|
||||
|
||||
$response = $this->mouserClient->request('POST', self::ENDPOINT_URL."/partnumber", [
|
||||
'query' => [
|
||||
'apiKey' => $this->api_key,
|
||||
'apiKey' => $this->settings->apiKey,
|
||||
],
|
||||
'json' => [
|
||||
'SearchByPartRequest' => [
|
||||
@@ -176,11 +175,16 @@ class MouserProvider implements InfoProviderInterface
|
||||
throw new \RuntimeException('No part found with ID '.$id);
|
||||
}
|
||||
|
||||
//Manually filter out the part with the correct ID
|
||||
$tmp = array_filter($tmp, fn(PartDetailDTO $part) => $part->provider_id === $id);
|
||||
if (count($tmp) === 0) {
|
||||
throw new \RuntimeException('No part found with ID '.$id);
|
||||
}
|
||||
if (count($tmp) > 1) {
|
||||
throw new \RuntimeException('Multiple parts found with ID '.$id . ' ('.count($tmp).' found). This is basically a bug in Mousers API response. See issue #616.');
|
||||
throw new \RuntimeException('Multiple parts found with ID '.$id);
|
||||
}
|
||||
|
||||
return $tmp[0];
|
||||
return reset($tmp);
|
||||
}
|
||||
|
||||
public function getCapabilities(): array
|
||||
|
||||
@@ -88,6 +88,8 @@ use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PriceDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
|
||||
use App\Settings\InfoProviderSystem\OEMSecretsSettings;
|
||||
use App\Settings\InfoProviderSystem\OEMSecretsSortMode;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
|
||||
@@ -99,12 +101,7 @@ class OEMSecretsProvider implements InfoProviderInterface
|
||||
|
||||
public function __construct(
|
||||
private readonly HttpClientInterface $oemsecretsClient,
|
||||
private readonly string $api_key,
|
||||
private readonly string $country_code,
|
||||
private readonly string $currency,
|
||||
private readonly string $zero_price,
|
||||
private readonly string $set_param,
|
||||
private readonly string $sort_criteria,
|
||||
private readonly OEMSecretsSettings $settings,
|
||||
private readonly CacheItemPoolInterface $partInfoCache
|
||||
)
|
||||
{
|
||||
@@ -268,7 +265,7 @@ class OEMSecretsProvider implements InfoProviderInterface
|
||||
*/
|
||||
public function isActive(): bool
|
||||
{
|
||||
return $this->api_key !== '';
|
||||
return $this->settings->apiKey !== '';
|
||||
}
|
||||
|
||||
|
||||
@@ -324,9 +321,9 @@ class OEMSecretsProvider implements InfoProviderInterface
|
||||
$response = $this->oemsecretsClient->request('GET', self::ENDPOINT_URL, [
|
||||
'query' => [
|
||||
'searchTerm' => $keyword,
|
||||
'apiKey' => $this->api_key,
|
||||
'currency' => $this->currency,
|
||||
'countryCode' => $this->country_code,
|
||||
'apiKey' => $this->settings->apiKey,
|
||||
'currency' => $this->settings->currency,
|
||||
'countryCode' => $this->settings->country,
|
||||
],
|
||||
]);
|
||||
|
||||
@@ -533,7 +530,7 @@ class OEMSecretsProvider implements InfoProviderInterface
|
||||
|
||||
// Extract prices
|
||||
$priceDTOs = $this->getPrices($product);
|
||||
if (empty($priceDTOs) && (int)$this->zero_price === 0) {
|
||||
if (empty($priceDTOs) && !$this->settings->keepZeroPrices) {
|
||||
return null; // Skip products without valid prices
|
||||
}
|
||||
|
||||
@@ -557,7 +554,7 @@ class OEMSecretsProvider implements InfoProviderInterface
|
||||
}
|
||||
|
||||
$imagesResults[$provider_id] = $this->getImages($product, $imagesResults[$provider_id] ?? []);
|
||||
if ($this->set_param == 1) {
|
||||
if ($this->settings->parseParams) {
|
||||
$parametersResults[$provider_id] = $this->getParameters($product, $parametersResults[$provider_id] ?? []);
|
||||
} else {
|
||||
$parametersResults[$provider_id] = [];
|
||||
@@ -582,7 +579,7 @@ class OEMSecretsProvider implements InfoProviderInterface
|
||||
$regionB = $this->countryCodeToRegionMap[$countryCodeB] ?? '';
|
||||
|
||||
// If the map is empty or doesn't contain the key for $this->country_code, assign a placeholder region.
|
||||
$regionForEnvCountry = $this->countryCodeToRegionMap[$this->country_code] ?? '';
|
||||
$regionForEnvCountry = $this->countryCodeToRegionMap[$this->settings->country] ?? '';
|
||||
|
||||
// Convert to string before comparison to avoid mixed types
|
||||
$countryCodeA = (string) $countryCodeA;
|
||||
@@ -599,9 +596,9 @@ class OEMSecretsProvider implements InfoProviderInterface
|
||||
}
|
||||
|
||||
// Step 1: country_code from the environment
|
||||
if ($countryCodeA === $this->country_code && $countryCodeB !== $this->country_code) {
|
||||
if ($countryCodeA === $this->settings->country && $countryCodeB !== $this->settings->country) {
|
||||
return -1;
|
||||
} elseif ($countryCodeA !== $this->country_code && $countryCodeB === $this->country_code) {
|
||||
} elseif ($countryCodeA !== $this->settings->country && $countryCodeB === $this->settings->country) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -681,8 +678,8 @@ class OEMSecretsProvider implements InfoProviderInterface
|
||||
|
||||
if (is_array($prices)) {
|
||||
// Step 1: Check if prices exist in the preferred currency
|
||||
if (isset($prices[$this->currency]) && is_array($prices[$this->currency])) {
|
||||
$priceDetails = $prices[$this->currency];
|
||||
if (isset($prices[$this->settings->currency]) && is_array($prices[$this->settings->currency])) {
|
||||
$priceDetails = $prices[$this->$this->settings->currency];
|
||||
foreach ($priceDetails as $priceDetail) {
|
||||
if (
|
||||
is_array($priceDetail) &&
|
||||
@@ -694,7 +691,7 @@ class OEMSecretsProvider implements InfoProviderInterface
|
||||
$priceDTOs[] = new PriceDTO(
|
||||
minimum_discount_amount: (float)$priceDetail['unit_break'],
|
||||
price: (string)$priceDetail['unit_price'],
|
||||
currency_iso_code: $this->currency,
|
||||
currency_iso_code: $this->settings->currency,
|
||||
includes_tax: false,
|
||||
price_related_quantity: 1.0
|
||||
);
|
||||
@@ -1293,7 +1290,7 @@ class OEMSecretsProvider implements InfoProviderInterface
|
||||
private function sortResultsData(array &$resultsData, string $searchKeyword): void
|
||||
{
|
||||
// If the SORT_CRITERIA is not 'C' or 'M', do not sort
|
||||
if ($this->sort_criteria !== 'C' && $this->sort_criteria !== 'M') {
|
||||
if ($this->settings->sortMode !== OEMSecretsSortMode::COMPLETENESS && $this->settings->sortMode !== OEMSecretsSortMode::MANUFACTURER) {
|
||||
return;
|
||||
}
|
||||
usort($resultsData, function ($a, $b) use ($searchKeyword) {
|
||||
@@ -1332,9 +1329,9 @@ class OEMSecretsProvider implements InfoProviderInterface
|
||||
}
|
||||
|
||||
// Final sorting: by completeness or manufacturer, if necessary
|
||||
if ($this->sort_criteria === 'C') {
|
||||
if ($this->settings->sortMode === OEMSecretsSortMode::COMPLETENESS) {
|
||||
return $this->compareByCompleteness($a, $b);
|
||||
} elseif ($this->sort_criteria === 'M') {
|
||||
} elseif ($this->settings->sortMode === OEMSecretsSortMode::MANUFACTURER) {
|
||||
return strcasecmp($a->manufacturer, $b->manufacturer);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PriceDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
|
||||
use App\Services\OAuth\OAuthTokenManager;
|
||||
use App\Settings\InfoProviderSystem\OctopartSettings;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Symfony\Component\HttpClient\HttpOptions;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
@@ -114,9 +115,8 @@ class OctopartProvider implements InfoProviderInterface
|
||||
|
||||
public function __construct(private readonly HttpClientInterface $httpClient,
|
||||
private readonly OAuthTokenManager $authTokenManager, private readonly CacheItemPoolInterface $partInfoCache,
|
||||
private readonly string $clientId, private readonly string $secret,
|
||||
private readonly string $currency, private readonly string $country,
|
||||
private readonly int $search_limit, private readonly bool $onlyAuthorizedSellers)
|
||||
private readonly OctopartSettings $settings,
|
||||
)
|
||||
{
|
||||
|
||||
}
|
||||
@@ -183,7 +183,7 @@ class OctopartProvider implements InfoProviderInterface
|
||||
{
|
||||
//The client ID has to be set and a token has to be available (user clicked connect)
|
||||
//return /*!empty($this->clientId) && */ $this->authTokenManager->hasToken(self::OAUTH_APP_NAME);
|
||||
return $this->clientId !== '' && $this->secret !== '';
|
||||
return $this->settings->clientId !== '' && $this->settings->secret !== '';
|
||||
}
|
||||
|
||||
private function mapLifeCycleStatus(?string $value): ?ManufacturingStatus
|
||||
@@ -347,10 +347,10 @@ class OctopartProvider implements InfoProviderInterface
|
||||
|
||||
$result = $this->makeGraphQLCall($graphQL, [
|
||||
'keyword' => $keyword,
|
||||
'limit' => $this->search_limit,
|
||||
'currency' => $this->currency,
|
||||
'country' => $this->country,
|
||||
'authorizedOnly' => $this->onlyAuthorizedSellers,
|
||||
'limit' => $this->settings->searchLimit,
|
||||
'currency' => $this->settings->currency,
|
||||
'country' => $this->settings->country,
|
||||
'authorizedOnly' => $this->settings->onlyAuthorizedSellers,
|
||||
]);
|
||||
|
||||
$tmp = [];
|
||||
@@ -383,9 +383,9 @@ class OctopartProvider implements InfoProviderInterface
|
||||
|
||||
$result = $this->makeGraphQLCall($graphql, [
|
||||
'ids' => [$id],
|
||||
'currency' => $this->currency,
|
||||
'country' => $this->country,
|
||||
'authorizedOnly' => $this->onlyAuthorizedSellers,
|
||||
'currency' => $this->settings->currency,
|
||||
'country' => $this->settings->country,
|
||||
'authorizedOnly' => $this->settings->onlyAuthorizedSellers,
|
||||
]);
|
||||
|
||||
$tmp = $this->partResultToDTO($result['data']['supParts'][0]);
|
||||
|
||||
@@ -31,6 +31,7 @@ use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PriceDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
|
||||
use App\Settings\InfoProviderSystem\PollinSettings;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
@@ -39,8 +40,7 @@ class PollinProvider implements InfoProviderInterface
|
||||
{
|
||||
|
||||
public function __construct(private readonly HttpClientInterface $client,
|
||||
#[Autowire(env: 'bool:PROVIDER_POLLIN_ENABLED')]
|
||||
private readonly bool $enabled = true,
|
||||
private readonly PollinSettings $settings,
|
||||
)
|
||||
{
|
||||
}
|
||||
@@ -49,8 +49,8 @@ class PollinProvider implements InfoProviderInterface
|
||||
{
|
||||
return [
|
||||
'name' => 'Pollin',
|
||||
'description' => 'Webscrapping from pollin.de to get part information',
|
||||
'url' => 'https://www.reichelt.de/',
|
||||
'description' => 'Webscraping from pollin.de to get part information',
|
||||
'url' => 'https://www.pollin.de/',
|
||||
'disabled_help' => 'Set PROVIDER_POLLIN_ENABLED env to 1'
|
||||
];
|
||||
}
|
||||
@@ -62,7 +62,7 @@ class PollinProvider implements InfoProviderInterface
|
||||
|
||||
public function isActive(): bool
|
||||
{
|
||||
return $this->enabled;
|
||||
return $this->settings->enabled;
|
||||
}
|
||||
|
||||
public function searchByKeyword(string $keyword): array
|
||||
@@ -215,7 +215,7 @@ class PollinProvider implements InfoProviderInterface
|
||||
private function parseNotes(Crawler $dom): string
|
||||
{
|
||||
//Concat product highlights and product description
|
||||
return $dom->filter('div.product-detail-top-features')->html() . '<br><br>' . $dom->filter('div.product-detail-description-text')->html();
|
||||
return $dom->filter('div.product-detail-top-features')->html('') . '<br><br>' . $dom->filter('div.product-detail-description-text')->html('');
|
||||
}
|
||||
|
||||
private function parsePrices(Crawler $dom): array
|
||||
|
||||
@@ -29,6 +29,7 @@ use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PriceDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
|
||||
use App\Settings\InfoProviderSystem\ReicheltSettings;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
@@ -39,16 +40,7 @@ class ReicheltProvider implements InfoProviderInterface
|
||||
public const DISTRIBUTOR_NAME = "Reichelt";
|
||||
|
||||
public function __construct(private readonly HttpClientInterface $client,
|
||||
#[Autowire(env: "bool:PROVIDER_REICHELT_ENABLED")]
|
||||
private readonly bool $enabled = true,
|
||||
#[Autowire(env: "PROVIDER_REICHELT_LANGUAGE")]
|
||||
private readonly string $language = "en",
|
||||
#[Autowire(env: "PROVIDER_REICHELT_COUNTRY")]
|
||||
private readonly string $country = "DE",
|
||||
#[Autowire(env: "PROVIDER_REICHELT_INCLUDE_VAT")]
|
||||
private readonly bool $includeVAT = false,
|
||||
#[Autowire(env: "PROVIDER_REICHELT_CURRENCY")]
|
||||
private readonly string $currency = "EUR",
|
||||
private readonly ReicheltSettings $settings,
|
||||
)
|
||||
{
|
||||
}
|
||||
@@ -57,7 +49,7 @@ class ReicheltProvider implements InfoProviderInterface
|
||||
{
|
||||
return [
|
||||
'name' => 'Reichelt',
|
||||
'description' => 'Webscrapping from reichelt.com to get part information',
|
||||
'description' => 'Webscraping from reichelt.com to get part information',
|
||||
'url' => 'https://www.reichelt.com/',
|
||||
'disabled_help' => 'Set PROVIDER_REICHELT_ENABLED env to 1'
|
||||
];
|
||||
@@ -70,7 +62,7 @@ class ReicheltProvider implements InfoProviderInterface
|
||||
|
||||
public function isActive(): bool
|
||||
{
|
||||
return $this->enabled;
|
||||
return $this->settings->enabled;
|
||||
}
|
||||
|
||||
public function searchByKeyword(string $keyword): array
|
||||
@@ -121,8 +113,8 @@ class ReicheltProvider implements InfoProviderInterface
|
||||
sprintf(
|
||||
'https://www.reichelt.com/?ACTION=514&id=74&article=%s&LANGUAGE=%s&CCOUNTRY=%s',
|
||||
$id,
|
||||
strtoupper($this->language),
|
||||
strtoupper($this->country)
|
||||
strtoupper($this->settings->language),
|
||||
strtoupper($this->settings->country)
|
||||
)
|
||||
);
|
||||
$json = $response->toArray();
|
||||
@@ -133,8 +125,8 @@ class ReicheltProvider implements InfoProviderInterface
|
||||
|
||||
$response = $this->client->request('GET', $productPage, [
|
||||
'query' => [
|
||||
'CCTYPE' => $this->includeVAT ? 'private' : 'business',
|
||||
'currency' => $this->currency,
|
||||
'CCTYPE' => $this->settings->includeVAT ? 'private' : 'business',
|
||||
'currency' => $this->settings->currency,
|
||||
],
|
||||
]);
|
||||
$html = $response->getContent();
|
||||
@@ -158,7 +150,7 @@ class ReicheltProvider implements InfoProviderInterface
|
||||
distributor_name: self::DISTRIBUTOR_NAME,
|
||||
order_number: $json[0]['article_artnr'],
|
||||
prices: array_merge(
|
||||
[new PriceDTO(1.0, $priceString, $currency, $this->includeVAT)]
|
||||
[new PriceDTO(1.0, $priceString, $currency, $this->settings->includeVAT)]
|
||||
, $this->parseBatchPrices($dom, $currency)),
|
||||
product_url: $productPage
|
||||
);
|
||||
@@ -218,7 +210,7 @@ class ReicheltProvider implements InfoProviderInterface
|
||||
//Strip any non-numeric characters
|
||||
$priceString = preg_replace('/[^0-9.]/', '', $priceString);
|
||||
|
||||
$prices[] = new PriceDTO($minAmount, $priceString, $currency, $this->includeVAT);
|
||||
$prices[] = new PriceDTO($minAmount, $priceString, $currency, $this->settings->includeVAT);
|
||||
});
|
||||
|
||||
return $prices;
|
||||
@@ -270,7 +262,7 @@ class ReicheltProvider implements InfoProviderInterface
|
||||
private function getBaseURL(): string
|
||||
{
|
||||
//Without the trailing slash
|
||||
return 'https://www.reichelt.com/' . strtolower($this->country) . '/' . strtolower($this->language);
|
||||
return 'https://www.reichelt.com/' . strtolower($this->settings->country) . '/' . strtolower($this->settings->language);
|
||||
}
|
||||
|
||||
public function getCapabilities(): array
|
||||
|
||||
@@ -23,6 +23,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Services\InfoProviderSystem\Providers;
|
||||
|
||||
use App\Settings\InfoProviderSystem\TMESettings;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
|
||||
@@ -30,15 +31,15 @@ class TMEClient
|
||||
{
|
||||
public const BASE_URI = 'https://api.tme.eu';
|
||||
|
||||
public function __construct(private readonly HttpClientInterface $tmeClient, private readonly string $token, private readonly string $secret)
|
||||
public function __construct(private readonly HttpClientInterface $tmeClient, private readonly TMESettings $settings)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function makeRequest(string $action, array $parameters): ResponseInterface
|
||||
{
|
||||
$parameters['Token'] = $this->token;
|
||||
$parameters['ApiSignature'] = $this->getSignature($action, $parameters, $this->secret);
|
||||
$parameters['Token'] = $this->settings->apiToken;
|
||||
$parameters['ApiSignature'] = $this->getSignature($action, $parameters, $this->settings->apiSecret);
|
||||
|
||||
return $this->tmeClient->request('POST', $this->getUrlForAction($action), [
|
||||
'body' => $parameters,
|
||||
@@ -47,7 +48,7 @@ class TMEClient
|
||||
|
||||
public function isUsable(): bool
|
||||
{
|
||||
return $this->token !== '' && $this->secret !== '';
|
||||
return !($this->settings->apiToken === '' || $this->settings->apiSecret === '');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,7 +59,7 @@ class TMEClient
|
||||
public function isUsingPrivateToken(): bool
|
||||
{
|
||||
//Private tokens are longer than anonymous ones (50 instead of 45 characters)
|
||||
return strlen($this->token) > 45;
|
||||
return strlen($this->settings->apiToken) > 45;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -30,24 +30,21 @@ use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PriceDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
|
||||
use App\Settings\InfoProviderSystem\TMESettings;
|
||||
|
||||
class TMEProvider implements InfoProviderInterface
|
||||
{
|
||||
|
||||
private const VENDOR_NAME = 'TME';
|
||||
|
||||
/** @var bool If true, the prices are gross prices. If false, the prices are net prices. */
|
||||
private readonly bool $get_gross_prices;
|
||||
|
||||
public function __construct(private readonly TMEClient $tmeClient, private readonly string $country,
|
||||
private readonly string $language, private readonly string $currency,
|
||||
bool $get_gross_prices)
|
||||
public function __construct(private readonly TMEClient $tmeClient, private readonly TMESettings $settings)
|
||||
{
|
||||
//If we have a private token, set get_gross_prices to false, as it is automatically determined by the account type then
|
||||
if ($this->tmeClient->isUsingPrivateToken()) {
|
||||
$this->get_gross_prices = false;
|
||||
} else {
|
||||
$this->get_gross_prices = $get_gross_prices;
|
||||
$this->get_gross_prices = $this->settings->grossPrices;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,8 +71,8 @@ class TMEProvider implements InfoProviderInterface
|
||||
public function searchByKeyword(string $keyword): array
|
||||
{
|
||||
$response = $this->tmeClient->makeRequest('Products/Search', [
|
||||
'Country' => $this->country,
|
||||
'Language' => $this->language,
|
||||
'Country' => $this->settings->country,
|
||||
'Language' => $this->settings->language,
|
||||
'SearchPlain' => $keyword,
|
||||
]);
|
||||
|
||||
@@ -104,8 +101,8 @@ class TMEProvider implements InfoProviderInterface
|
||||
public function getDetails(string $id): PartDetailDTO
|
||||
{
|
||||
$response = $this->tmeClient->makeRequest('Products/GetProducts', [
|
||||
'Country' => $this->country,
|
||||
'Language' => $this->language,
|
||||
'Country' => $this->settings->country,
|
||||
'Language' => $this->settings->language,
|
||||
'SymbolList' => [$id],
|
||||
]);
|
||||
|
||||
@@ -149,8 +146,8 @@ class TMEProvider implements InfoProviderInterface
|
||||
public function getFiles(string $id): array
|
||||
{
|
||||
$response = $this->tmeClient->makeRequest('Products/GetProductsFiles', [
|
||||
'Country' => $this->country,
|
||||
'Language' => $this->language,
|
||||
'Country' => $this->settings->country,
|
||||
'Language' => $this->settings->language,
|
||||
'SymbolList' => [$id],
|
||||
]);
|
||||
|
||||
@@ -191,9 +188,9 @@ class TMEProvider implements InfoProviderInterface
|
||||
public function getVendorInfo(string $id, ?string $productURL = null): PurchaseInfoDTO
|
||||
{
|
||||
$response = $this->tmeClient->makeRequest('Products/GetPricesAndStocks', [
|
||||
'Country' => $this->country,
|
||||
'Language' => $this->language,
|
||||
'Currency' => $this->currency,
|
||||
'Country' => $this->settings->country,
|
||||
'Language' => $this->settings->language,
|
||||
'Currency' => $this->settings->currency,
|
||||
'GrossPrices' => $this->get_gross_prices,
|
||||
'SymbolList' => [$id],
|
||||
]);
|
||||
@@ -234,8 +231,8 @@ class TMEProvider implements InfoProviderInterface
|
||||
public function getParameters(string $id, string|null &$footprint_name = null): array
|
||||
{
|
||||
$response = $this->tmeClient->makeRequest('Products/GetParameters', [
|
||||
'Country' => $this->country,
|
||||
'Language' => $this->language,
|
||||
'Country' => $this->settings->country,
|
||||
'Language' => $this->settings->language,
|
||||
'SymbolList' => [$id],
|
||||
]);
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ declare(strict_types=1);
|
||||
namespace App\Services\LabelSystem;
|
||||
|
||||
use App\Entity\LabelSystem\LabelProcessMode;
|
||||
use App\Settings\SystemSettings\CustomizationSettings;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use App\Entity\Contracts\NamedElementInterface;
|
||||
use App\Entity\LabelSystem\LabelOptions;
|
||||
@@ -60,7 +61,7 @@ final class LabelHTMLGenerator
|
||||
private readonly LabelBarcodeGenerator $barcodeGenerator,
|
||||
private readonly SandboxedTwigFactory $sandboxedTwigProvider,
|
||||
private readonly Security $security,
|
||||
private readonly string $partdb_title)
|
||||
private readonly CustomizationSettings $customizationSettings,)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -88,7 +89,8 @@ final class LabelHTMLGenerator
|
||||
'page' => $page,
|
||||
'last_page' => count($elements),
|
||||
'user' => $current_user,
|
||||
'install_title' => $this->partdb_title,
|
||||
'install_title' => $this->customizationSettings->instanceName,
|
||||
'partdb_title' => $this->customizationSettings->instanceName,
|
||||
'paper_width' => $options->getWidth(),
|
||||
'paper_height' => $options->getHeight(),
|
||||
]
|
||||
|
||||
@@ -63,12 +63,24 @@ final class BarcodeProvider implements PlaceholderProviderInterface
|
||||
return $this->barcodeGenerator->generateHTMLBarcode($label_options, $label_target);
|
||||
}
|
||||
|
||||
if ('[[BARCODE_DATAMATRIX]]' === $placeholder) {
|
||||
$label_options = new LabelOptions();
|
||||
$label_options->setBarcodeType(BarcodeType::DATAMATRIX);
|
||||
return $this->barcodeGenerator->generateHTMLBarcode($label_options, $label_target);
|
||||
}
|
||||
|
||||
if ('[[BARCODE_C39]]' === $placeholder) {
|
||||
$label_options = new LabelOptions();
|
||||
$label_options->setBarcodeType(BarcodeType::CODE39);
|
||||
return $this->barcodeGenerator->generateHTMLBarcode($label_options, $label_target);
|
||||
}
|
||||
|
||||
if ('[[BARCODE_C93]]' === $placeholder) {
|
||||
$label_options = new LabelOptions();
|
||||
$label_options->setBarcodeType(BarcodeType::CODE93);
|
||||
return $this->barcodeGenerator->generateHTMLBarcode($label_options, $label_target);
|
||||
}
|
||||
|
||||
if ('[[BARCODE_C128]]' === $placeholder) {
|
||||
$label_options = new LabelOptions();
|
||||
$label_options->setBarcodeType(BarcodeType::CODE128);
|
||||
|
||||
@@ -41,6 +41,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Services\LabelSystem\PlaceholderProviders;
|
||||
|
||||
use App\Settings\SystemSettings\CustomizationSettings;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use App\Entity\UserSystem\User;
|
||||
use DateTime;
|
||||
@@ -54,14 +55,18 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
*/
|
||||
final class GlobalProviders implements PlaceholderProviderInterface
|
||||
{
|
||||
public function __construct(private readonly string $partdb_title, private readonly Security $security, private readonly UrlGeneratorInterface $url_generator)
|
||||
public function __construct(
|
||||
private readonly Security $security,
|
||||
private readonly UrlGeneratorInterface $url_generator,
|
||||
private CustomizationSettings $customizationSettings,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function replace(string $placeholder, object $label_target, array $options = []): ?string
|
||||
{
|
||||
if ('[[INSTALL_NAME]]' === $placeholder) {
|
||||
return $this->partdb_title;
|
||||
return $this->customizationSettings->instanceName;
|
||||
}
|
||||
|
||||
$user = $this->security->getUser();
|
||||
|
||||
@@ -22,37 +22,25 @@ declare(strict_types=1);
|
||||
*/
|
||||
namespace App\Services\LogSystem;
|
||||
|
||||
use App\Settings\SystemSettings\HistorySettings;
|
||||
|
||||
/**
|
||||
* This service is used to check if a log change comment is needed for a given operation type.
|
||||
* It is configured using the "enforce_change_comments_for" config parameter.
|
||||
* @see \App\Tests\Services\LogSystem\EventCommentNeededHelperTest
|
||||
*/
|
||||
class EventCommentNeededHelper
|
||||
final class EventCommentNeededHelper
|
||||
{
|
||||
final public const VALID_OPERATION_TYPES = [
|
||||
'part_edit',
|
||||
'part_create',
|
||||
'part_delete',
|
||||
'part_stock_operation',
|
||||
'datastructure_edit',
|
||||
'datastructure_create',
|
||||
'datastructure_delete',
|
||||
];
|
||||
|
||||
public function __construct(protected array $enforce_change_comments_for)
|
||||
public function __construct(private readonly HistorySettings $settings)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a log change comment is needed for the given operation type
|
||||
*/
|
||||
public function isCommentNeeded(string $comment_type): bool
|
||||
public function isCommentNeeded(EventCommentType $comment_type): bool
|
||||
{
|
||||
//Check if the comment type is valid
|
||||
if (! in_array($comment_type, self::VALID_OPERATION_TYPES, true)) {
|
||||
throw new \InvalidArgumentException('The comment type "'.$comment_type.'" is not valid!');
|
||||
}
|
||||
|
||||
return in_array($comment_type, $this->enforce_change_comments_for, true);
|
||||
return in_array($comment_type, $this->settings->enforceComments, true);
|
||||
}
|
||||
}
|
||||
|
||||
47
src/Services/LogSystem/EventCommentType.php
Normal file
47
src/Services/LogSystem/EventCommentType.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2024 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace App\Services\LogSystem;
|
||||
|
||||
use Symfony\Contracts\Translation\TranslatableInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* This enum represents the different types of event comments that could be required, by the system.
|
||||
* They are almost only useful when working with the EventCommentNeededHelper service.
|
||||
*/
|
||||
enum EventCommentType: string implements TranslatableInterface
|
||||
{
|
||||
case PART_EDIT = 'part_edit';
|
||||
case PART_CREATE = 'part_create';
|
||||
case PART_DELETE = 'part_delete';
|
||||
case PART_STOCK_OPERATION = 'part_stock_operation';
|
||||
case DATASTRUCTURE_EDIT = 'datastructure_edit';
|
||||
case DATASTRUCTURE_CREATE = 'datastructure_create';
|
||||
case DATASTRUCTURE_DELETE = 'datastructure_delete';
|
||||
|
||||
public function trans(TranslatorInterface $translator, ?string $locale = null): string
|
||||
{
|
||||
return $translator->trans('settings.system.history.enforceComments.type.' . $this->value, locale: $locale);
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ declare(strict_types=1);
|
||||
*/
|
||||
namespace App\Services\Parts;
|
||||
|
||||
use App\Entity\Parts\StorageLocation;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use App\Entity\Parts\Category;
|
||||
use App\Entity\Parts\Footprint;
|
||||
@@ -35,6 +36,9 @@ use InvalidArgumentException;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
use Symfony\Contracts\Translation\TranslatableInterface;
|
||||
|
||||
use function Symfony\Component\Translation\t;
|
||||
|
||||
final class PartsTableActionHandler
|
||||
{
|
||||
@@ -61,8 +65,9 @@ final class PartsTableActionHandler
|
||||
/**
|
||||
* @param Part[] $selected_parts
|
||||
* @return RedirectResponse|null Returns a redirect response if the user should be redirected to another page, otherwise null
|
||||
* //@param-out list<array{'part': Part, 'message': string|TranslatableInterface}>|array<void> $errors
|
||||
*/
|
||||
public function handleAction(string $action, array $selected_parts, ?int $target_id, ?string $redirect_url = null): ?RedirectResponse
|
||||
public function handleAction(string $action, array $selected_parts, ?int $target_id, ?string $redirect_url = null, array &$errors = []): ?RedirectResponse
|
||||
{
|
||||
if ($action === 'add_to_project') {
|
||||
return new RedirectResponse(
|
||||
@@ -161,6 +166,29 @@ implode(',', array_map(static fn (PartLot $lot) => $lot->getID(), $part->getPart
|
||||
$this->denyAccessUnlessGranted('@measurement_units.read');
|
||||
$part->setPartUnit(null === $target_id ? null : $this->entityManager->find(MeasurementUnit::class, $target_id));
|
||||
break;
|
||||
case 'change_location':
|
||||
$this->denyAccessUnlessGranted('@storelocations.read');
|
||||
//Retrieve the first part lot and set the location for it
|
||||
$part_lots = $part->getPartLots();
|
||||
if ($part_lots->count() > 0) {
|
||||
if ($part_lots->count() > 1) {
|
||||
$errors[] = [
|
||||
'part' => $part,
|
||||
'message' => t('parts.table.action_handler.error.part_lots_multiple'),
|
||||
];
|
||||
break;
|
||||
}
|
||||
|
||||
$part_lot = $part_lots->first();
|
||||
$part_lot->setStorageLocation(null === $target_id ? null : $this->entityManager->find(StorageLocation::class, $target_id));
|
||||
} else { //Create a new part lot if there are none
|
||||
$part_lot = new PartLot();
|
||||
$part_lot->setPart($part);
|
||||
$part_lot->setInstockUnknown(true); //We do not know how many parts are in stock, so we set it to true
|
||||
$part_lot->setStorageLocation(null === $target_id ? null : $this->entityManager->find(StorageLocation::class, $target_id));
|
||||
$this->entityManager->persist($part_lot);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new InvalidArgumentException('The given action is unknown! ('.$action.')');
|
||||
|
||||
@@ -25,6 +25,7 @@ namespace App\Services\Parts;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Entity\PriceInformations\Currency;
|
||||
use App\Entity\PriceInformations\Pricedetail;
|
||||
use App\Settings\SystemSettings\LocalizationSettings;
|
||||
use Brick\Math\BigDecimal;
|
||||
use Brick\Math\RoundingMode;
|
||||
use Doctrine\ORM\PersistentCollection;
|
||||
@@ -39,7 +40,7 @@ class PricedetailHelper
|
||||
{
|
||||
protected string $locale;
|
||||
|
||||
public function __construct(protected string $base_currency)
|
||||
public function __construct()
|
||||
{
|
||||
$this->locale = Locale::getDefault();
|
||||
}
|
||||
|
||||
@@ -23,12 +23,14 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Services\System;
|
||||
|
||||
use App\Settings\SystemSettings\CustomizationSettings;
|
||||
|
||||
/**
|
||||
* Helper service to retrieve the banner of this Part-DB installation
|
||||
*/
|
||||
class BannerHelper
|
||||
{
|
||||
public function __construct(private readonly string $project_dir, private readonly string $partdb_banner)
|
||||
public function __construct(private CustomizationSettings $customizationSettings)
|
||||
{
|
||||
|
||||
}
|
||||
@@ -39,18 +41,6 @@ class BannerHelper
|
||||
*/
|
||||
public function getBanner(): string
|
||||
{
|
||||
$banner = $this->partdb_banner;
|
||||
if ($banner === '') {
|
||||
$banner_path = $this->project_dir
|
||||
.DIRECTORY_SEPARATOR.'config'.DIRECTORY_SEPARATOR.'banner.md';
|
||||
|
||||
$tmp = file_get_contents($banner_path);
|
||||
if (false === $tmp) {
|
||||
throw new \RuntimeException('The banner file could not be read.');
|
||||
}
|
||||
$banner = $tmp;
|
||||
}
|
||||
|
||||
return $banner;
|
||||
return $this->customizationSettings->banner ?? "";
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Services\System;
|
||||
|
||||
use App\Settings\SystemSettings\PrivacySettings;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Shivas\VersioningBundle\Service\VersionManagerInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
@@ -43,7 +44,7 @@ class UpdateAvailableManager
|
||||
|
||||
public function __construct(private readonly HttpClientInterface $httpClient,
|
||||
private readonly CacheInterface $updateCache, private readonly VersionManagerInterface $versionManager,
|
||||
private readonly bool $check_for_updates, private readonly LoggerInterface $logger,
|
||||
private readonly PrivacySettings $privacySettings, private readonly LoggerInterface $logger,
|
||||
#[Autowire(param: 'kernel.debug')] private readonly bool $is_dev_mode)
|
||||
{
|
||||
|
||||
@@ -83,7 +84,7 @@ class UpdateAvailableManager
|
||||
public function isUpdateAvailable(): bool
|
||||
{
|
||||
//If we don't want to check for updates, we can return false
|
||||
if (!$this->check_for_updates) {
|
||||
if (!$this->privacySettings->checkForUpdates) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -101,7 +102,7 @@ class UpdateAvailableManager
|
||||
private function getLatestVersionInfo(): array
|
||||
{
|
||||
//If we don't want to check for updates, we can return dummy data
|
||||
if (!$this->check_for_updates) {
|
||||
if (!$this->privacySettings->checkForUpdates) {
|
||||
return [
|
||||
'version' => '0.0.1',
|
||||
'url' => 'update-checking-disabled'
|
||||
|
||||
@@ -23,13 +23,14 @@ declare(strict_types=1);
|
||||
namespace App\Services\Tools;
|
||||
|
||||
use App\Entity\PriceInformations\Currency;
|
||||
use App\Settings\SystemSettings\LocalizationSettings;
|
||||
use Brick\Math\BigDecimal;
|
||||
use Brick\Math\RoundingMode;
|
||||
use Swap\Swap;
|
||||
|
||||
class ExchangeRateUpdater
|
||||
{
|
||||
public function __construct(private readonly string $base_currency, private readonly Swap $swap)
|
||||
public function __construct(private LocalizationSettings $localizationSettings, private readonly Swap $swap)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -39,7 +40,7 @@ class ExchangeRateUpdater
|
||||
public function update(Currency $currency): Currency
|
||||
{
|
||||
//Currency pairs are always in the format "BASE/QUOTE"
|
||||
$rate = $this->swap->latest($this->base_currency.'/'.$currency->getIsoCode());
|
||||
$rate = $this->swap->latest($this->localizationSettings->baseCurrency.'/'.$currency->getIsoCode());
|
||||
//The rate says how many quote units are worth one base unit
|
||||
//So we need to invert it to get the exchange rate
|
||||
|
||||
|
||||
@@ -289,6 +289,13 @@ class ToolsTreeBuilder
|
||||
))->setIcon('fa-fw fa-treeview fa-solid fa-database');
|
||||
}
|
||||
|
||||
if ($this->security->isGranted('@config.change_system_settings')) {
|
||||
$nodes[] = (new TreeViewNode(
|
||||
$this->translator->trans('tree.tools.system.settings'),
|
||||
$this->urlGenerator->generate('system_settings')
|
||||
))->setIcon('fa fa-fw fa-gears fa-solid');
|
||||
}
|
||||
|
||||
return $nodes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ use App\Repository\StructuralDBElementRepository;
|
||||
use App\Services\Cache\ElementCacheTagGenerator;
|
||||
use App\Services\Cache\UserCacheKeyGenerator;
|
||||
use App\Services\EntityURLGenerator;
|
||||
use App\Settings\BehaviorSettings\SidebarSettings;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use InvalidArgumentException;
|
||||
use RecursiveIteratorIterator;
|
||||
@@ -53,6 +54,10 @@ use function count;
|
||||
*/
|
||||
class TreeViewGenerator
|
||||
{
|
||||
|
||||
private readonly bool $rootNodeExpandedByDefault;
|
||||
private readonly bool $rootNodeEnabled;
|
||||
|
||||
public function __construct(
|
||||
protected EntityURLGenerator $urlGenerator,
|
||||
protected EntityManagerInterface $em,
|
||||
@@ -61,10 +66,10 @@ class TreeViewGenerator
|
||||
protected UserCacheKeyGenerator $keyGenerator,
|
||||
protected TranslatorInterface $translator,
|
||||
private readonly UrlGeneratorInterface $router,
|
||||
protected bool $rootNodeExpandedByDefault,
|
||||
protected bool $rootNodeEnabled,
|
||||
|
||||
private readonly SidebarSettings $sidebarSettings,
|
||||
) {
|
||||
$this->rootNodeEnabled = $this->sidebarSettings->rootNodeEnabled;
|
||||
$this->rootNodeExpandedByDefault = $this->sidebarSettings->rootNodeExpanded;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -174,10 +179,7 @@ class TreeViewGenerator
|
||||
}
|
||||
|
||||
if (($mode === 'list_parts_root' || $mode === 'devices') && $this->rootNodeEnabled) {
|
||||
//We show the root node as a link to the list of all parts
|
||||
$show_all_parts_url = $this->router->generate('parts_show_all');
|
||||
|
||||
$root_node = new TreeViewNode($this->entityClassToRootNodeString($class), $show_all_parts_url, $generic);
|
||||
$root_node = new TreeViewNode($this->entityClassToRootNodeString($class), $this->entityClassToRootNodeHref($class), $generic);
|
||||
$root_node->setExpanded($this->rootNodeExpandedByDefault);
|
||||
$root_node->setIcon($this->entityClassToRootNodeIcon($class));
|
||||
|
||||
@@ -187,6 +189,27 @@ class TreeViewGenerator
|
||||
return array_merge($head, $generic);
|
||||
}
|
||||
|
||||
protected function entityClassToRootNodeHref(string $class): ?string
|
||||
{
|
||||
//If the root node should redirect to the new entity page, we return the URL for the new entity.
|
||||
if ($this->sidebarSettings->rootNodeRedirectsToNewEntity) {
|
||||
return match ($class) {
|
||||
Category::class => $this->router->generate('category_new'),
|
||||
StorageLocation::class => $this->router->generate('store_location_new'),
|
||||
Footprint::class => $this->router->generate('footprint_new'),
|
||||
Manufacturer::class => $this->router->generate('manufacturer_new'),
|
||||
Supplier::class => $this->router->generate('supplier_new'),
|
||||
Project::class => $this->router->generate('project_new'),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
return match ($class) {
|
||||
Project::class => $this->router->generate('project_new'),
|
||||
default => $this->router->generate('parts_show_all')
|
||||
};
|
||||
}
|
||||
|
||||
protected function entityClassToRootNodeString(string $class): string
|
||||
{
|
||||
return match ($class) {
|
||||
|
||||
@@ -105,6 +105,9 @@ class PermissionPresetsHelper
|
||||
$this->permissionResolver->setAllOperationsOfPermission($perm_holder, 'suppliers', PermissionData::ALLOW);
|
||||
$this->permissionResolver->setAllOperationsOfPermission($perm_holder, 'projects', PermissionData::ALLOW);
|
||||
|
||||
//Allow to change system settings
|
||||
$this->permissionResolver->setPermission($perm_holder, 'config', 'change_system_settings', PermissionData::ALLOW);
|
||||
|
||||
//Allow to manage Oauth tokens
|
||||
$this->permissionResolver->setPermission($perm_holder, 'system', 'manage_oauth_tokens', PermissionData::ALLOW);
|
||||
//Allow to show updates
|
||||
|
||||
@@ -30,6 +30,7 @@ use App\Entity\Attachments\UserAttachment;
|
||||
use App\Entity\UserSystem\User;
|
||||
use App\Services\Attachments\AttachmentSubmitHandler;
|
||||
use App\Services\Attachments\AttachmentURLGenerator;
|
||||
use App\Settings\SystemSettings\PrivacySettings;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Asset\Packages;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
@@ -42,7 +43,7 @@ class UserAvatarHelper
|
||||
public const IMG_DEFAULT_AVATAR_PATH = 'img/default_avatar.svg';
|
||||
|
||||
public function __construct(
|
||||
private readonly bool $use_gravatar,
|
||||
private readonly PrivacySettings $privacySettings,
|
||||
private readonly Packages $packages,
|
||||
private readonly AttachmentURLGenerator $attachmentURLGenerator,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
@@ -65,7 +66,7 @@ class UserAvatarHelper
|
||||
}
|
||||
|
||||
//If not check if gravatar is enabled (then use gravatar URL)
|
||||
if ($this->use_gravatar) {
|
||||
if ($this->privacySettings->useGravatar) {
|
||||
return $this->getGravatar($user, 200); //200px wide picture
|
||||
}
|
||||
|
||||
@@ -82,7 +83,7 @@ class UserAvatarHelper
|
||||
}
|
||||
|
||||
//If not check if gravatar is enabled (then use gravatar URL)
|
||||
if ($this->use_gravatar) {
|
||||
if ($this->privacySettings->useGravatar) {
|
||||
return $this->getGravatar($user, 50); //50px wide picture
|
||||
}
|
||||
|
||||
@@ -99,7 +100,7 @@ class UserAvatarHelper
|
||||
}
|
||||
|
||||
//If not check if gravatar is enabled (then use gravatar URL)
|
||||
if ($this->use_gravatar) {
|
||||
if ($this->privacySettings->useGravatar) {
|
||||
return $this->getGravatar($user, 150);
|
||||
}
|
||||
|
||||
|
||||
52
src/Settings/AppSettings.php
Normal file
52
src/Settings/AppSettings.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?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\Settings;
|
||||
|
||||
use App\Settings\BehaviorSettings\BehaviorSettings;
|
||||
use App\Settings\InfoProviderSystem\InfoProviderSettings;
|
||||
use App\Settings\MiscSettings\MiscSettings;
|
||||
use App\Settings\SystemSettings\AttachmentsSettings;
|
||||
use Jbtronics\SettingsBundle\Settings\EmbeddedSettings;
|
||||
use Jbtronics\SettingsBundle\Settings\Settings;
|
||||
use Jbtronics\SettingsBundle\Settings\SettingsTrait;
|
||||
|
||||
#[Settings]
|
||||
#[SettingsIcon('folder-tree')]
|
||||
class AppSettings
|
||||
{
|
||||
use SettingsTrait;
|
||||
|
||||
|
||||
#[EmbeddedSettings()]
|
||||
public ?SystemSettings $system = null;
|
||||
|
||||
#[EmbeddedSettings()]
|
||||
public ?BehaviorSettings $behavior = null;
|
||||
|
||||
#[EmbeddedSettings()]
|
||||
public ?InfoProviderSettings $infoProviders = null;
|
||||
|
||||
#[EmbeddedSettings()]
|
||||
public ?MiscSettings $miscSettings = null;
|
||||
}
|
||||
43
src/Settings/BehaviorSettings/BehaviorSettings.php
Normal file
43
src/Settings/BehaviorSettings/BehaviorSettings.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?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\Settings\BehaviorSettings;
|
||||
|
||||
use Jbtronics\SettingsBundle\Settings\EmbeddedSettings;
|
||||
use Jbtronics\SettingsBundle\Settings\Settings;
|
||||
use Jbtronics\SettingsBundle\Settings\SettingsTrait;
|
||||
|
||||
#[Settings]
|
||||
class BehaviorSettings
|
||||
{
|
||||
use SettingsTrait;
|
||||
|
||||
#[EmbeddedSettings]
|
||||
public ?SidebarSettings $sidebar = null;
|
||||
|
||||
#[EmbeddedSettings]
|
||||
public ?TableSettings $table = null;
|
||||
|
||||
#[EmbeddedSettings]
|
||||
public ?PartInfoSettings $partInfo = null;
|
||||
}
|
||||
43
src/Settings/BehaviorSettings/PartInfoSettings.php
Normal file
43
src/Settings/BehaviorSettings/PartInfoSettings.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?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\Settings\BehaviorSettings;
|
||||
|
||||
use App\Settings\SettingsIcon;
|
||||
use Jbtronics\SettingsBundle\Metadata\EnvVarMode;
|
||||
use Jbtronics\SettingsBundle\Settings\Settings;
|
||||
use Jbtronics\SettingsBundle\Settings\SettingsParameter;
|
||||
use Symfony\Component\Translation\TranslatableMessage as TM;
|
||||
|
||||
#[Settings(name: "part_info", label: new TM("settings.behavior.part_info"))]
|
||||
#[SettingsIcon('fa-circle-info')]
|
||||
class PartInfoSettings
|
||||
{
|
||||
/**
|
||||
* Whether to show the part image overlays in the part info view
|
||||
* @var bool
|
||||
*/
|
||||
#[SettingsParameter(label: new TM("settings.behavior.part_info.show_part_image_overlay"), description: new TM("settings.behavior.part_info.show_part_image_overlay.help"),
|
||||
envVar: "bool:SHOW_PART_IMAGE_OVERLAY", envVarMode: EnvVarMode::OVERWRITE)]
|
||||
public bool $showPartImageOverlay = true;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user