diff --git a/.docker/debian-manual-guide/Dockerfile b/.docker/debian-manual-guide/Dockerfile new file mode 100644 index 00000000..e3fb174b --- /dev/null +++ b/.docker/debian-manual-guide/Dockerfile @@ -0,0 +1,61 @@ +# Reproduces https://docs.part-db.de/installation/installation_guide-debian.html +# on Debian 13 (trixie) as closely as possible, for debugging issues reported +# by users who followed that guide (Apache2 + mod_php, not the project's own +# FrankenPHP-based Dockerfile at the repo root). +FROM debian:13 + +ARG HOST_UID=1000 +ARG HOST_GID=1000 + +ENV DEBIAN_FRONTEND=noninteractive + +# --- Prerequisites (guide: "Prerequisites Installation") --- +RUN apt-get update && apt-get upgrade -y && \ + apt-get install -y --no-install-recommends \ + git curl zip ca-certificates software-properties-common \ + apt-transport-https lsb-release nano wget sqlite3 gnupg openssl \ + && rm -rf /var/lib/apt/lists/* + +# --- PHP and Apache2 (guide: "PHP and Apache2 Setup") --- +# Debian 13 ships PHP 8.4 by default, so no extra PHP repo is needed. +RUN apt-get update && apt-get install -y --no-install-recommends \ + apache2 php8.4 libapache2-mod-php8.4 \ + php8.4-opcache php8.4-curl php8.4-gd php8.4-mbstring \ + php8.4-xml php8.4-bcmath php8.4-intl php8.4-zip php8.4-xsl \ + php8.4-sqlite3 php8.4-mysql \ + && rm -rf /var/lib/apt/lists/* + +# --- Composer (guide: "Composer Installation") --- +RUN wget -O /tmp/composer-setup.php https://getcomposer.org/installer \ + && php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer \ + && chmod +x /usr/local/bin/composer \ + && rm /tmp/composer-setup.php + +# --- Node.js and Yarn (guide: "Node.js and Yarn Setup") --- +RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ + && apt-get install -y --no-install-recommends nodejs \ + && curl -sL https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor -o /usr/share/keyrings/yarnkey.gpg \ + && echo "deb [signed-by=/usr/share/keyrings/yarnkey.gpg] https://dl.yarnpkg.com/debian stable main" > /etc/apt/sources.list.d/yarn.list \ + && apt-get update && apt-get install -y --no-install-recommends yarn \ + && rm -rf /var/lib/apt/lists/* + +# Map www-data to the host user's uid/gid so files created inside the +# bind-mounted repo (composer/yarn artifacts, chown'd dirs) don't end up +# owned by a foreign uid on the host. +RUN groupmod -g ${HOST_GID} www-data && usermod -u ${HOST_UID} -g ${HOST_GID} www-data \ + && mkdir -p /var/www/partdb && chown -R www-data:www-data /var/www + +# --- Apache2 Configuration (guide: "Apache2 Configuration") --- +COPY partdb.conf /etc/apache2/sites-available/partdb.conf +RUN ln -sf /etc/apache2/sites-available/partdb.conf /etc/apache2/sites-enabled/partdb.conf \ + && a2enmod rewrite headers access_compat \ + && rm -f /etc/apache2/sites-enabled/000-default.conf + +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +WORKDIR /var/www/partdb +EXPOSE 80 + +ENTRYPOINT ["/entrypoint.sh"] +CMD ["apache2ctl", "-D", "FOREGROUND"] diff --git a/.docker/debian-manual-guide/docker-compose.yml b/.docker/debian-manual-guide/docker-compose.yml new file mode 100644 index 00000000..efe47d00 --- /dev/null +++ b/.docker/debian-manual-guide/docker-compose.yml @@ -0,0 +1,41 @@ +# Reproduces a Debian 12 install following +# https://docs.part-db.de/installation/installation_guide-debian.html +# using the repo checkout two levels up, for debugging. +# +# Usage (from this directory): +# HOST_UID=$(id -u) HOST_GID=$(id -g) docker compose up --build +# +# Then open http://localhost:8080 (login: admin / password printed in the +# container logs by doctrine:migrations:migrate on first run). +# +# vendor/, node_modules/, var/, public/build, public/media and uploads live +# in named volumes so composer/yarn/console never write into the host's own +# working copy - only src/, config/, templates/, assets/, migrations/, etc. +# are shared live for debugging. +services: + partdb: + build: + context: . + args: + HOST_UID: ${HOST_UID:-1000} + HOST_GID: ${HOST_GID:-1000} + container_name: partdb-debian-guide + ports: + - "8080:80" + volumes: + - ../..:/var/www/partdb + - ./env.local:/var/www/partdb/.env.local:ro + - partdb_vendor:/var/www/partdb/vendor + - partdb_node_modules:/var/www/partdb/node_modules + - partdb_var:/var/www/partdb/var + - partdb_public_build:/var/www/partdb/public/build + - partdb_public_media:/var/www/partdb/public/media + - partdb_uploads:/var/www/partdb/uploads + +volumes: + partdb_vendor: + partdb_node_modules: + partdb_var: + partdb_public_build: + partdb_public_media: + partdb_uploads: diff --git a/.docker/debian-manual-guide/entrypoint.sh b/.docker/debian-manual-guide/entrypoint.sh new file mode 100644 index 00000000..b085f7c1 --- /dev/null +++ b/.docker/debian-manual-guide/entrypoint.sh @@ -0,0 +1,48 @@ +#!/bin/bash +# Reproduces the app-setup steps from +# https://docs.part-db.de/installation/installation_guide-debian.html +# against the repo bind-mounted at /var/www/partdb. +set -euo pipefail + +cd /var/www/partdb + +# Only chown the named-volume mount points (see docker-compose.yml) - they're +# fresh docker volumes owned by root until first use. The bind-mounted source +# tree doesn't need it (www-data's uid/gid is mapped to the host user's at +# image build time, so ownership already matches), and recursing into it +# would also try (and fail) to chown the read-only .env.local mount. +chown -R www-data:www-data \ + var vendor node_modules public/build public/media uploads + +as_www() { + # -p preserves the environment (APP_ENV/APP_SECRET/DATABASE_URL from + # docker-compose) across the user switch. Plain `su` resets it, which + # would silently fall back to whatever's baked into .env.local. + su -p -s /bin/bash www-data -c "$*" +} + +if [ ! -f vendor/autoload.php ] || [ "${FORCE_REINSTALL:-0}" = "1" ]; then + echo "==> composer install -o" + as_www "composer install -o" +fi + +if [ ! -d node_modules/.bin ] || [ "${FORCE_REINSTALL:-0}" = "1" ]; then + echo "==> yarn install" + as_www "yarn install" +fi + +if [ ! -f public/build/manifest.json ] || [ "${FORCE_REINSTALL:-0}" = "1" ]; then + echo "==> yarn build" + as_www "yarn build" +fi + +echo "==> cache:clear" +as_www "php bin/console cache:clear" + +echo "==> partdb:check-requirements" +as_www "php bin/console partdb:check-requirements" || true + +echo "==> doctrine:migrations:migrate" +as_www "php bin/console doctrine:migrations:migrate --no-interaction" + +exec "$@" diff --git a/.docker/debian-manual-guide/env.local b/.docker/debian-manual-guide/env.local new file mode 100644 index 00000000..61545710 --- /dev/null +++ b/.docker/debian-manual-guide/env.local @@ -0,0 +1,11 @@ +# Bind-mounted over /var/www/partdb/.env.local inside the container only. +# The host's real .env.local (dev, MySQL on 127.0.0.1) is never touched or +# read - this guarantees APP_ENV/DATABASE_URL are correct for BOTH Apache/ +# mod_php web requests and CLI (console/composer) commands, without relying +# on environment variables being propagated into Apache's subprocess env +# (which is what caused the "WebProfilerBundle" dev-bundle error: web +# requests were silently falling back to the host .env.local's APP_ENV=dev, +# while --no-dev composer install never installed that bundle). +APP_ENV=prod +APP_SECRET=0000000000000000000000000000000000000000000000000000000000debug +DATABASE_URL="sqlite:///%kernel.project_dir%/var/app.db" diff --git a/.docker/debian-manual-guide/partdb.conf b/.docker/debian-manual-guide/partdb.conf new file mode 100644 index 00000000..8e53b236 --- /dev/null +++ b/.docker/debian-manual-guide/partdb.conf @@ -0,0 +1,15 @@ +# Verbatim from https://docs.part-db.de/installation/installation_guide-debian.html + + ServerName partdb.lan + ServerAlias www.partdb.lan + + DocumentRoot /var/www/partdb/public + + AllowOverride All + Order Allow,Deny + Allow from All + + + ErrorLog /var/log/apache2/partdb_error.log + CustomLog /var/log/apache2/partdb_access.log combined + diff --git a/.docker/frankenphp/Caddyfile b/.docker/frankenphp/Caddyfile index f26b6f22..293ab18e 100644 --- a/.docker/frankenphp/Caddyfile +++ b/.docker/frankenphp/Caddyfile @@ -51,6 +51,15 @@ # Disable Topics tracking if not enabled explicitly: https://github.com/jkarlin/topics header ?Permissions-Policy "browsing-topics=()" + # Set a strict CSP and nosniff for all static assets not handled by PHP. + # ? means "set only if not already present", so PHP responses carrying a Nelmio CSP are left untouched. + header ?Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; sandbox;" + header ?X-Content-Type-Options "nosniff" + + # SVG files get a slightly different CSP because they can embed resources and must not be framed. + @svg path *.svg *.svg.gz *.svg.br + header @svg Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'; sandbox;" + # Prevent PHP execution in the media upload directory @php_in_media path_regexp (?i)^/media/.*\.(php[3-8]?|phar|phtml|pht|phps)$ respond @php_in_media 403 diff --git a/.env b/.env index 8311abad..c175012b 100644 --- a/.env +++ b/.env @@ -5,6 +5,15 @@ # Share that value with nobody and keep it secret APP_SECRET=a03498528f5a5fc089273ec9ae5b2849 +# Change this and uncomment the following line to set the trusted hosts for your Part-DB installation, aka under which +# domain names it is reachable. This is makes things more secure, because it can prevent certain header attacks. +# You have to escape dots in the domain name with a backslash +# IMPORTANT: In .env files (like this one), the value must be wrapped in single quotes as shown below. +#TRUSTED_HOSTS='^(your-domain\.invalid)$' + +# You can also allow multiple domain names, e.g. for testing or development purposes, with an | between the domain names. +#TRUSTED_HOSTS='^(localhost|your-domain\.invalid)$' + ################################################################################### # Database settings ################################################################################### @@ -149,6 +158,16 @@ DISABLE_YEAR2038_BUG_CHECK=0 #TRUSTED_PROXIES=127.0.0.0/8,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 #TRUSTED_HOSTS='^(localhost|example\.com)$' +################################################################################### +# Logging settings +################################################################################### + +# The minimum level a deprecation notice must have to be written to the var/log/_deprecations.log file. +# Deprecation notices are logged with level "info", so this disables the deprecation log by default. +# Set to debug to log all deprecation notices +DEPRECATION_LOG_LEVEL=emergency + + ###> symfony/lock ### # Choose one of the stores below diff --git a/.github/workflows/assets_artifact_build.yml b/.github/workflows/assets_artifact_build.yml index a74ae7cc..c92f6ec5 100644 --- a/.github/workflows/assets_artifact_build.yml +++ b/.github/workflows/assets_artifact_build.yml @@ -27,7 +27,7 @@ jobs: APP_ENV: prod steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -42,7 +42,7 @@ jobs: run: | echo "::set-output name=dir::$(composer config cache-files-dir)" - - uses: actions/cache@v5 + - uses: actions/cache@v6 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} @@ -56,7 +56,7 @@ jobs: id: yarn-cache-dir-path run: echo "::set-output name=dir::$(yarn cache dir)" - - uses: actions/cache@v5 + - uses: actions/cache@v6 id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} @@ -65,7 +65,7 @@ jobs: ${{ runner.os }}-yarn- - name: Setup node - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version: '22' diff --git a/.github/workflows/docker_build.yml b/.github/workflows/docker_build.yml index 210dbc18..d1f53c38 100644 --- a/.github/workflows/docker_build.yml +++ b/.github/workflows/docker_build.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Docker meta id: docker_meta diff --git a/.github/workflows/docker_frankenphp.yml b/.github/workflows/docker_frankenphp.yml index 36ec322d..fc69b29b 100644 --- a/.github/workflows/docker_frankenphp.yml +++ b/.github/workflows/docker_frankenphp.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Docker meta id: docker_meta diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index f47ce87b..ab44f1af 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -34,7 +34,7 @@ jobs: run: | echo "::set-output name=dir::$(composer config cache-files-dir)" - - uses: actions/cache@v5 + - uses: actions/cache@v6 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5b756228..c2c9e93a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -46,7 +46,7 @@ jobs: if: matrix.db-type == 'postgres' - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -81,7 +81,7 @@ jobs: id: composer-cache run: | echo "::set-output name=dir::$(composer config cache-files-dir)" - - uses: actions/cache@v5 + - uses: actions/cache@v6 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} @@ -92,7 +92,7 @@ jobs: id: yarn-cache-dir-path run: echo "::set-output name=dir::$(yarn cache dir)" - - uses: actions/cache@v5 + - uses: actions/cache@v6 id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} @@ -104,7 +104,7 @@ jobs: run: composer install --prefer-dist --no-progress - name: Setup node - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version: '22' @@ -129,7 +129,7 @@ jobs: run: ./bin/phpunit --coverage-clover=coverage.xml - name: Upload coverage - uses: codecov/codecov-action@v6 + uses: codecov/codecov-action@v7 with: env_vars: PHP_VERSION,DB_TYPE token: ${{ secrets.CODECOV_TOKEN }} diff --git a/Dockerfile b/Dockerfile index e848acc1..049de283 100644 --- a/Dockerfile +++ b/Dockerfile @@ -193,7 +193,7 @@ RUN a2dissite 000-default.conf && \ a2enmod proxy_fcgi setenvif && \ a2enconf php${PHP_VERSION}-fpm && \ a2enconf docker-php && \ - a2enmod rewrite + a2enmod rewrite headers # Install composer and yarn dependencies for Part-DB USER www-data diff --git a/VERSION b/VERSION index 3cf561c0..965a689e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.12.1 +2.13.4 diff --git a/assets/controllers/common/dirty_form_controller.js b/assets/controllers/common/dirty_form_controller.js index aad2e6b0..8e560a3f 100644 --- a/assets/controllers/common/dirty_form_controller.js +++ b/assets/controllers/common/dirty_form_controller.js @@ -19,8 +19,7 @@ import {Controller} from "@hotwired/stimulus"; import {visit} from "@hotwired/turbo"; -import * as bootbox from "bootbox"; -import "../../css/components/bootbox_extensions.css"; +import {ConfirmSwal} from "../../helpers/swal"; import "../../css/components/dirty_form.css"; /** @@ -207,11 +206,10 @@ export default class extends Controller { } _confirmNavigation(onConfirm) { - bootbox.confirm({ - title: this.confirmTitleValue, - message: this.confirmMessageValue, - callback: (result) => { if (result) onConfirm(); } - }); + ConfirmSwal.fire({ + titleText: this.confirmTitleValue, + text: this.confirmMessageValue, + }).then(({isConfirmed}) => { if (isConfirmed) onConfirm(); }); } _handleLinkClick(event) { diff --git a/assets/controllers/common/hide_sidebar_controller.js b/assets/controllers/common/hide_sidebar_controller.js index 4be304ff..c65cdcdf 100644 --- a/assets/controllers/common/hide_sidebar_controller.js +++ b/assets/controllers/common/hide_sidebar_controller.js @@ -51,7 +51,7 @@ export default class extends Controller { //Make the state persistent over reloads if(localStorage.getItem(STORAGE_KEY) === 'true') { - sidebarHide(); + this.hideSidebar(); } } diff --git a/assets/controllers/elements/collection_type_controller.js b/assets/controllers/elements/collection_type_controller.js index 647ed5e5..caeb4122 100644 --- a/assets/controllers/elements/collection_type_controller.js +++ b/assets/controllers/elements/collection_type_controller.js @@ -19,8 +19,7 @@ import {Controller} from "@hotwired/stimulus"; -import * as bootbox from "bootbox"; -import "../../css/components/bootbox_extensions.css"; +import {AlertSwal, ConfirmSwal} from "../../helpers/swal"; import accept from "attr-accept"; export default class extends Controller { @@ -62,7 +61,7 @@ export default class extends Controller { if(!prototype) { console.warn("Prototype is not set, we cannot create a new element. This is most likely due to missing permissions."); - bootbox.alert("You do not have the permissions to create a new element. (No protoype element is set)"); + AlertSwal.fire({"text": "You do not have the permissions to create a new element. (No protoype element is set)"}); return; } @@ -226,8 +225,10 @@ export default class extends Controller { } if(this.deleteMessageValue) { - bootbox.confirm(this.deleteMessageValue, (result) => { - if (result) { + ConfirmSwal.fire({ + text: this.deleteMessageValue, + }).then(({isConfirmed}) => { + if (isConfirmed) { del(); } }); diff --git a/assets/controllers/elements/datatables/datatables_controller.js b/assets/controllers/elements/datatables/datatables_controller.js index d945004b..4b84a834 100644 --- a/assets/controllers/elements/datatables/datatables_controller.js +++ b/assets/controllers/elements/datatables/datatables_controller.js @@ -38,9 +38,7 @@ import 'datatables.net-colreorder-bs5'; import 'datatables.net-responsive-bs5'; import '../../../js/lib/datatables'; -//import 'datatables.net-select-bs5'; -//Use the local version containing the fix for the select extension -import '../../../js/lib/dataTables.select.mjs'; +import 'datatables.net-select-bs5'; const EVENT_DT_LOADED = 'dt:loaded'; diff --git a/assets/controllers/elements/datatables/parts_controller.js b/assets/controllers/elements/datatables/parts_controller.js index c43fa276..cfa386cc 100644 --- a/assets/controllers/elements/datatables/parts_controller.js +++ b/assets/controllers/elements/datatables/parts_controller.js @@ -20,7 +20,7 @@ import DatatablesController from "./datatables_controller.js"; import TomSelect from "tom-select"; -import * as bootbox from "bootbox"; +import {ConfirmSwal} from "../../../helpers/swal"; /** * This is the datatables controller for parts lists @@ -146,15 +146,17 @@ export default class extends DatatablesController { bubbles: true, //This line is important, otherwise Turbo will not receive the event }); - const confirm = bootbox.confirm({ - message: message, title: title, callback: function (result) { - //If the dialog was confirmed, then submit the form. - if (result) { - that._confirmed = true; - form.dispatchEvent(that._our_event); - } else { - that._confirmed = false; - } + ConfirmSwal.fire({ + titleText: title, + text: message, + icon: "warning" + }).then(({isConfirmed}) => { + //If the dialog was confirmed, then submit the form. + if (isConfirmed) { + that._confirmed = true; + form.dispatchEvent(that._our_event); + } else { + that._confirmed = false; } }); } diff --git a/assets/controllers/elements/delete_btn_controller.js b/assets/controllers/elements/delete_btn_controller.js index 9ab15f7d..e1b37bcc 100644 --- a/assets/controllers/elements/delete_btn_controller.js +++ b/assets/controllers/elements/delete_btn_controller.js @@ -19,8 +19,7 @@ import {Controller} from "@hotwired/stimulus"; -import * as bootbox from "bootbox"; -import "../../css/components/bootbox_extensions.css"; +import {ConfirmSwal} from "../../helpers/swal"; export default class extends Controller { @@ -48,32 +47,33 @@ export default class extends Controller const submitter = event.submitter; const that = this; - const confirm = bootbox.confirm({ - message: message, title: title, callback: function (result) { - //If the dialog was confirmed, then submit the form. - if (result) { - //Set a flag to prevent the dialog from popping up again and allowing turbo to submit the form - that._confirmed = true; + ConfirmSwal.fire({ + titleText: title, + html: message, //Message contains a
tag and no user injectable HTML + }).then(({isConfirmed}) => { + //If the dialog was confirmed, then submit the form. + if (isConfirmed) { + //Set a flag to prevent the dialog from popping up again and allowing turbo to submit the form + that._confirmed = true; - //Create a submit button in the form and click it to submit the form - //Before a submit event was dispatched, but this caused weird issues on Firefox causing the delete request being posted twice (and the second time was returning 404). See https://github.com/Part-DB/Part-DB-server/issues/273 - const submit_btn = document.createElement('button'); - submit_btn.type = 'submit'; - submit_btn.style.display = 'none'; + //Create a submit button in the form and click it to submit the form + //Before a submit event was dispatched, but this caused weird issues on Firefox causing the delete request being posted twice (and the second time was returning 404). See https://github.com/Part-DB/Part-DB-server/issues/273 + const submit_btn = document.createElement('button'); + submit_btn.type = 'submit'; + submit_btn.style.display = 'none'; - //If the clicked button has a value, set it on the submit button - if (submitter.value) { - submit_btn.value = submitter.value; - } - if (submitter.name) { - submit_btn.name = submitter.name; - } - form.appendChild(submit_btn); - submit_btn.click(); - } else { - that._confirmed = false; + //If the clicked button has a value, set it on the submit button + if (submitter.value) { + submit_btn.value = submitter.value; } + if (submitter.name) { + submit_btn.name = submitter.name; + } + form.appendChild(submit_btn); + submit_btn.click(); + } else { + that._confirmed = false; } }); } -} \ No newline at end of file +} diff --git a/assets/controllers/elements/link_confirm_controller.js b/assets/controllers/elements/link_confirm_controller.js index 3d59b492..be226517 100644 --- a/assets/controllers/elements/link_confirm_controller.js +++ b/assets/controllers/elements/link_confirm_controller.js @@ -19,8 +19,7 @@ import {Controller} from "@hotwired/stimulus"; -import * as bootbox from "bootbox"; -import "../../css/components/bootbox_extensions.css"; +import {ConfirmSwal} from "../../helpers/swal"; export default class extends Controller { @@ -53,20 +52,19 @@ export default class extends Controller const that = this; - bootbox.confirm({ - title: this.titleValue, - message: this.messageValue, - callback: (result) => { - if (result) { - //Set a flag to prevent the dialog from popping up again and allowing turbo to submit the form - that._confirmed = true; + ConfirmSwal.fire({ + titleText: this.titleValue, + text: this.messageValue, + }).then(({isConfirmed}) => { + if (isConfirmed) { + //Set a flag to prevent the dialog from popping up again and allowing turbo to submit the form + that._confirmed = true; - //Click the link - that.element.click(); - } else { - that._confirmed = false; - } + //Click the link + that.element.click(); + } else { + that._confirmed = false; } }); } -} \ No newline at end of file +} diff --git a/assets/controllers/elements/password_strength_estimate_controller.js b/assets/controllers/elements/password_strength_estimate_controller.js index d9cfbc87..9ad2da1c 100644 --- a/assets/controllers/elements/password_strength_estimate_controller.js +++ b/assets/controllers/elements/password_strength_estimate_controller.js @@ -19,12 +19,14 @@ import {Controller} from "@hotwired/stimulus"; -import { zxcvbn, zxcvbnOptions } from '@zxcvbn-ts/core'; +import { ZxcvbnFactory } from '@zxcvbn-ts/core'; import * as zxcvbnCommonPackage from '@zxcvbn-ts/language-common'; import * as zxcvbnEnPackage from '@zxcvbn-ts/language-en'; import * as zxcvbnDePackage from '@zxcvbn-ts/language-de'; import * as zxcvbnFrPackage from '@zxcvbn-ts/language-fr'; import * as zxcvbnJaPackage from '@zxcvbn-ts/language-ja'; +import * as zxcvbnItPackage from '@zxcvbn-ts/language-it'; +import * as zxcvbnPlPackage from '@zxcvbn-ts/language-pl'; import {trans} from '../../translator.js'; /* stimulusFetch: 'lazy' */ @@ -34,6 +36,8 @@ export default class extends Controller { static targets = ["badge", "warning"] + _zxcvbnFactory; + _getTranslations() { //Get the current locale const locale = document.documentElement.lang; @@ -43,6 +47,10 @@ export default class extends Controller { return zxcvbnFrPackage.translations; } else if (locale.includes('ja')) { return zxcvbnJaPackage.translations; + } else if (locale.includes('it')) { + return zxcvbnItPackage.translations; + } else if (locale.includes('pl')) { + return zxcvbnPlPackage.translations; } //Fallback to english @@ -56,34 +64,39 @@ export default class extends Controller { //Configure zxcvbn const options = { graphs: zxcvbnCommonPackage.adjacencyGraphs, + useLevenshtein: true, dictionary: { ...zxcvbnCommonPackage.dictionary, // We could use the english dictionary here too, but it is very big. So we just use the common words - //...zxcvbnEnPackage.dictionary, + ...zxcvbnEnPackage.dictionary, + ...zxcvbnDePackage.dictionary, + + "partdb": ['part-db', 'partdb', 'part_db', 'part-db-symfony', 'partdb-symfony', 'part_db_symfony'], }, translations: this._getTranslations(), }; - zxcvbnOptions.setOptions(options); + + this._zxcvbnFactory = new ZxcvbnFactory(options); //Add event listener to the password input field this._passwordInput.addEventListener('input', this._onPasswordInput.bind(this)); } - _onPasswordInput() { + async _onPasswordInput() { //Retrieve the password const password = this._passwordInput.value; //Estimate the password strength - const result = zxcvbn(password); + const result = await this._zxcvbnFactory.checkAsync(password); //Update the badge this.badgeTarget.parentElement.classList.remove("d-none"); - this._setBadgeToLevel(result.score); + this._setBadgeToLevel(result.score, result.crackTimes.onlineNoThrottlingXPerSecond.display); this.warningTarget.innerHTML = result.feedback.warning; } - _setBadgeToLevel(level) { + _setBadgeToLevel(level, time = null) { let text, classes; switch (level) { @@ -118,5 +131,11 @@ export default class extends Controller { //Re-add the classes this.badgeTarget.classList.add("badge"); this.badgeTarget.classList.add(...classes.split(" ")); + + if (time) { + this.badgeTarget.setAttribute("title", trans("user.password_strength.crack_time", {"%time%": time})); + } else { + this.badgeTarget.removeAttribute("title"); + } } } diff --git a/assets/controllers/pages/reelCalculator_controller.js b/assets/controllers/pages/reelCalculator_controller.js index a134bf9b..e0b2c4ba 100644 --- a/assets/controllers/pages/reelCalculator_controller.js +++ b/assets/controllers/pages/reelCalculator_controller.js @@ -18,7 +18,7 @@ */ import {Controller} from "@hotwired/stimulus"; -import * as bootbox from "bootbox"; +import {AlertSwal} from "../../helpers/swal"; export default class extends Controller { @@ -35,12 +35,12 @@ export default class extends Controller { const part_distance = document.getElementById('reel_part_distance').value; if (dia_inner == "" || dia_outer == "" || tape_thickness == "") { - bootbox.alert(this.errorMissingValuesValue); + AlertSwal.fire({title: this.errorMissingValuesValue}); return; } if (dia_outer**dia_outer < dia_inner**dia_inner) { - bootbox.alert(this.errorOuterGreaterInnerValue); + AlertSwal.fire({title: this.errorOuterGreaterInnerValue}); return; } @@ -61,12 +61,12 @@ export default class extends Controller { return; } - var parts_per_meter = 1 / (part_distance / 1000); + const parts_per_meter = 1 / (part_distance / 1000); document.getElementById('result_parts_per_meter').textContent = parts_per_meter.toFixed(2) + ' 1/m'; - var parts_amount = (length/1000) * parts_per_meter; + const parts_amount = (length / 1000) * parts_per_meter; - document.getElementById('result_amount').textContent = Math.floor(parts_amount); + document.getElementById('result_amount').textContent = Math.floor(parts_amount).toString(); } -} \ No newline at end of file +} diff --git a/assets/css/components/swal.css b/assets/css/components/swal.css new file mode 100644 index 00000000..4c2302a9 --- /dev/null +++ b/assets/css/components/swal.css @@ -0,0 +1,50 @@ +/* + * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). + * + * Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * Respect the dark mode of Bootstrap 5 set via data-bs-theme="dark" on the element. This is done by overriding the CSS variables of the bootstrap-5 theme of SweetAlert2. + */ + +html[data-bs-theme="dark"] [data-swal2-theme='bootstrap-5'] { + /* POPUP */ + --swal2-background: #212529; + --swal2-color: #fff; + --swal2-border: 1px solid #495057; + + /* INPUT */ + --swal2-input-background: #2b3035; + --swal2-input-border: 1px solid #495057; + --swal2-input-focus-border: 1px solid #86b7fe; + --swal2-input-focus-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.075), 0 0 0 0.25rem rgba(13, 110, 253, 0.25); + + /* VALIDATION MESSAGE */ + --swal2-validation-message-background: #2c0b0e; + --swal2-validation-message-color: #ea868f; + + /* FOOTER */ + --swal2-footer-border-color: #495057; + --swal2-footer-background: #343a40; + --swal2-footer-color: #adb5bd; + + /* CLOSE BUTTON */ + --swal2-close-button-color: #fff; + + /* TOASTS */ + --swal2-toast-border: 1px solid #495057; +} diff --git a/assets/helpers/swal.js b/assets/helpers/swal.js new file mode 100644 index 00000000..e370a9ed --- /dev/null +++ b/assets/helpers/swal.js @@ -0,0 +1,44 @@ +/* + * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). + * + * Copyright (C) 2019 - 2022 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 . + */ + +import Swal from 'sweetalert2'; +import 'sweetalert2/themes/bootstrap-5.css'; +import '../css/components/swal.css' +import { trans } from '../translator'; + +const BaseSwal = Swal.mixin({ + position: "top", + theme: "bootstrap-5", + confirmButtonText: trans('dialog.btn.ok'), + cancelButtonText: trans('dialog.btn.cancel'), + denyButtonText: trans('dialog.btn.deny'), +}); + +const ConfirmSwal = BaseSwal.mixin({ + showCancelButton: true, + showCloseButton: true, + icon: "warning", +}); + +const AlertSwal = BaseSwal.mixin({ + showCloseButton: true, + icon: "info", +}); + +export { ConfirmSwal, AlertSwal, BaseSwal, BaseSwal as default,}; diff --git a/assets/js/app.js b/assets/js/app.js index 4dd39581..355fe919 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -30,21 +30,21 @@ import '../css/app/images.css'; // start the Stimulus application import '../stimulus_bootstrap'; -// Need jQuery? Install it with "yarn add jquery", then uncomment to require it. -const $ = require('jquery'); +import $ from 'jquery'; //Only include javascript import '@fortawesome/fontawesome-free/css/all.css' -require('bootstrap'); +import 'bootstrap'; import "./error_handler"; import "./tab_remember"; import "./register_events"; import "./tristate_checkboxes"; -//Define jquery globally -global.$ = global.jQuery = require("jquery"); +// Expose jQuery globally so legacy plugins and Bootstrap's jQuery integration +// can find it on window at runtime. +global.$ = global.jQuery = $; //Use the local WASM file for the ZXing library import { diff --git a/assets/js/error_handler.js b/assets/js/error_handler.js index 7f047af9..67695fb9 100644 --- a/assets/js/error_handler.js +++ b/assets/js/error_handler.js @@ -17,7 +17,7 @@ * along with this program. If not, see . */ -import * as bootbox from "bootbox"; +import Swal from "../helpers/swal"; /** * If this class is imported the user is shown an error dialog if he calls an page via Turbo and an error is responded. @@ -40,21 +40,6 @@ class ErrorHandlerHelper { _showAlert(statusText, statusCode, location, responseHTML) { const httpStatusToText = { - '200': 'OK', - '201': 'Created', - '202': 'Accepted', - '203': 'Non-Authoritative Information', - '204': 'No Content', - '205': 'Reset Content', - '206': 'Partial Content', - '300': 'Multiple Choices', - '301': 'Moved Permanently', - '302': 'Found', - '303': 'See Other', - '304': 'Not Modified', - '305': 'Use Proxy', - '306': 'Unused', - '307': 'Temporary Redirect', '400': 'Bad Request', '401': 'Unauthorized', '402': 'Payment Required', @@ -83,49 +68,67 @@ class ErrorHandlerHelper { '505': 'HTTP Version Not Supported', }; - //If the statusText is empty, we use the status code as text - if (!statusText) { - statusText = httpStatusToText[statusCode]; - } - - //Create error text - const title = statusText + ' (Status ' + statusCode + ')'; - - let trimString = function (string, length) { - return string.length > length ? - string.substring(0, length) + '...' : - string; + const userFriendlyMessages = { + '400': 'The request was invalid or malformed.', + '401': 'You need to log in to access this resource.', + '403': 'You don\'t have permission to access this resource.', + '404': 'The requested page or resource could not be found.', + '408': 'The request timed out. Please check your connection and try again.', + '409': 'There was a conflict with the current state of the resource.', + '429': 'Too many requests sent. Please wait a moment and try again.', + '500': 'An internal server error occurred. This is not your fault.', + '502': 'The server received an invalid response from an upstream service.', + '503': 'The service is temporarily unavailable. Please try again later.', + '504': 'The server did not respond in time. Please try again later.', }; - const short_location = trimString(location, 50); + if (!statusText) { + statusText = httpStatusToText[String(statusCode)] ?? 'Unknown Error'; + } - const alert = bootbox.alert( - { - size: 'large', - message: function() { - let url = location; - let msg = `Error calling ${short_location}.
`; - msg += 'Try to reload the page or contact the administrator if this error persists.'; + const title = `${statusText} (HTTP ${statusCode})`; + const friendlyMsg = userFriendlyMessages[String(statusCode)] + ?? 'An unexpected error occurred. Please try again or contact the administrator.'; - msg += '

' + 'View details' + ""; - msg += "
"; + const short_location = location.length > 80 + ? location.substring(0, 80) + '…' + : location; - return msg; - }, - title: title, - callback: function () { - //Remove blur - $('#content').removeClass('loading-content'); - } + const msg = ` +

${friendlyMsg}

+

If this error keeps happening, please contact your administrator.

+ +
+ +
`; - }); + const footer = `Error while loading: ${short_location}`; - alert.init(function (){ - var dstFrame = document.getElementById('error-iframe'); - //@ts-ignore - var dstDoc = dstFrame.contentDocument || dstFrame.contentWindow.document; - dstDoc.write(responseHTML) - dstDoc.close(); + Swal.fire({ + icon: 'error', + title: title, + html: msg, + footer: footer, + width: '90%', + confirmButtonText: 'Reload page', + showCancelButton: true, + cancelButtonText: 'Close', + showCloseButton: true, + reverseButtons: true, + didOpen: () => { + const dstFrame = document.getElementById('error-iframe'); + //@ts-ignore + const dstDoc = dstFrame.contentDocument || dstFrame.contentWindow.document; + dstDoc.write(responseHTML); + dstDoc.close(); + }, + }).then((result) => { + document.getElementById('content').classList.remove('loading-content'); + if (result.isConfirmed) { + window.location.reload(); + } }); } @@ -171,4 +174,4 @@ class ErrorHandlerHelper { } } -export default new ErrorHandlerHelper(); \ No newline at end of file +export default new ErrorHandlerHelper(); diff --git a/assets/js/lib/dataTables.select.mjs b/assets/js/lib/dataTables.select.mjs deleted file mode 100644 index bba97692..00000000 --- a/assets/js/lib/dataTables.select.mjs +++ /dev/null @@ -1,1538 +0,0 @@ -/********************* - * This is the fixed version of the select extension for DataTables with the fix for the issue with the select extension - * (https://github.com/DataTables/Select/issues/51) - * We use this instead of the yarn version until the PR (https://github.com/DataTables/Select/pull/52) is merged and released - * /*******************/ - - -/*! Select for DataTables 2.0.0 - * © SpryMedia Ltd - datatables.net/license/mit - */ - -import jQuery from 'jquery'; -import DataTable from 'datatables.net'; - -// Allow reassignment of the $ variable -let $ = jQuery; - - -// Version information for debugger -DataTable.select = {}; - -DataTable.select.version = '2.0.0'; - -DataTable.select.init = function (dt) { - var ctx = dt.settings()[0]; - - if (!DataTable.versionCheck('2')) { - throw 'Warning: Select requires DataTables 2 or newer'; - } - - if (ctx._select) { - return; - } - - var savedSelected = dt.state.loaded(); - - var selectAndSave = function (e, settings, data) { - if (data === null || data.select === undefined) { - return; - } - - // Clear any currently selected rows, before restoring state - // None will be selected on first initialisation - if (dt.rows({ selected: true }).any()) { - dt.rows().deselect(); - } - if (data.select.rows !== undefined) { - dt.rows(data.select.rows).select(); - } - - if (dt.columns({ selected: true }).any()) { - dt.columns().deselect(); - } - if (data.select.columns !== undefined) { - dt.columns(data.select.columns).select(); - } - - if (dt.cells({ selected: true }).any()) { - dt.cells().deselect(); - } - if (data.select.cells !== undefined) { - for (var i = 0; i < data.select.cells.length; i++) { - dt.cell(data.select.cells[i].row, data.select.cells[i].column).select(); - } - } - - dt.state.save(); - }; - - dt.on('stateSaveParams', function (e, settings, data) { - data.select = {}; - data.select.rows = dt.rows({ selected: true }).ids(true).toArray(); - data.select.columns = dt.columns({ selected: true })[0]; - data.select.cells = dt.cells({ selected: true })[0].map(function (coords) { - return { row: dt.row(coords.row).id(true), column: coords.column }; - }); - }) - .on('stateLoadParams', selectAndSave) - .one('init', function () { - selectAndSave(undefined, undefined, savedSelected); - }); - - var init = ctx.oInit.select; - var defaults = DataTable.defaults.select; - var opts = init === undefined ? defaults : init; - - // Set defaults - var items = 'row'; - var style = 'api'; - var blurable = false; - var toggleable = true; - var info = true; - var selector = 'td, th'; - var className = 'selected'; - var headerCheckbox = true; - var setStyle = false; - - ctx._select = { - infoEls: [] - }; - - // Initialisation customisations - if (opts === true) { - style = 'os'; - setStyle = true; - } - else if (typeof opts === 'string') { - style = opts; - setStyle = true; - } - else if ($.isPlainObject(opts)) { - if (opts.blurable !== undefined) { - blurable = opts.blurable; - } - - if (opts.toggleable !== undefined) { - toggleable = opts.toggleable; - } - - if (opts.info !== undefined) { - info = opts.info; - } - - if (opts.items !== undefined) { - items = opts.items; - } - - if (opts.style !== undefined) { - style = opts.style; - setStyle = true; - } - else { - style = 'os'; - setStyle = true; - } - - if (opts.selector !== undefined) { - selector = opts.selector; - } - - if (opts.className !== undefined) { - className = opts.className; - } - - if (opts.headerCheckbox !== undefined) { - headerCheckbox = opts.headerCheckbox; - } - } - - dt.select.selector(selector); - dt.select.items(items); - dt.select.style(style); - dt.select.blurable(blurable); - dt.select.toggleable(toggleable); - dt.select.info(info); - ctx._select.className = className; - - // If the init options haven't enabled select, but there is a selectable - // class name, then enable - if (!setStyle && $(dt.table().node()).hasClass('selectable')) { - dt.select.style('os'); - } - - // Insert a checkbox into the header if needed - might need to wait - // for init complete, or it might already be done - if (headerCheckbox) { - initCheckboxHeader(dt); - - dt.on('init', function () { - initCheckboxHeader(dt); - }); - } -}; - -/* - -Select is a collection of API methods, event handlers, event emitters and -buttons (for the `Buttons` extension) for DataTables. It provides the following -features, with an overview of how they are implemented: - -## Selection of rows, columns and cells. Whether an item is selected or not is - stored in: - -* rows: a `_select_selected` property which contains a boolean value of the - DataTables' `aoData` object for each row -* columns: a `_select_selected` property which contains a boolean value of the - DataTables' `aoColumns` object for each column -* cells: a `_selected_cells` property which contains an array of boolean values - of the `aoData` object for each row. The array is the same length as the - columns array, with each element of it representing a cell. - -This method of using boolean flags allows Select to operate when nodes have not -been created for rows / cells (DataTables' defer rendering feature). - -## API methods - -A range of API methods are available for triggering selection and de-selection -of rows. Methods are also available to configure the selection events that can -be triggered by an end user (such as which items are to be selected). To a large -extent, these of API methods *is* Select. It is basically a collection of helper -functions that can be used to select items in a DataTable. - -Configuration of select is held in the object `_select` which is attached to the -DataTables settings object on initialisation. Select being available on a table -is not optional when Select is loaded, but its default is for selection only to -be available via the API - so the end user wouldn't be able to select rows -without additional configuration. - -The `_select` object contains the following properties: - -``` -{ - items:string - Can be `rows`, `columns` or `cells`. Defines what item - will be selected if the user is allowed to activate row - selection using the mouse. - style:string - Can be `none`, `single`, `multi` or `os`. Defines the - interaction style when selecting items - blurable:boolean - If row selection can be cleared by clicking outside of - the table - toggleable:boolean - If row selection can be cancelled by repeated clicking - on the row - info:boolean - If the selection summary should be shown in the table - information elements - infoEls:element[] - List of HTML elements with info elements for a table -} -``` - -In addition to the API methods, Select also extends the DataTables selector -options for rows, columns and cells adding a `selected` option to the selector -options object, allowing the developer to select only selected items or -unselected items. - -## Mouse selection of items - -Clicking on items can be used to select items. This is done by a simple event -handler that will select the items using the API methods. - - */ - -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * Local functions - */ - -/** - * Add one or more cells to the selection when shift clicking in OS selection - * style cell selection. - * - * Cell range is more complicated than row and column as we want to select - * in the visible grid rather than by index in sequence. For example, if you - * click first in cell 1-1 and then shift click in 2-2 - cells 1-2 and 2-1 - * should also be selected (and not 1-3, 1-4. etc) - * - * @param {DataTable.Api} dt DataTable - * @param {object} idx Cell index to select to - * @param {object} last Cell index to select from - * @private - */ -function cellRange(dt, idx, last) { - var indexes; - var columnIndexes; - var rowIndexes; - var selectColumns = function (start, end) { - if (start > end) { - var tmp = end; - end = start; - start = tmp; - } - - var record = false; - return dt - .columns(':visible') - .indexes() - .filter(function (i) { - if (i === start) { - record = true; - } - - if (i === end) { - // not else if, as start might === end - record = false; - return true; - } - - return record; - }); - }; - - var selectRows = function (start, end) { - var indexes = dt.rows({ search: 'applied' }).indexes(); - - // Which comes first - might need to swap - if (indexes.indexOf(start) > indexes.indexOf(end)) { - var tmp = end; - end = start; - start = tmp; - } - - var record = false; - return indexes.filter(function (i) { - if (i === start) { - record = true; - } - - if (i === end) { - record = false; - return true; - } - - return record; - }); - }; - - if (!dt.cells({ selected: true }).any() && !last) { - // select from the top left cell to this one - columnIndexes = selectColumns(0, idx.column); - rowIndexes = selectRows(0, idx.row); - } - else { - // Get column indexes between old and new - columnIndexes = selectColumns(last.column, idx.column); - rowIndexes = selectRows(last.row, idx.row); - } - - indexes = dt.cells(rowIndexes, columnIndexes).flatten(); - - if (!dt.cells(idx, { selected: true }).any()) { - // Select range - dt.cells(indexes).select(); - } - else { - // Deselect range - dt.cells(indexes).deselect(); - } -} - -/** - * Disable mouse selection by removing the selectors - * - * @param {DataTable.Api} dt DataTable to remove events from - * @private - */ -function disableMouseSelection(dt) { - var ctx = dt.settings()[0]; - var selector = ctx._select.selector; - - $(dt.table().container()) - .off('mousedown.dtSelect', selector) - .off('mouseup.dtSelect', selector) - .off('click.dtSelect', selector); - - $('body').off('click.dtSelect' + _safeId(dt.table().node())); -} - -/** - * Attach mouse listeners to the table to allow mouse selection of items - * - * @param {DataTable.Api} dt DataTable to remove events from - * @private - */ -function enableMouseSelection(dt) { - var container = $(dt.table().container()); - var ctx = dt.settings()[0]; - var selector = ctx._select.selector; - var matchSelection; - - container - .on('mousedown.dtSelect', selector, function (e) { - // Disallow text selection for shift clicking on the table so multi - // element selection doesn't look terrible! - if (e.shiftKey || e.metaKey || e.ctrlKey) { - container - .css('-moz-user-select', 'none') - .one('selectstart.dtSelect', selector, function () { - return false; - }); - } - - if (window.getSelection) { - matchSelection = window.getSelection(); - } - }) - .on('mouseup.dtSelect', selector, function () { - // Allow text selection to occur again, Mozilla style (tested in FF - // 35.0.1 - still required) - container.css('-moz-user-select', ''); - }) - .on('click.dtSelect', selector, function (e) { - var items = dt.select.items(); - var idx; - - // If text was selected (click and drag), then we shouldn't change - // the row's selected state - if (matchSelection) { - var selection = window.getSelection(); - - // If the element that contains the selection is not in the table, we can ignore it - // This can happen if the developer selects text from the click event - if ( - !selection.anchorNode || - $(selection.anchorNode).closest('table')[0] === dt.table().node() - ) { - if (selection !== matchSelection) { - return; - } - } - } - - var ctx = dt.settings()[0]; - var container = dt.table().container(); - - // Ignore clicks inside a sub-table - if ($(e.target).closest('div.dt-container')[0] != container) { - return; - } - - var cell = dt.cell($(e.target).closest('td, th')); - - // Check the cell actually belongs to the host DataTable (so child - // rows, etc, are ignored) - if (!cell.any()) { - return; - } - - var event = $.Event('user-select.dt'); - eventTrigger(dt, event, [items, cell, e]); - - if (event.isDefaultPrevented()) { - return; - } - - var cellIndex = cell.index(); - if (items === 'row') { - idx = cellIndex.row; - typeSelect(e, dt, ctx, 'row', idx); - } - else if (items === 'column') { - idx = cell.index().column; - typeSelect(e, dt, ctx, 'column', idx); - } - else if (items === 'cell') { - idx = cell.index(); - typeSelect(e, dt, ctx, 'cell', idx); - } - - ctx._select_lastCell = cellIndex; - }); - - // Blurable - $('body').on('click.dtSelect' + _safeId(dt.table().node()), function (e) { - if (ctx._select.blurable) { - // If the click was inside the DataTables container, don't blur - if ($(e.target).parents().filter(dt.table().container()).length) { - return; - } - - // Ignore elements which have been removed from the DOM (i.e. paging - // buttons) - if ($(e.target).parents('html').length === 0) { - return; - } - - // Don't blur in Editor form - if ($(e.target).parents('div.DTE').length) { - return; - } - - var event = $.Event('select-blur.dt'); - eventTrigger(dt, event, [e.target, e]); - - if (event.isDefaultPrevented()) { - return; - } - - clear(ctx, true); - } - }); -} - -/** - * Trigger an event on a DataTable - * - * @param {DataTable.Api} api DataTable to trigger events on - * @param {boolean} selected true if selected, false if deselected - * @param {string} type Item type acting on - * @param {boolean} any Require that there are values before - * triggering - * @private - */ -function eventTrigger(api, type, args, any) { - if (any && !api.flatten().length) { - return; - } - - if (typeof type === 'string') { - type = type + '.dt'; - } - - args.unshift(api); - - $(api.table().node()).trigger(type, args); -} - -/** - * Update the information element of the DataTable showing information about the - * items selected. This is done by adding tags to the existing text - * - * @param {DataTable.Api} api DataTable to update - * @private - */ -function info(api, node) { - if (api.select.style() === 'api' || api.select.info() === false) { - return; - } - - var rows = api.rows({ selected: true }).flatten().length; - var columns = api.columns({ selected: true }).flatten().length; - var cells = api.cells({ selected: true }).flatten().length; - - var add = function (el, name, num) { - el.append( - $('').append( - api.i18n( - 'select.' + name + 's', - { _: '%d ' + name + 's selected', 0: '', 1: '1 ' + name + ' selected' }, - num - ) - ) - ); - }; - - var el = $(node); - var output = $(''); - - add(output, 'row', rows); - add(output, 'column', columns); - add(output, 'cell', cells); - - var existing = el.children('span.select-info'); - - if (existing.length) { - existing.remove(); - } - - if (output.text() !== '') { - el.append(output); - } -} - -/** - * Add a checkbox to the header for checkbox columns, allowing all rows to - * be selected, deselected or just to show the state. - * - * @param {*} dt API - */ -function initCheckboxHeader( dt ) { - // Find any checkbox column(s) - dt.columns('.dt-select').every(function () { - var header = this.header(); - - if (! $('input', header).length) { - // If no checkbox yet, insert one - var input = $('') - .attr({ - class: 'dt-select-checkbox', - type: 'checkbox', - 'aria-label': dt.i18n('select.aria.headerCheckbox') || 'Select all rows' - }) - .appendTo(header) - .on('change', function () { - if (this.checked) { - dt.rows({search: 'applied'}).select(); - } - else { - dt.rows({selected: true}).deselect(); - } - }) - .on('click', function (e) { - e.stopPropagation(); - }); - - // Update the header checkbox's state when the selection in the - // table changes - dt.on('draw select deselect', function (e, pass, type) { - if (type === 'row' || ! type) { - var count = dt.rows({selected: true}).count(); - var search = dt.rows({search: 'applied', selected: true}).count(); - var available = dt.rows({search: 'applied'}).count(); - - if (search && search <= count && search === available) { - input - .prop('checked', true) - .prop('indeterminate', false); - } - else if (search === 0 && count === 0) { - input - .prop('checked', false) - .prop('indeterminate', false); - } - else { - input - .prop('checked', false) - .prop('indeterminate', true); - } - } - }); - } - }); -} - -/** - * Initialisation of a new table. Attach event handlers and callbacks to allow - * Select to operate correctly. - * - * This will occur _after_ the initial DataTables initialisation, although - * before Ajax data is rendered, if there is ajax data - * - * @param {DataTable.settings} ctx Settings object to operate on - * @private - */ -function init(ctx) { - var api = new DataTable.Api(ctx); - ctx._select_init = true; - - // Row callback so that classes can be added to rows and cells if the item - // was selected before the element was created. This will happen with the - // `deferRender` option enabled. - // - // This method of attaching to `aoRowCreatedCallback` is a hack until - // DataTables has proper events for row manipulation If you are reviewing - // this code to create your own plug-ins, please do not do this! - ctx.aoRowCreatedCallback.push(function (row, data, index) { - var i, ien; - var d = ctx.aoData[index]; - - // Row - if (d._select_selected) { - $(row).addClass(ctx._select.className); - } - - // Cells and columns - if separated out, we would need to do two - // loops, so it makes sense to combine them into a single one - for (i = 0, ien = ctx.aoColumns.length; i < ien; i++) { - if ( - ctx.aoColumns[i]._select_selected || - (d._selected_cells && d._selected_cells[i]) - ) { - $(d.anCells[i]).addClass(ctx._select.className); - } - } - } - ); - - // On Ajax reload we want to reselect all rows which are currently selected, - // if there is an rowId (i.e. a unique value to identify each row with) - api.on('preXhr.dt.dtSelect', function (e, settings) { - if (settings !== api.settings()[0]) { - // Not triggered by our DataTable! - return; - } - - // note that column selection doesn't need to be cached and then - // reselected, as they are already selected - var rows = api - .rows({ selected: true }) - .ids(true) - .filter(function (d) { - return d !== undefined; - }); - - var cells = api - .cells({ selected: true }) - .eq(0) - .map(function (cellIdx) { - var id = api.row(cellIdx.row).id(true); - return id ? { row: id, column: cellIdx.column } : undefined; - }) - .filter(function (d) { - return d !== undefined; - }); - - // On the next draw, reselect the currently selected items - api.one('draw.dt.dtSelect', function () { - api.rows(rows).select(); - - // `cells` is not a cell index selector, so it needs a loop - if (cells.any()) { - cells.each(function (id) { - api.cells(id.row, id.column).select(); - }); - } - }); - }); - - // Update the table information element with selected item summary - api.on('info.dt', function (e, ctx, node) { - // Store the info node for updating on select / deselect - if (!ctx._select.infoEls.includes(node)) { - ctx._select.infoEls.push(node); - } - - info(api, node); - }); - - api.on('select.dtSelect.dt deselect.dtSelect.dt', function () { - ctx._select.infoEls.forEach(function (el) { - info(api, el); - }); - - api.state.save(); - }); - - // Clean up and release - api.on('destroy.dtSelect', function () { - // Remove class directly rather than calling deselect - which would trigger events - $(api.rows({ selected: true }).nodes()).removeClass(api.settings()[0]._select.className); - - disableMouseSelection(api); - api.off('.dtSelect'); - $('body').off('.dtSelect' + _safeId(api.table().node())); - }); -} - -/** - * Add one or more items (rows or columns) to the selection when shift clicking - * in OS selection style - * - * @param {DataTable.Api} dt DataTable - * @param {string} type Row or column range selector - * @param {object} idx Item index to select to - * @param {object} last Item index to select from - * @private - */ -function rowColumnRange(dt, type, idx, last) { - // Add a range of rows from the last selected row to this one - var indexes = dt[type + 's']({ search: 'applied' }).indexes(); - var idx1 = indexes.indexOf(last); - var idx2 = indexes.indexOf(idx); - - if (!dt[type + 's']({ selected: true }).any() && idx1 === -1) { - // select from top to here - slightly odd, but both Windows and Mac OS - // do this - indexes.splice(indexes.indexOf(idx) + 1, indexes.length); - } - else { - // reverse so we can shift click 'up' as well as down - if (idx1 > idx2) { - var tmp = idx2; - idx2 = idx1; - idx1 = tmp; - } - - indexes.splice(idx2 + 1, indexes.length); - indexes.splice(0, idx1); - } - - if (!dt[type](idx, { selected: true }).any()) { - // Select range - dt[type + 's'](indexes).select(); - } - else { - // Deselect range - need to keep the clicked on row selected - indexes.splice(indexes.indexOf(idx), 1); - dt[type + 's'](indexes).deselect(); - } -} - -/** - * Clear all selected items - * - * @param {DataTable.settings} ctx Settings object of the host DataTable - * @param {boolean} [force=false] Force the de-selection to happen, regardless - * of selection style - * @private - */ -function clear(ctx, force) { - if (force || ctx._select.style === 'single') { - var api = new DataTable.Api(ctx); - - api.rows({ selected: true }).deselect(); - api.columns({ selected: true }).deselect(); - api.cells({ selected: true }).deselect(); - } -} - -/** - * Select items based on the current configuration for style and items. - * - * @param {object} e Mouse event object - * @param {DataTables.Api} dt DataTable - * @param {DataTable.settings} ctx Settings object of the host DataTable - * @param {string} type Items to select - * @param {int|object} idx Index of the item to select - * @private - */ -function typeSelect(e, dt, ctx, type, idx) { - var style = dt.select.style(); - var toggleable = dt.select.toggleable(); - var isSelected = dt[type](idx, { selected: true }).any(); - - if (isSelected && !toggleable) { - return; - } - - if (style === 'os') { - if (e.ctrlKey || e.metaKey) { - // Add or remove from the selection - dt[type](idx).select(!isSelected); - } - else if (e.shiftKey) { - if (type === 'cell') { - cellRange(dt, idx, ctx._select_lastCell || null); - } - else { - rowColumnRange( - dt, - type, - idx, - ctx._select_lastCell ? ctx._select_lastCell[type] : null - ); - } - } - else { - // No cmd or shift click - deselect if selected, or select - // this row only - var selected = dt[type + 's']({ selected: true }); - - if (isSelected && selected.flatten().length === 1) { - dt[type](idx).deselect(); - } - else { - selected.deselect(); - dt[type](idx).select(); - } - } - } - else if (style == 'multi+shift') { - if (e.shiftKey) { - if (type === 'cell') { - cellRange(dt, idx, ctx._select_lastCell || null); - } - else { - rowColumnRange( - dt, - type, - idx, - ctx._select_lastCell ? ctx._select_lastCell[type] : null - ); - } - } - else { - dt[type](idx).select(!isSelected); - } - } - else { - dt[type](idx).select(!isSelected); - } -} - -function _safeId(node) { - return node.id.replace(/[^a-zA-Z0-9\-\_]/g, '-'); -} - -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * DataTables selectors - */ - -// row and column are basically identical just assigned to different properties -// and checking a different array, so we can dynamically create the functions to -// reduce the code size -$.each( - [ - { type: 'row', prop: 'aoData' }, - { type: 'column', prop: 'aoColumns' } - ], - function (i, o) { - DataTable.ext.selector[o.type].push(function (settings, opts, indexes) { - var selected = opts.selected; - var data; - var out = []; - - if (selected !== true && selected !== false) { - return indexes; - } - - for (var i = 0, ien = indexes.length; i < ien; i++) { - data = settings[o.prop][indexes[i]]; - - if ( - data && ( - (selected === true && data._select_selected === true) || - (selected === false && !data._select_selected) - ) - ) { - out.push(indexes[i]); - } - } - - return out; - }); - } -); - -DataTable.ext.selector.cell.push(function (settings, opts, cells) { - var selected = opts.selected; - var rowData; - var out = []; - - if (selected === undefined) { - return cells; - } - - for (var i = 0, ien = cells.length; i < ien; i++) { - rowData = settings.aoData[cells[i].row]; - - if ( - rowData && ( - (selected === true && - rowData._selected_cells && - rowData._selected_cells[cells[i].column] === true) || - (selected === false && - (!rowData._selected_cells || !rowData._selected_cells[cells[i].column])) - ) - ) { - out.push(cells[i]); - } - } - - return out; -}); - -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * DataTables API - * - * For complete documentation, please refer to the docs/api directory or the - * DataTables site - */ - -// Local variables to improve compression -var apiRegister = DataTable.Api.register; -var apiRegisterPlural = DataTable.Api.registerPlural; - -apiRegister('select()', function () { - return this.iterator('table', function (ctx) { - DataTable.select.init(new DataTable.Api(ctx)); - }); -}); - -apiRegister('select.blurable()', function (flag) { - if (flag === undefined) { - return this.context[0]._select.blurable; - } - - return this.iterator('table', function (ctx) { - ctx._select.blurable = flag; - }); -}); - -apiRegister('select.toggleable()', function (flag) { - if (flag === undefined) { - return this.context[0]._select.toggleable; - } - - return this.iterator('table', function (ctx) { - ctx._select.toggleable = flag; - }); -}); - -apiRegister('select.info()', function (flag) { - if (flag === undefined) { - return this.context[0]._select.info; - } - - return this.iterator('table', function (ctx) { - ctx._select.info = flag; - }); -}); - -apiRegister('select.items()', function (items) { - if (items === undefined) { - return this.context[0]._select.items; - } - - return this.iterator('table', function (ctx) { - ctx._select.items = items; - - eventTrigger(new DataTable.Api(ctx), 'selectItems', [items]); - }); -}); - -// Takes effect from the _next_ selection. None disables future selection, but -// does not clear the current selection. Use the `deselect` methods for that -apiRegister('select.style()', function (style) { - if (style === undefined) { - return this.context[0]._select.style; - } - - return this.iterator('table', function (ctx) { - if (!ctx._select) { - DataTable.select.init(new DataTable.Api(ctx)); - } - - if (!ctx._select_init) { - init(ctx); - } - - ctx._select.style = style; - - // Add / remove mouse event handlers. They aren't required when only - // API selection is available - var dt = new DataTable.Api(ctx); - disableMouseSelection(dt); - - if (style !== 'api') { - enableMouseSelection(dt); - } - - eventTrigger(new DataTable.Api(ctx), 'selectStyle', [style]); - }); -}); - -apiRegister('select.selector()', function (selector) { - if (selector === undefined) { - return this.context[0]._select.selector; - } - - return this.iterator('table', function (ctx) { - disableMouseSelection(new DataTable.Api(ctx)); - - ctx._select.selector = selector; - - if (ctx._select.style !== 'api') { - enableMouseSelection(new DataTable.Api(ctx)); - } - }); -}); - -apiRegister('select.last()', function (set) { - let ctx = this.context[0]; - - if (set) { - ctx._select_lastCell = set; - return this; - } - - return ctx._select_lastCell; -}); - -apiRegisterPlural('rows().select()', 'row().select()', function (select) { - var api = this; - - if (select === false) { - return this.deselect(); - } - - this.iterator('row', function (ctx, idx) { - clear(ctx); - - // There is a good amount of knowledge of DataTables internals in - // this function. It _could_ be done without that, but it would hurt - // performance (or DT would need new APIs for this work) - var dtData = ctx.aoData[idx]; - var dtColumns = ctx.aoColumns; - - $(dtData.nTr).addClass(ctx._select.className); - dtData._select_selected = true; - - for (var i=0 ; i 0); - }); - - this.disable(); - }, - destroy: function (dt, node, config) { - dt.off(config._eventNamespace); - } - }, - showSelected: { - text: i18n('showSelected', 'Show only selected'), - className: 'buttons-show-selected', - action: function (e, dt) { - if (dt.search.fixed('dt-select')) { - // Remove existing function - dt.search.fixed('dt-select', null); - - this.active(false); - } - else { - // Use a fixed filtering function to match on selected rows - // This needs to reference the internal aoData since that is - // where Select stores its reference for the selected state - var dataSrc = dt.settings()[0].aoData; - - dt.search.fixed('dt-select', function (text, data, idx) { - // _select_selected is set by Select on the data object for the row - return dataSrc[idx]._select_selected; - }); - - this.active(true); - } - - dt.draw(); - } - } -}); - -$.each(['Row', 'Column', 'Cell'], function (i, item) { - var lc = item.toLowerCase(); - - DataTable.ext.buttons['select' + item + 's'] = { - text: i18n('select' + item + 's', 'Select ' + lc + 's'), - className: 'buttons-select-' + lc + 's', - action: function () { - this.select.items(lc); - }, - init: function (dt) { - var that = this; - - dt.on('selectItems.dt.DT', function (e, ctx, items) { - that.active(items === lc); - }); - } - }; -}); - -DataTable.type('select-checkbox', { - className: 'dt-select', - detect: function (data) { - // Rendering function will tell us if it is a checkbox type - return data === 'select-checkbox' ? data : false; - }, - order: { - pre: function (d) { - return d === 'X' ? -1 : 0; - } - } -}); - -$.extend(true, DataTable.defaults.oLanguage, { - select: { - aria: { - rowCheckbox: 'Select row' - } - } -}); - -DataTable.render.select = function (valueProp, nameProp) { - var valueFn = valueProp ? DataTable.util.get(valueProp) : null; - var nameFn = nameProp ? DataTable.util.get(nameProp) : null; - - return function (data, type, row, meta) { - var dtRow = meta.settings.aoData[meta.row]; - var selected = dtRow._select_selected; - var ariaLabel = meta.settings.oLanguage.select.aria.rowCheckbox; - - if (type === 'display') { - return $('') - .attr({ - 'aria-label': ariaLabel, - class: 'dt-select-checkbox', - name: nameFn ? nameFn(row) : null, - type: 'checkbox', - value: valueFn ? valueFn(row) : null, - checked: selected - })[0]; - } - else if (type === 'type') { - return 'select-checkbox'; - } - else if (type === 'filter') { - return ''; - } - - return selected ? 'X' : ''; - } -} - -// Legacy checkbox ordering -DataTable.ext.order['select-checkbox'] = function (settings, col) { - return this.api() - .column(col, { order: 'index' }) - .nodes() - .map(function (td) { - if (settings._select.items === 'row') { - return $(td).parent().hasClass(settings._select.className); - } - else if (settings._select.items === 'cell') { - return $(td).hasClass(settings._select.className); - } - return false; - }); -}; - -$.fn.DataTable.select = DataTable.select; - -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * Initialisation - */ - -// DataTables creation - check if select has been defined in the options. Note -// this required that the table be in the document! If it isn't then something -// needs to trigger this method unfortunately. The next major release of -// DataTables will rework the events and address this. -$(document).on('preInit.dt.dtSelect', function (e, ctx) { - if (e.namespace !== 'dt') { - return; - } - - DataTable.select.init(new DataTable.Api(ctx)); -}); - - -export default DataTable; diff --git a/assets/js/register_events.js b/assets/js/register_events.js index 6a068bd3..2a8f5ef4 100644 --- a/assets/js/register_events.js +++ b/assets/js/register_events.js @@ -19,9 +19,8 @@ 'use strict'; -import {Dropdown} from "bootstrap"; +import {Dropdown, Modal, Tooltip} from "bootstrap"; import ClipboardJS from "clipboard"; -import {Modal} from "bootstrap"; class RegisterEventHelper { constructor() { @@ -40,8 +39,6 @@ class RegisterEventHelper { }); this.registerModalDropRemovalOnFormSubmit(); - - } registerModalDropRemovalOnFormSubmit() { @@ -83,11 +80,17 @@ class RegisterEventHelper { registerTooltips() { const handler = () => { - $(".tooltip").remove(); + document.querySelectorAll('.tooltip').forEach(el => el.remove()); + //Exclude dropdown buttons from tooltips, otherwise we run into endless errors from bootstrap (bootstrap.esm.js:614 Bootstrap doesn't allow more than one instance per element. Bound instance: bs.dropdown.) - $('a[title], label[title], button[title]:not([data-bs-toggle="dropdown"]), p[title], span[title], h6[title], h3[title], i[title], small[title], div[title]') - //@ts-ignore - .tooltip("hide").tooltip({container: "body", placement: "auto", boundary: 'window'}); + const tooltipSelector = 'a[title], label[title], button[title]:not([data-bs-toggle="dropdown"]), p[title], span[title], h6[title], h3[title], i[title], small[title], div[title]'; + document.querySelectorAll(tooltipSelector).forEach(el => { + const existing = Tooltip.getInstance(el); + if (existing) { + existing.dispose(); + } + new Tooltip(el, {container: 'body', placement: 'auto', boundary: 'window'}); + }); }; this.registerLoadHandler(handler); @@ -95,241 +98,239 @@ class RegisterEventHelper { } registerSpecialCharInput() { - this.registerLoadHandler(() => { - //@ts-ignore - $("input[type=text], input[type=search]").unbind("keydown").keydown(function (event) { - let use_special_char = event.altKey; - - let greek_char = ""; - if (use_special_char){ - //Use the key property to determine the greek letter (as it is independent of the keyboard layout) - switch(event.key) { - //Greek letters - case "a": //Alpha (lowercase) - greek_char = "\u03B1"; - break; - case "A": //Alpha (uppercase) - greek_char = "\u0391"; - break; - case "b": //Beta (lowercase) - greek_char = "\u03B2"; - break; - case "B": //Beta (uppercase) - greek_char = "\u0392"; - break; - case "g": //Gamma (lowercase) - greek_char = "\u03B3"; - break; - case "G": //Gamma (uppercase) - greek_char = "\u0393"; - break; - case "d": //Delta (lowercase) - greek_char = "\u03B4"; - break; - case "D": //Delta (uppercase) - greek_char = "\u0394"; - break; - case "e": //Epsilon (lowercase) - greek_char = "\u03B5"; - break; - case "E": //Epsilon (uppercase) - greek_char = "\u0395"; - break; - case "z": //Zeta (lowercase) - greek_char = "\u03B6"; - break; - case "Z": //Zeta (uppercase) - greek_char = "\u0396"; - break; - case "h": //Eta (lowercase) - greek_char = "\u03B7"; - break; - case "H": //Eta (uppercase) - greek_char = "\u0397"; - break; - case "q": //Theta (lowercase) - greek_char = "\u03B8"; - break; - case "Q": //Theta (uppercase) - greek_char = "\u0398"; - break; - case "i": //Iota (lowercase) - greek_char = "\u03B9"; - break; - case "I": //Iota (uppercase) - greek_char = "\u0399"; - break; - case "k": //Kappa (lowercase) - greek_char = "\u03BA"; - break; - case "K": //Kappa (uppercase) - greek_char = "\u039A"; - break; - case "l": //Lambda (lowercase) - greek_char = "\u03BB"; - break; - case "L": //Lambda (uppercase) - greek_char = "\u039B"; - break; - case "m": //Mu (lowercase) - greek_char = "\u03BC"; - break; - case "M": //Mu (uppercase) - greek_char = "\u039C"; - break; - case "n": //Nu (lowercase) - greek_char = "\u03BD"; - break; - case "N": //Nu (uppercase) - greek_char = "\u039D"; - break; - case "x": //Xi (lowercase) - greek_char = "\u03BE"; - break; - case "X": //Xi (uppercase) - greek_char = "\u039E"; - break; - case "o": //Omicron (lowercase) - greek_char = "\u03BF"; - break; - case "O": //Omicron (uppercase) - greek_char = "\u039F"; - break; - case "p": //Pi (lowercase) - greek_char = "\u03C0"; - break; - case "P": //Pi (uppercase) - greek_char = "\u03A0"; - break; - case "r": //Rho (lowercase) - greek_char = "\u03C1"; - break; - case "R": //Rho (uppercase) - greek_char = "\u03A1"; - break; - case "s": //Sigma (lowercase) - greek_char = "\u03C3"; - break; - case "S": //Sigma (uppercase) - greek_char = "\u03A3"; - break; - case "t": //Tau (lowercase) - greek_char = "\u03C4"; - break; - case "T": //Tau (uppercase) - greek_char = "\u03A4"; - break; - case "u": //Upsilon (lowercase) - greek_char = "\u03C5"; - break; - case "U": //Upsilon (uppercase) - greek_char = "\u03A5"; - break; - case "f": //Phi (lowercase) - greek_char = "\u03C6"; - break; - case "F": //Phi (uppercase) - greek_char = "\u03A6"; - break; - case "c": //Chi (lowercase) - greek_char = "\u03C7"; - break; - case "C": //Chi (uppercase) - greek_char = "\u03A7"; - break; - case "y": //Psi (lowercase) - greek_char = "\u03C8"; - break; - case "Y": //Psi (uppercase) - greek_char = "\u03A8"; - break; - case "w": //Omega (lowercase) - greek_char = "\u03C9"; - break; - case "W": //Omega (uppercase) - greek_char = "\u03A9"; - break; - } - - //Use keycodes for special characters as the shift char on the number keys are layout dependent - switch (event.keyCode) { - case 49: //1 key - //Product symbol on shift, sum on no shift - greek_char = event.shiftKey ? "\u220F" : "\u2211"; - break; - case 50: //2 key - //Integral on no shift, partial derivative on shift - greek_char = event.shiftKey ? "\u2202" : "\u222B"; - break; - case 51: //3 key - //Less than or equal on no shift, greater than or equal on shift - greek_char = event.shiftKey ? "\u2265" : "\u2264"; - break; - case 52: //4 key - //Empty set on shift, infinity on no shift - greek_char = event.shiftKey ? "\u2205" : "\u221E"; - break; - case 53: //5 key - //Not equal on shift, approx equal on no shift - greek_char = event.shiftKey ? "\u2260" : "\u2248"; - break; - case 54: //6 key - //Element of on no shift, not element of on shift - greek_char = event.shiftKey ? "\u2209" : "\u2208"; - break; - case 55: //7 key - //And on shift, or on no shift - greek_char = event.shiftKey ? "\u2227" : "\u2228"; - break; - case 56: //8 key - //Proportional to on shift, angle on no shift - greek_char = event.shiftKey ? "\u221D" : "\u2220"; - break; - case 57: //9 key - //Cube root on shift, square root on no shift - greek_char = event.shiftKey ? "\u221B" : "\u221A"; - break; - case 48: //0 key - //Minus-Plus on shift, plus-minus on no shift - greek_char = event.shiftKey ? "\u2213" : "\u00B1"; - break; - - //Special characters - case 219: //hyphen (or ß on german layout) - //Copyright on no shift, TM on shift - greek_char = event.shiftKey ? "\u2122" : "\u00A9"; - break; - case 191: //forward slash (or # on german layout) - //Generic currency on no shift, paragraph on shift - greek_char = event.shiftKey ? "\u00B6" : "\u00A4"; - break; - - //Currency symbols - case 192: //: or (ö on german layout) - //Euro on no shift, pound on shift - greek_char = event.shiftKey ? "\u00A3" : "\u20AC"; - break; - case 221: //; or (ä on german layout) - //Yen on no shift, dollar on shift - greek_char = event.shiftKey ? "\u0024" : "\u00A5"; - break; - - - } - - if(greek_char=="") return; - - let $txt = $(this); - //@ts-ignore - let caretPos = $txt[0].selectionStart; - let textAreaTxt = $txt.val().toString(); - $txt.val(textAreaTxt.substring(0, caretPos) + greek_char + textAreaTxt.substring(caretPos) ); + const keydownHandler = function(event) { + let use_special_char = event.altKey; + let greek_char = ""; + if (use_special_char){ + //Use the key property to determine the greek letter (as it is independent of the keyboard layout) + switch(event.key) { + //Greek letters + case "a": //Alpha (lowercase) + greek_char = "α"; + break; + case "A": //Alpha (uppercase) + greek_char = "Α"; + break; + case "b": //Beta (lowercase) + greek_char = "β"; + break; + case "B": //Beta (uppercase) + greek_char = "Β"; + break; + case "g": //Gamma (lowercase) + greek_char = "γ"; + break; + case "G": //Gamma (uppercase) + greek_char = "Γ"; + break; + case "d": //Delta (lowercase) + greek_char = "δ"; + break; + case "D": //Delta (uppercase) + greek_char = "Δ"; + break; + case "e": //Epsilon (lowercase) + greek_char = "ε"; + break; + case "E": //Epsilon (uppercase) + greek_char = "Ε"; + break; + case "z": //Zeta (lowercase) + greek_char = "ζ"; + break; + case "Z": //Zeta (uppercase) + greek_char = "Ζ"; + break; + case "h": //Eta (lowercase) + greek_char = "η"; + break; + case "H": //Eta (uppercase) + greek_char = "Η"; + break; + case "q": //Theta (lowercase) + greek_char = "θ"; + break; + case "Q": //Theta (uppercase) + greek_char = "Θ"; + break; + case "i": //Iota (lowercase) + greek_char = "ι"; + break; + case "I": //Iota (uppercase) + greek_char = "Ι"; + break; + case "k": //Kappa (lowercase) + greek_char = "κ"; + break; + case "K": //Kappa (uppercase) + greek_char = "Κ"; + break; + case "l": //Lambda (lowercase) + greek_char = "λ"; + break; + case "L": //Lambda (uppercase) + greek_char = "Λ"; + break; + case "m": //Mu (lowercase) + greek_char = "μ"; + break; + case "M": //Mu (uppercase) + greek_char = "Μ"; + break; + case "n": //Nu (lowercase) + greek_char = "ν"; + break; + case "N": //Nu (uppercase) + greek_char = "Ν"; + break; + case "x": //Xi (lowercase) + greek_char = "ξ"; + break; + case "X": //Xi (uppercase) + greek_char = "Ξ"; + break; + case "o": //Omicron (lowercase) + greek_char = "ο"; + break; + case "O": //Omicron (uppercase) + greek_char = "Ο"; + break; + case "p": //Pi (lowercase) + greek_char = "π"; + break; + case "P": //Pi (uppercase) + greek_char = "Π"; + break; + case "r": //Rho (lowercase) + greek_char = "ρ"; + break; + case "R": //Rho (uppercase) + greek_char = "Ρ"; + break; + case "s": //Sigma (lowercase) + greek_char = "σ"; + break; + case "S": //Sigma (uppercase) + greek_char = "Σ"; + break; + case "t": //Tau (lowercase) + greek_char = "τ"; + break; + case "T": //Tau (uppercase) + greek_char = "Τ"; + break; + case "u": //Upsilon (lowercase) + greek_char = "υ"; + break; + case "U": //Upsilon (uppercase) + greek_char = "Υ"; + break; + case "f": //Phi (lowercase) + greek_char = "φ"; + break; + case "F": //Phi (uppercase) + greek_char = "Φ"; + break; + case "c": //Chi (lowercase) + greek_char = "χ"; + break; + case "C": //Chi (uppercase) + greek_char = "Χ"; + break; + case "y": //Psi (lowercase) + greek_char = "ψ"; + break; + case "Y": //Psi (uppercase) + greek_char = "Ψ"; + break; + case "w": //Omega (lowercase) + greek_char = "ω"; + break; + case "W": //Omega (uppercase) + greek_char = "Ω"; + break; } + + //Use keycodes for special characters as the shift char on the number keys are layout dependent + switch (event.keyCode) { + case 49: //1 key + //Product symbol on shift, sum on no shift + greek_char = event.shiftKey ? "∏" : "∑"; + break; + case 50: //2 key + //Integral on no shift, partial derivative on shift + greek_char = event.shiftKey ? "∂" : "∫"; + break; + case 51: //3 key + //Less than or equal on no shift, greater than or equal on shift + greek_char = event.shiftKey ? "≥" : "≤"; + break; + case 52: //4 key + //Empty set on shift, infinity on no shift + greek_char = event.shiftKey ? "∅" : "∞"; + break; + case 53: //5 key + //Not equal on shift, approx equal on no shift + greek_char = event.shiftKey ? "≠" : "≈"; + break; + case 54: //6 key + //Element of on no shift, not element of on shift + greek_char = event.shiftKey ? "∉" : "∈"; + break; + case 55: //7 key + //And on shift, or on no shift + greek_char = event.shiftKey ? "∧" : "∨"; + break; + case 56: //8 key + //Proportional to on shift, angle on no shift + greek_char = event.shiftKey ? "∝" : "∠"; + break; + case 57: //9 key + //Cube root on shift, square root on no shift + greek_char = event.shiftKey ? "∛" : "√"; + break; + case 48: //0 key + //Minus-Plus on shift, plus-minus on no shift + greek_char = event.shiftKey ? "∓" : "±"; + break; + + //Special characters + case 219: //hyphen (or ß on german layout) + //Copyright on no shift, TM on shift + greek_char = event.shiftKey ? "™" : "©"; + break; + case 191: //forward slash (or # on german layout) + //Generic currency on no shift, paragraph on shift + greek_char = event.shiftKey ? "¶" : "¤"; + break; + + //Currency symbols + case 192: //: or (ö on german layout) + //Euro on no shift, pound on shift + greek_char = event.shiftKey ? "£" : "€"; + break; + case 221: //; or (ä on german layout) + //Yen on no shift, dollar on shift + greek_char = event.shiftKey ? "$" : "¥"; + break; + } + + if(greek_char=="") return; + + const txt = event.currentTarget; + const caretPos = txt.selectionStart; + const textAreaTxt = txt.value; + txt.value = textAreaTxt.substring(0, caretPos) + greek_char + textAreaTxt.substring(caretPos); + } + }; + + this.registerLoadHandler(() => { + document.querySelectorAll('input[type=text], input[type=search]').forEach(input => { + input.removeEventListener('keydown', keydownHandler); + input.addEventListener('keydown', keydownHandler); }); - //@ts-ignore - this.greek_once = true; - }) + }); } } diff --git a/assets/themes/brite.js b/assets/themes/brite.js new file mode 100644 index 00000000..41b82e93 --- /dev/null +++ b/assets/themes/brite.js @@ -0,0 +1,20 @@ +/* + * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). + * + * Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import "bootswatch/dist/brite/bootstrap.css"; diff --git a/composer.json b/composer.json index a1f15834..bd3eb9f1 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,9 @@ "amphp/http-client": "^5.1", "api-platform/doctrine-orm": "^4.1", "api-platform/json-api": "^4.0.0", + "api-platform/mcp": "^v4.3.17", "api-platform/symfony": "^4.0.0", + "api-platform/metadata": "^v4.3.17", "beberlei/doctrineextensions": "^1.2", "brick/math": "^0.17.0", "brick/schema": "^0.2.0", @@ -27,7 +29,7 @@ "doctrine/doctrine-migrations-bundle": "^3.0", "doctrine/orm": "^3.2.0", "dompdf/dompdf": "^3.1.2", - "gregwar/captcha-bundle": "^2.1.0", + "gregwar/captcha-bundle": "^3.0.0", "hshn/base64-encoded-file": "^6.0", "jbtronics/2fa-webauthn": "^3.0.0", "jbtronics/dompdf-font-loader-bundle": "^1.0.0", @@ -40,6 +42,7 @@ "league/html-to-markdown": "^5.0.1", "liip/imagine-bundle": "^2.2", "maennchen/zipstream-php": "2.1", + "mcp/sdk": "v0.7.0 as v0.6.0", "nbgrp/onelogin-saml-bundle": "^v2.0.2", "nelexa/zip": "^4.0", "nelmio/cors-bundle": "^2.3", @@ -57,9 +60,10 @@ "scheb/2fa-trusted-device": "^v7.11.0", "shivas/versioning-bundle": "^4.0", "spatie/db-dumper": "^3.3.1", - "symfony/ai-bundle": "^0.9.0", - "symfony/ai-lm-studio-platform": "^0.9.0", - "symfony/ai-open-router-platform": "^0.9.0", + "symfony/ai-bundle": "^0.12.0", + "symfony/ai-lm-studio-platform": "^v0.12.0", + "symfony/ai-ollama-platform": "^0.12.0", + "symfony/ai-open-router-platform": "^0.12.0", "symfony/apache-pack": "^1.0", "symfony/asset": "7.4.*", "symfony/console": "7.4.*", @@ -70,9 +74,11 @@ "symfony/flex": "^v2.3.1", "symfony/form": "7.4.*", "symfony/framework-bundle": "7.4.*", + "symfony/html-sanitizer": "7.4.*", "symfony/http-client": "7.4.*", "symfony/http-kernel": "7.4.*", "symfony/mailer": "7.4.*", + "symfony/mcp-bundle": "^v0.12.0", "symfony/monolog-bundle": "^4.0", "symfony/process": "7.4.*", "symfony/property-access": "7.4.*", diff --git a/composer.lock b/composer.lock index 1c2f7dda..39341239 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d6bda397c505e1e6d540c814a2368fbb", + "content-hash": "df7fa4865832ec3d95601bb711eb96d6", "packages": [ { "name": "amphp/amp", - "version": "v3.1.1", + "version": "v3.1.3", "source": { "type": "git", "url": "https://github.com/amphp/amp.git", - "reference": "fa0ab33a6f47a82929c38d03ca47ebb71086a93f" + "reference": "73c38b323ff8d790abf0f76c56fcc892bda4111a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/amp/zipball/fa0ab33a6f47a82929c38d03ca47ebb71086a93f", - "reference": "fa0ab33a6f47a82929c38d03ca47ebb71086a93f", + "url": "https://api.github.com/repos/amphp/amp/zipball/73c38b323ff8d790abf0f76c56fcc892bda4111a", + "reference": "73c38b323ff8d790abf0f76c56fcc892bda4111a", "shasum": "" }, "require": { @@ -27,7 +27,7 @@ "require-dev": { "amphp/php-cs-fixer-config": "^2", "phpunit/phpunit": "^9", - "psalm/phar": "5.23.1" + "psalm/phar": "6.16.1" }, "type": "library", "autoload": { @@ -77,7 +77,7 @@ ], "support": { "issues": "https://github.com/amphp/amp/issues", - "source": "https://github.com/amphp/amp/tree/v3.1.1" + "source": "https://github.com/amphp/amp/tree/v3.1.3" }, "funding": [ { @@ -85,7 +85,7 @@ "type": "github" } ], - "time": "2025-08-27T21:42:00+00:00" + "time": "2026-07-19T17:59:20+00:00" }, { "name": "amphp/byte-stream", @@ -615,16 +615,16 @@ }, { "name": "amphp/pipeline", - "version": "v1.2.4", + "version": "v1.2.7", "source": { "type": "git", "url": "https://github.com/amphp/pipeline.git", - "reference": "a044733e080940d1483f56caff0c412ad6982776" + "reference": "cf2d67696c2015ea7c7fd8f6ec5f8ed7c2d17c17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/pipeline/zipball/a044733e080940d1483f56caff0c412ad6982776", - "reference": "a044733e080940d1483f56caff0c412ad6982776", + "url": "https://api.github.com/repos/amphp/pipeline/zipball/cf2d67696c2015ea7c7fd8f6ec5f8ed7c2d17c17", + "reference": "cf2d67696c2015ea7c7fd8f6ec5f8ed7c2d17c17", "shasum": "" }, "require": { @@ -670,7 +670,7 @@ ], "support": { "issues": "https://github.com/amphp/pipeline/issues", - "source": "https://github.com/amphp/pipeline/tree/v1.2.4" + "source": "https://github.com/amphp/pipeline/tree/v1.2.7" }, "funding": [ { @@ -678,7 +678,7 @@ "type": "github" } ], - "time": "2026-05-06T05:37:57+00:00" + "time": "2026-07-26T14:50:43+00:00" }, { "name": "amphp/process", @@ -976,16 +976,16 @@ }, { "name": "api-platform/doctrine-common", - "version": "v4.3.10", + "version": "v4.3.17", "source": { "type": "git", "url": "https://github.com/api-platform/doctrine-common.git", - "reference": "a342c7e4cd4a7545d355b8eaae6d2f46de4f8936" + "reference": "e4dee10c45bd701c5984321bc98adc0c3760ec48" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/doctrine-common/zipball/a342c7e4cd4a7545d355b8eaae6d2f46de4f8936", - "reference": "a342c7e4cd4a7545d355b8eaae6d2f46de4f8936", + "url": "https://api.github.com/repos/api-platform/doctrine-common/zipball/e4dee10c45bd701c5984321bc98adc0c3760ec48", + "reference": "e4dee10c45bd701c5984321bc98adc0c3760ec48", "shasum": "" }, "require": { @@ -1060,22 +1060,22 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/doctrine-common/tree/v4.3.10" + "source": "https://github.com/api-platform/doctrine-common/tree/v4.3.17" }, - "time": "2026-06-05T09:05:29+00:00" + "time": "2026-06-16T14:59:18+00:00" }, { "name": "api-platform/doctrine-orm", - "version": "v4.3.10", + "version": "v4.3.17", "source": { "type": "git", "url": "https://github.com/api-platform/doctrine-orm.git", - "reference": "db75bac977cc9fe76cee90c1e5361c744cfa2144" + "reference": "8da8676d5933235aa3592e613f837fe3c687a0cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/doctrine-orm/zipball/db75bac977cc9fe76cee90c1e5361c744cfa2144", - "reference": "db75bac977cc9fe76cee90c1e5361c744cfa2144", + "url": "https://api.github.com/repos/api-platform/doctrine-orm/zipball/8da8676d5933235aa3592e613f837fe3c687a0cb", + "reference": "8da8676d5933235aa3592e613f837fe3c687a0cb", "shasum": "" }, "require": { @@ -1149,13 +1149,13 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/doctrine-orm/tree/v4.3.10" + "source": "https://github.com/api-platform/doctrine-orm/tree/v4.3.17" }, - "time": "2026-06-04T13:58:17+00:00" + "time": "2026-07-08T06:49:21+00:00" }, { "name": "api-platform/documentation", - "version": "v4.3.10", + "version": "v4.3.17", "source": { "type": "git", "url": "https://github.com/api-platform/documentation.git", @@ -1212,22 +1212,22 @@ ], "description": "API Platform documentation controller.", "support": { - "source": "https://github.com/api-platform/documentation/tree/v4.3.10" + "source": "https://github.com/api-platform/documentation/tree/v4.4.0-alpha.1" }, "time": "2026-04-30T12:21:24+00:00" }, { "name": "api-platform/http-cache", - "version": "v4.3.10", + "version": "v4.3.17", "source": { "type": "git", "url": "https://github.com/api-platform/http-cache.git", - "reference": "98e5ba9344abe9db7d6789429745ee4eb0a2fd8f" + "reference": "8e71916de766f503dd60f1a1e886b4d704f881a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/http-cache/zipball/98e5ba9344abe9db7d6789429745ee4eb0a2fd8f", - "reference": "98e5ba9344abe9db7d6789429745ee4eb0a2fd8f", + "url": "https://api.github.com/repos/api-platform/http-cache/zipball/8e71916de766f503dd60f1a1e886b4d704f881a8", + "reference": "8e71916de766f503dd60f1a1e886b4d704f881a8", "shasum": "" }, "require": { @@ -1292,22 +1292,22 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/http-cache/tree/v4.3.10" + "source": "https://github.com/api-platform/http-cache/tree/v4.4.0-alpha.1" }, - "time": "2026-05-29T13:58:43+00:00" + "time": "2026-06-09T14:20:49+00:00" }, { "name": "api-platform/hydra", - "version": "v4.3.10", + "version": "v4.3.17", "source": { "type": "git", "url": "https://github.com/api-platform/hydra.git", - "reference": "a0314c8dda95f69f15054d7309ddbc1ca32f2149" + "reference": "2570883edf78970ad845f6eb7d69ae7894e6c480" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/hydra/zipball/a0314c8dda95f69f15054d7309ddbc1ca32f2149", - "reference": "a0314c8dda95f69f15054d7309ddbc1ca32f2149", + "url": "https://api.github.com/repos/api-platform/hydra/zipball/2570883edf78970ad845f6eb7d69ae7894e6c480", + "reference": "2570883edf78970ad845f6eb7d69ae7894e6c480", "shasum": "" }, "require": { @@ -1315,7 +1315,7 @@ "api-platform/json-schema": "^4.3", "api-platform/jsonld": "^4.3", "api-platform/metadata": "^4.3", - "api-platform/serializer": "^4.3", + "api-platform/serializer": "^4.3.12", "api-platform/state": "^4.3", "php": ">=8.2", "symfony/type-info": "^7.3 || ^8.0", @@ -1379,29 +1379,29 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/hydra/tree/v4.3.10" + "source": "https://github.com/api-platform/hydra/tree/v4.3.17" }, - "time": "2026-06-04T13:52:24+00:00" + "time": "2026-06-22T16:14:53+00:00" }, { "name": "api-platform/json-api", - "version": "v4.3.10", + "version": "v4.3.17", "source": { "type": "git", "url": "https://github.com/api-platform/json-api.git", - "reference": "2e1773bc4b098119531c59de14124afa0cb693a2" + "reference": "30f70ddc6d865e9c36d99c0255bb1f407c4d4258" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/json-api/zipball/2e1773bc4b098119531c59de14124afa0cb693a2", - "reference": "2e1773bc4b098119531c59de14124afa0cb693a2", + "url": "https://api.github.com/repos/api-platform/json-api/zipball/30f70ddc6d865e9c36d99c0255bb1f407c4d4258", + "reference": "30f70ddc6d865e9c36d99c0255bb1f407c4d4258", "shasum": "" }, "require": { "api-platform/documentation": "^4.3", "api-platform/json-schema": "^4.3", "api-platform/metadata": "^4.3", - "api-platform/serializer": "^4.3.8", + "api-platform/serializer": "^4.3.12", "api-platform/state": "^4.3", "php": ">=8.2", "symfony/error-handler": "^6.4 || ^7.0 || ^8.0", @@ -1461,22 +1461,22 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/json-api/tree/v4.3.10" + "source": "https://github.com/api-platform/json-api/tree/v4.3.17" }, - "time": "2026-06-05T15:17:34+00:00" + "time": "2026-06-17T18:14:46+00:00" }, { "name": "api-platform/json-schema", - "version": "v4.3.10", + "version": "v4.3.17", "source": { "type": "git", "url": "https://github.com/api-platform/json-schema.git", - "reference": "6a0a2ddc4914bb1fdb1b71777c49aef388f6ea31" + "reference": "d44aa7acae9e748b3ec55af19f2de4a9a7e79133" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/json-schema/zipball/6a0a2ddc4914bb1fdb1b71777c49aef388f6ea31", - "reference": "6a0a2ddc4914bb1fdb1b71777c49aef388f6ea31", + "url": "https://api.github.com/repos/api-platform/json-schema/zipball/d44aa7acae9e748b3ec55af19f2de4a9a7e79133", + "reference": "d44aa7acae9e748b3ec55af19f2de4a9a7e79133", "shasum": "" }, "require": { @@ -1542,27 +1542,27 @@ "swagger" ], "support": { - "source": "https://github.com/api-platform/json-schema/tree/v4.3.10" + "source": "https://github.com/api-platform/json-schema/tree/v4.3.17" }, - "time": "2026-06-03T14:46:27+00:00" + "time": "2026-06-29T14:45:14+00:00" }, { "name": "api-platform/jsonld", - "version": "v4.3.10", + "version": "v4.3.17", "source": { "type": "git", "url": "https://github.com/api-platform/jsonld.git", - "reference": "20ca6d7b5c11674c3046d710aaa0c9bc1795e54b" + "reference": "026a380c3c85c4210028da43e0cea1b64211bbf5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/jsonld/zipball/20ca6d7b5c11674c3046d710aaa0c9bc1795e54b", - "reference": "20ca6d7b5c11674c3046d710aaa0c9bc1795e54b", + "url": "https://api.github.com/repos/api-platform/jsonld/zipball/026a380c3c85c4210028da43e0cea1b64211bbf5", + "reference": "026a380c3c85c4210028da43e0cea1b64211bbf5", "shasum": "" }, "require": { "api-platform/metadata": "^4.3", - "api-platform/serializer": "^4.3", + "api-platform/serializer": "^4.3.12", "api-platform/state": "^4.3", "php": ">=8.2" }, @@ -1622,22 +1622,99 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/jsonld/tree/v4.3.10" + "source": "https://github.com/api-platform/jsonld/tree/v4.3.17" }, - "time": "2026-04-30T12:21:24+00:00" + "time": "2026-06-13T05:11:46+00:00" }, { - "name": "api-platform/metadata", - "version": "v4.3.10", + "name": "api-platform/mcp", + "version": "v4.3.17", "source": { "type": "git", - "url": "https://github.com/api-platform/metadata.git", - "reference": "b835ce534861f9e7b2a316fa582f28afe4ad5fe6" + "url": "https://github.com/api-platform/mcp.git", + "reference": "97eaecbf7b30fe5cf05a0278251c421d39be047a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/metadata/zipball/b835ce534861f9e7b2a316fa582f28afe4ad5fe6", - "reference": "b835ce534861f9e7b2a316fa582f28afe4ad5fe6", + "url": "https://api.github.com/repos/api-platform/mcp/zipball/97eaecbf7b30fe5cf05a0278251c421d39be047a", + "reference": "97eaecbf7b30fe5cf05a0278251c421d39be047a", + "shasum": "" + }, + "require": { + "api-platform/json-schema": "^4.3", + "api-platform/metadata": "^4.3", + "mcp/sdk": "^0.6", + "php": ">=8.2", + "symfony/object-mapper": "^7.4 || ^8.0", + "symfony/polyfill-php85": "^1.32" + }, + "require-dev": { + "phpunit/phpunit": "^11.5 || ^12.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/api-platform/api-platform", + "name": "api-platform/api-platform" + }, + "symfony": { + "require": "^6.4 || ^7.0 || ^8.0" + }, + "branch-alias": { + "dev-main": "4.4.x-dev" + } + }, + "autoload": { + "psr-4": { + "ApiPlatform\\Mcp\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "kevin@dunglas.fr", + "homepage": "https://dunglas.fr" + }, + { + "name": "Antoine Bluchet", + "email": "soyuka@gmail.com", + "homepage": "https://soyuka.me" + }, + { + "name": "API Platform Community", + "homepage": "https://api-platform.com/community/contributors" + } + ], + "description": "Model Context Protocol (MCP) integration for API Platform", + "homepage": "https://api-platform.com", + "keywords": [ + "Model Context Protocol", + "api", + "mcp", + "rest" + ], + "support": { + "issues": "https://github.com/api-platform/mcp/issues", + "source": "https://github.com/api-platform/mcp/tree/v4.3.17" + }, + "time": "2026-07-03T04:48:00+00:00" + }, + { + "name": "api-platform/metadata", + "version": "v4.3.17", + "source": { + "type": "git", + "url": "https://github.com/api-platform/metadata.git", + "reference": "748f495474b4deedcba286631c2adf96d7bcba0a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/api-platform/metadata/zipball/748f495474b4deedcba286631c2adf96d7bcba0a", + "reference": "748f495474b4deedcba286631c2adf96d7bcba0a", "shasum": "" }, "require": { @@ -1720,22 +1797,22 @@ "swagger" ], "support": { - "source": "https://github.com/api-platform/metadata/tree/v4.3.10" + "source": "https://github.com/api-platform/metadata/tree/v4.3.17" }, - "time": "2026-06-05T09:05:29+00:00" + "time": "2026-07-03T06:30:28+00:00" }, { "name": "api-platform/openapi", - "version": "v4.3.10", + "version": "v4.3.17", "source": { "type": "git", "url": "https://github.com/api-platform/openapi.git", - "reference": "21e22f4d74fafc4b01ee5790be7a264387445dcf" + "reference": "c72470132f2eb35a4f8f252e60342f0f7c487704" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/openapi/zipball/21e22f4d74fafc4b01ee5790be7a264387445dcf", - "reference": "21e22f4d74fafc4b01ee5790be7a264387445dcf", + "url": "https://api.github.com/repos/api-platform/openapi/zipball/c72470132f2eb35a4f8f252e60342f0f7c487704", + "reference": "c72470132f2eb35a4f8f252e60342f0f7c487704", "shasum": "" }, "require": { @@ -1753,7 +1830,7 @@ "api-platform/doctrine-common": "^4.3", "api-platform/doctrine-odm": "^4.3", "api-platform/doctrine-orm": "^4.3", - "api-platform/serializer": "^4.3", + "api-platform/serializer": "^4.3.12", "phpspec/prophecy-phpunit": "^2.2", "phpunit/phpunit": "^11.5 || ^12.2", "symfony/type-info": "^7.3 || ^8.0" @@ -1811,22 +1888,22 @@ "swagger" ], "support": { - "source": "https://github.com/api-platform/openapi/tree/v4.3.10" + "source": "https://github.com/api-platform/openapi/tree/v4.3.17" }, - "time": "2026-06-03T14:37:29+00:00" + "time": "2026-06-16T10:01:53+00:00" }, { "name": "api-platform/serializer", - "version": "v4.3.10", + "version": "v4.3.17", "source": { "type": "git", "url": "https://github.com/api-platform/serializer.git", - "reference": "04d56abff2910390111613716cd75796f23e34d5" + "reference": "946998cb8203e7dee5ddd716d4c0e20098d2bf69" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/serializer/zipball/04d56abff2910390111613716cd75796f23e34d5", - "reference": "04d56abff2910390111613716cd75796f23e34d5", + "url": "https://api.github.com/repos/api-platform/serializer/zipball/946998cb8203e7dee5ddd716d4c0e20098d2bf69", + "reference": "946998cb8203e7dee5ddd716d4c0e20098d2bf69", "shasum": "" }, "require": { @@ -1905,22 +1982,22 @@ "serializer" ], "support": { - "source": "https://github.com/api-platform/serializer/tree/v4.3.10" + "source": "https://github.com/api-platform/serializer/tree/v4.3.17" }, - "time": "2026-06-05T12:07:51+00:00" + "time": "2026-07-11T23:00:58+00:00" }, { "name": "api-platform/state", - "version": "v4.3.10", + "version": "v4.3.17", "source": { "type": "git", "url": "https://github.com/api-platform/state.git", - "reference": "06067bf0efae09badc618fac8add67191f12f1e3" + "reference": "2a9a18aad1c3363a32381f8c6e21c41334e23b88" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/state/zipball/06067bf0efae09badc618fac8add67191f12f1e3", - "reference": "06067bf0efae09badc618fac8add67191f12f1e3", + "url": "https://api.github.com/repos/api-platform/state/zipball/2a9a18aad1c3363a32381f8c6e21c41334e23b88", + "reference": "2a9a18aad1c3363a32381f8c6e21c41334e23b88", "shasum": "" }, "require": { @@ -1933,7 +2010,7 @@ "symfony/translation-contracts": "^3.0" }, "require-dev": { - "api-platform/serializer": "^4.3", + "api-platform/serializer": "^4.3.12", "api-platform/validator": "^4.3.1", "phpunit/phpunit": "^11.5 || ^12.2", "symfony/http-foundation": "^6.4.14 || ^7.0 || ^8.0", @@ -2002,22 +2079,22 @@ "swagger" ], "support": { - "source": "https://github.com/api-platform/state/tree/v4.3.10" + "source": "https://github.com/api-platform/state/tree/v4.3.17" }, - "time": "2026-06-05T15:16:47+00:00" + "time": "2026-07-11T07:03:31+00:00" }, { "name": "api-platform/symfony", - "version": "v4.3.10", + "version": "v4.3.17", "source": { "type": "git", "url": "https://github.com/api-platform/symfony.git", - "reference": "70a1a4468d77c9761ac1a6cba681277fa986c39b" + "reference": "55d387dc3d7384791015373030d1430c7c4d9f5e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/symfony/zipball/70a1a4468d77c9761ac1a6cba681277fa986c39b", - "reference": "70a1a4468d77c9761ac1a6cba681277fa986c39b", + "url": "https://api.github.com/repos/api-platform/symfony/zipball/55d387dc3d7384791015373030d1430c7c4d9f5e", + "reference": "55d387dc3d7384791015373030d1430c7c4d9f5e", "shasum": "" }, "require": { @@ -2028,7 +2105,7 @@ "api-platform/jsonld": "^4.3", "api-platform/metadata": "^4.3", "api-platform/openapi": "^4.3", - "api-platform/serializer": "^4.3", + "api-platform/serializer": "^4.3.12", "api-platform/state": "^4.3", "api-platform/validator": "^4.3.1", "php": ">=8.2", @@ -2131,13 +2208,13 @@ "symfony" ], "support": { - "source": "https://github.com/api-platform/symfony/tree/v4.3.10" + "source": "https://github.com/api-platform/symfony/tree/v4.3.17" }, - "time": "2026-06-05T15:17:34+00:00" + "time": "2026-07-08T06:54:33+00:00" }, { "name": "api-platform/validator", - "version": "v4.3.10", + "version": "v4.3.17", "source": { "type": "git", "url": "https://github.com/api-platform/validator.git", @@ -2207,22 +2284,22 @@ "validator" ], "support": { - "source": "https://github.com/api-platform/validator/tree/v4.3.10" + "source": "https://github.com/api-platform/validator/tree/v4.3.17" }, "time": "2026-05-07T11:45:31+00:00" }, { "name": "beberlei/assert", - "version": "v3.3.3", + "version": "v3.3.4", "source": { "type": "git", "url": "https://github.com/beberlei/assert.git", - "reference": "b5fd8eacd8915a1b627b8bfc027803f1939734dd" + "reference": "f193f4613c7d7fbcee2c05e4daff4061d49c040e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/beberlei/assert/zipball/b5fd8eacd8915a1b627b8bfc027803f1939734dd", - "reference": "b5fd8eacd8915a1b627b8bfc027803f1939734dd", + "url": "https://api.github.com/repos/beberlei/assert/zipball/f193f4613c7d7fbcee2c05e4daff4061d49c040e", + "reference": "f193f4613c7d7fbcee2c05e4daff4061d49c040e", "shasum": "" }, "require": { @@ -2274,9 +2351,9 @@ ], "support": { "issues": "https://github.com/beberlei/assert/issues", - "source": "https://github.com/beberlei/assert/tree/v3.3.3" + "source": "https://github.com/beberlei/assert/tree/v3.3.4" }, - "time": "2024-07-15T13:18:35+00:00" + "time": "2026-06-10T19:47:05+00:00" }, { "name": "beberlei/doctrineextensions", @@ -2512,16 +2589,16 @@ }, { "name": "composer/ca-bundle", - "version": "1.5.12", + "version": "1.5.13", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "00a2f4201641d5c53f7fc0195e6c8d9fcc321a78" + "reference": "c008272789979f709f7fcb32c2ecf1d2db5e84e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/00a2f4201641d5c53f7fc0195e6c8d9fcc321a78", - "reference": "00a2f4201641d5c53f7fc0195e6c8d9fcc321a78", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/c008272789979f709f7fcb32c2ecf1d2db5e84e5", + "reference": "c008272789979f709f7fcb32c2ecf1d2db5e84e5", "shasum": "" }, "require": { @@ -2568,7 +2645,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.5.12" + "source": "https://github.com/composer/ca-bundle/tree/1.5.13" }, "funding": [ { @@ -2580,7 +2657,7 @@ "type": "github" } ], - "time": "2026-05-19T11:26:22+00:00" + "time": "2026-07-18T12:35:13+00:00" }, { "name": "composer/package-versions-deprecated", @@ -2658,28 +2735,29 @@ }, { "name": "composer/pcre", - "version": "3.3.2", + "version": "3.4.0", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "url": "https://api.github.com/repos/composer/pcre/zipball/d5a341b3fb61f3001970940afb1d332968a183ed", + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed", "shasum": "" }, "require": { "php": "^7.4 || ^8.0" }, "conflict": { - "phpstan/phpstan": "<1.11.10" + "phpstan/phpstan": "<2.2.2" }, "require-dev": { - "phpstan/phpstan": "^1.12 || ^2", - "phpstan/phpstan-strict-rules": "^1 || ^2", - "phpunit/phpunit": "^8 || ^9" + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^9" }, "type": "library", "extra": { @@ -2717,7 +2795,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.3.2" + "source": "https://github.com/composer/pcre/tree/3.4.0" }, "funding": [ { @@ -2727,13 +2805,9 @@ { "url": "https://github.com/composer", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2024-11-12T16:29:46+00:00" + "time": "2026-06-07T11:47:49+00:00" }, { "name": "composer/semver", @@ -3195,16 +3269,16 @@ }, { "name": "doctrine/dbal", - "version": "4.4.3", + "version": "4.4.4", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "61e730f1658814821a85f2402c945f3883407dec" + "reference": "fb9e0ffe15e1590e24dc61c0c0a23f9a33ee42ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/61e730f1658814821a85f2402c945f3883407dec", - "reference": "61e730f1658814821a85f2402c945f3883407dec", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/fb9e0ffe15e1590e24dc61c0c0a23f9a33ee42ce", + "reference": "fb9e0ffe15e1590e24dc61c0c0a23f9a33ee42ce", "shasum": "" }, "require": { @@ -3281,7 +3355,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/4.4.3" + "source": "https://github.com/doctrine/dbal/tree/4.4.4" }, "funding": [ { @@ -3297,7 +3371,7 @@ "type": "tidelift" } ], - "time": "2026-03-20T08:52:12+00:00" + "time": "2026-07-21T14:34:40+00:00" }, { "name": "doctrine/deprecations", @@ -3349,16 +3423,16 @@ }, { "name": "doctrine/doctrine-bundle", - "version": "2.18.2", + "version": "2.19.0", "source": { "type": "git", "url": "https://github.com/doctrine/DoctrineBundle.git", - "reference": "0ff098b29b8b3c68307c8987dcaed7fd829c6546" + "reference": "07b90f707b82981097731c419f546e7ba97fba3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/0ff098b29b8b3c68307c8987dcaed7fd829c6546", - "reference": "0ff098b29b8b3c68307c8987dcaed7fd829c6546", + "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/07b90f707b82981097731c419f546e7ba97fba3c", + "reference": "07b90f707b82981097731c419f546e7ba97fba3c", "shasum": "" }, "require": { @@ -3380,7 +3454,7 @@ "doctrine/cache": "< 1.11", "doctrine/orm": "<2.17 || >=4.0", "symfony/var-exporter": "< 6.4.1 || 7.0.0", - "twig/twig": "<2.13 || >=3.0 <3.0.4" + "twig/twig": "<2.13 || >=3.0 <3.0.4 || >=5" }, "require-dev": { "doctrine/annotations": "^1 || ^2", @@ -3394,9 +3468,12 @@ "phpunit/phpunit": "^10.5.53 || ^12.3.10", "psr/log": "^1.1.4 || ^2.0 || ^3.0", "symfony/doctrine-messenger": "^6.4 || ^7.0", + "symfony/event-dispatcher": "^6.4 || ^7.0", "symfony/expression-language": "^6.4 || ^7.0", + "symfony/http-kernel": "^6.4 || ^7.0", "symfony/messenger": "^6.4 || ^7.0", "symfony/property-info": "^6.4 || ^7.0", + "symfony/runtime": "^6.4 || ^7.0", "symfony/security-bundle": "^6.4 || ^7.0", "symfony/stopwatch": "^6.4 || ^7.0", "symfony/string": "^6.4 || ^7.0", @@ -3405,7 +3482,7 @@ "symfony/var-exporter": "^6.4.1 || ^7.0.1", "symfony/web-profiler-bundle": "^6.4 || ^7.0", "symfony/yaml": "^6.4 || ^7.0", - "twig/twig": "^2.14.7 || ^3.0.4" + "twig/twig": "^2.14.7 || ^3.0.4 || ^4" }, "suggest": { "doctrine/orm": "The Doctrine ORM integration is optional in the bundle.", @@ -3450,7 +3527,7 @@ ], "support": { "issues": "https://github.com/doctrine/DoctrineBundle/issues", - "source": "https://github.com/doctrine/DoctrineBundle/tree/2.18.2" + "source": "https://github.com/doctrine/DoctrineBundle/tree/2.19.0" }, "funding": [ { @@ -3466,7 +3543,7 @@ "type": "tidelift" } ], - "time": "2025-12-20T21:35:32+00:00" + "time": "2026-07-23T14:52:05+00:00" }, { "name": "doctrine/doctrine-migrations-bundle", @@ -4223,16 +4300,16 @@ }, { "name": "dompdf/dompdf", - "version": "v3.1.5", + "version": "v3.1.6", "source": { "type": "git", "url": "https://github.com/dompdf/dompdf.git", - "reference": "f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496" + "reference": "6d4b4eb8500f7a786da8868ba463a71b725a4005" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dompdf/dompdf/zipball/f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496", - "reference": "f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496", + "url": "https://api.github.com/repos/dompdf/dompdf/zipball/6d4b4eb8500f7a786da8868ba463a71b725a4005", + "reference": "6d4b4eb8500f7a786da8868ba463a71b725a4005", "shasum": "" }, "require": { @@ -4281,9 +4358,9 @@ "homepage": "https://github.com/dompdf/dompdf", "support": { "issues": "https://github.com/dompdf/dompdf/issues", - "source": "https://github.com/dompdf/dompdf/tree/v3.1.5" + "source": "https://github.com/dompdf/dompdf/tree/v3.1.6" }, - "time": "2026-03-03T13:54:37+00:00" + "time": "2026-07-20T12:29:38+00:00" }, { "name": "dompdf/php-font-lib", @@ -4512,27 +4589,30 @@ }, { "name": "gregwar/captcha", - "version": "v1.3.0", + "version": "v2.1.0", "source": { "type": "git", "url": "https://github.com/Gregwar/Captcha.git", - "reference": "4edbcd09fde4353b94ce550f43460eba73baf2cc" + "reference": "3c2f6f0c301b94d9ec18ba769015a8a32b95765c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Gregwar/Captcha/zipball/4edbcd09fde4353b94ce550f43460eba73baf2cc", - "reference": "4edbcd09fde4353b94ce550f43460eba73baf2cc", + "url": "https://api.github.com/repos/Gregwar/Captcha/zipball/3c2f6f0c301b94d9ec18ba769015a8a32b95765c", + "reference": "3c2f6f0c301b94d9ec18ba769015a8a32b95765c", "shasum": "" }, "require": { "ext-fileinfo": "*", "ext-gd": "*", "ext-mbstring": "*", - "php": ">=5.3.0", + "php": "^8.2", "symfony/finder": "*" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6.4 || ^7.0 || ^8.0 || ^9.0" + "phpstan/phpstan": "2.1.17", + "phpstan/phpstan-phpunit": "2.0.6", + "phpunit/phpunit": "^9.6", + "squizlabs/php_codesniffer": "4.0.1" }, "type": "library", "autoload": { @@ -4564,28 +4644,28 @@ ], "support": { "issues": "https://github.com/Gregwar/Captcha/issues", - "source": "https://github.com/Gregwar/Captcha/tree/v1.3.0" + "source": "https://github.com/Gregwar/Captcha/tree/v2.1.0" }, - "time": "2025-06-23T12:25:54+00:00" + "time": "2026-06-16T13:04:08+00:00" }, { "name": "gregwar/captcha-bundle", - "version": "v2.5.0", + "version": "v3.0.0", "source": { "type": "git", "url": "https://github.com/Gregwar/CaptchaBundle.git", - "reference": "b129efda562bf8361ca6eb77043036813f749de6" + "reference": "251aa803c9df56b755d342e455e1f5c572efc2f5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Gregwar/CaptchaBundle/zipball/b129efda562bf8361ca6eb77043036813f749de6", - "reference": "b129efda562bf8361ca6eb77043036813f749de6", + "url": "https://api.github.com/repos/Gregwar/CaptchaBundle/zipball/251aa803c9df56b755d342e455e1f5c572efc2f5", + "reference": "251aa803c9df56b755d342e455e1f5c572efc2f5", "shasum": "" }, "require": { "ext-gd": "*", - "gregwar/captcha": "^1.2.1", - "php": ">=8.0.2", + "gregwar/captcha": "^2.1", + "php": ">=8.2", "symfony/form": "~6.0|~7.0|~8.0", "symfony/framework-bundle": "~6.0|~7.0|~8.0", "symfony/translation": "~6.0|~7.0|~8.0", @@ -4593,7 +4673,7 @@ }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.45", - "phpstan/phpstan": "^1.10", + "phpstan/phpstan": "^2.2.5", "symplify/easy-coding-standard": "^12" }, "type": "symfony-bundle", @@ -4631,32 +4711,32 @@ ], "support": { "issues": "https://github.com/Gregwar/CaptchaBundle/issues", - "source": "https://github.com/Gregwar/CaptchaBundle/tree/v2.5.0" + "source": "https://github.com/Gregwar/CaptchaBundle/tree/v3.0.0" }, - "time": "2026-01-08T10:51:57+00:00" + "time": "2026-07-09T10:41:14+00:00" }, { "name": "guzzlehttp/guzzle", - "version": "7.11.0", + "version": "7.15.2", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "c987f8ce84b8434fa430795eca0f3430663da72b" + "reference": "744101956d78b7c1384d0cbf379db13e859167bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/c987f8ce84b8434fa430795eca0f3430663da72b", - "reference": "c987f8ce84b8434fa430795eca0f3430663da72b", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/744101956d78b7c1384d0cbf379db13e859167bf", + "reference": "744101956d78b7c1384d0cbf379db13e859167bf", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^2.5", - "guzzlehttp/psr7": "^2.11", + "guzzlehttp/promises": "^2.5.1", + "guzzlehttp/psr7": "^2.13", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.5 || ^3.0", - "symfony/polyfill-php80": "^1.24" + "symfony/polyfill-php80": "^1.25" }, "provide": { "psr/http-client-implementation": "1.0" @@ -4664,8 +4744,8 @@ "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", - "guzzle/client-integration-tests": "3.0.2", - "guzzlehttp/test-server": "^0.4", + "guzzle/client-integration-tests": "3.0.3", + "guzzlehttp/test-server": "^0.7", "php-http/message-factory": "^1.1", "phpunit/phpunit": "^8.5.52 || ^9.6.34", "psr/log": "^1.1 || ^2.0 || ^3.0" @@ -4745,7 +4825,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.11.0" + "source": "https://github.com/guzzle/guzzle/tree/7.15.2" }, "funding": [ { @@ -4761,20 +4841,20 @@ "type": "tidelift" } ], - "time": "2026-06-02T12:40:51+00:00" + "time": "2026-07-26T23:23:20+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.5.0", + "version": "2.5.1", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "4360e982f87f5f258bf872d094647791db2f4c8e" + "reference": "9ad1e4fc607446a055b95870c7f668e93b5cff29" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/4360e982f87f5f258bf872d094647791db2f4c8e", - "reference": "4360e982f87f5f258bf872d094647791db2f4c8e", + "url": "https://api.github.com/repos/guzzle/promises/zipball/9ad1e4fc607446a055b95870c7f668e93b5cff29", + "reference": "9ad1e4fc607446a055b95870c7f668e93b5cff29", "shasum": "" }, "require": { @@ -4829,7 +4909,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.5.0" + "source": "https://github.com/guzzle/promises/tree/2.5.1" }, "funding": [ { @@ -4845,20 +4925,20 @@ "type": "tidelift" } ], - "time": "2026-06-02T12:23:43+00:00" + "time": "2026-07-08T15:48:39+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.11.0", + "version": "2.13.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "bbb5e61349fa5cb822b3e87842b951088b76b81f" + "reference": "dad89620b7a6edb60c15858442eb2e408b45d8f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/bbb5e61349fa5cb822b3e87842b951088b76b81f", - "reference": "bbb5e61349fa5cb822b3e87842b951088b76b81f", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/dad89620b7a6edb60c15858442eb2e408b45d8f4", + "reference": "dad89620b7a6edb60c15858442eb2e408b45d8f4", "shasum": "" }, "require": { @@ -4867,7 +4947,7 @@ "psr/http-message": "^1.1 || ^2.0", "ralouphie/getallheaders": "^3.0", "symfony/deprecation-contracts": "^2.5 || ^3.0", - "symfony/polyfill-php80": "^1.24" + "symfony/polyfill-php80": "^1.25" }, "provide": { "psr/http-factory-implementation": "1.0", @@ -4948,7 +5028,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.11.0" + "source": "https://github.com/guzzle/psr7/tree/2.13.0" }, "funding": [ { @@ -4964,7 +5044,7 @@ "type": "tidelift" } ], - "time": "2026-06-02T12:30:48+00:00" + "time": "2026-07-16T22:23:49+00:00" }, { "name": "hshn/base64-encoded-file", @@ -5364,16 +5444,16 @@ }, { "name": "jfcherng/php-diff", - "version": "6.16.2", + "version": "6.16.3", "source": { "type": "git", "url": "https://github.com/jfcherng/php-diff.git", - "reference": "7f46bcfc582e81769237d0b3f6b8a548efe8799d" + "reference": "6d7332b6080cdd50011a364b58f24c8d0cdeb5da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jfcherng/php-diff/zipball/7f46bcfc582e81769237d0b3f6b8a548efe8799d", - "reference": "7f46bcfc582e81769237d0b3f6b8a548efe8799d", + "url": "https://api.github.com/repos/jfcherng/php-diff/zipball/6d7332b6080cdd50011a364b58f24c8d0cdeb5da", + "reference": "6d7332b6080cdd50011a364b58f24c8d0cdeb5da", "shasum": "" }, "require": { @@ -5418,7 +5498,7 @@ ], "support": { "issues": "https://github.com/jfcherng/php-diff/issues", - "source": "https://github.com/jfcherng/php-diff/tree/6.16.2" + "source": "https://github.com/jfcherng/php-diff/tree/6.16.3" }, "funding": [ { @@ -5426,7 +5506,7 @@ "type": "custom" } ], - "time": "2024-03-10T17:40:29+00:00" + "time": "2026-06-22T13:08:56+00:00" }, { "name": "jfcherng/php-mb-string", @@ -5965,16 +6045,16 @@ }, { "name": "league/commonmark", - "version": "2.8.2", + "version": "2.8.3", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "59fb075d2101740c337c7216e3f32b36c204218b" + "reference": "1902f60f984235023acbe03db6ad614a37b3c3e7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/59fb075d2101740c337c7216e3f32b36c204218b", - "reference": "59fb075d2101740c337c7216e3f32b36c204218b", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/1902f60f984235023acbe03db6ad614a37b3c3e7", + "reference": "1902f60f984235023acbe03db6ad614a37b3c3e7", "shasum": "" }, "require": { @@ -5996,8 +6076,8 @@ "github/gfm": "0.29.0", "michelf/php-markdown": "^1.4 || ^2.0", "nyholm/psr7": "^1.5", - "phpstan/phpstan": "^1.8.2", - "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", + "phpstan/phpstan": "^2.0.0", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0 || ^12.0.0 || ^13.0.0", "scrutinizer/ocular": "^1.8.1", "symfony/finder": "^5.3 | ^6.0 | ^7.0 || ^8.0", "symfony/process": "^5.4 | ^6.0 | ^7.0 || ^8.0", @@ -6068,7 +6148,7 @@ "type": "tidelift" } ], - "time": "2026-03-19T13:16:38+00:00" + "time": "2026-07-12T15:29:16+00:00" }, { "name": "league/config", @@ -7008,16 +7088,16 @@ }, { "name": "masterminds/html5", - "version": "2.10.0", + "version": "2.10.1", "source": { "type": "git", "url": "https://github.com/Masterminds/html5-php.git", - "reference": "fcf91eb64359852f00d921887b219479b4f21251" + "reference": "fd5018f6815fff903946d0564977b44ce8010e29" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fcf91eb64359852f00d921887b219479b4f21251", - "reference": "fcf91eb64359852f00d921887b219479b4f21251", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fd5018f6815fff903946d0564977b44ce8010e29", + "reference": "fd5018f6815fff903946d0564977b44ce8010e29", "shasum": "" }, "require": { @@ -7025,7 +7105,7 @@ "php": ">=5.3.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9" + "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9 || ^10" }, "type": "library", "extra": { @@ -7069,9 +7149,93 @@ ], "support": { "issues": "https://github.com/Masterminds/html5-php/issues", - "source": "https://github.com/Masterminds/html5-php/tree/2.10.0" + "source": "https://github.com/Masterminds/html5-php/tree/2.10.1" }, - "time": "2025-07-25T09:04:22+00:00" + "time": "2026-06-23T18:43:15+00:00" + }, + { + "name": "mcp/sdk", + "version": "v0.7.0", + "source": { + "type": "git", + "url": "https://github.com/modelcontextprotocol/php-sdk.git", + "reference": "a6f415578fa789783d010274d11c922e97659a8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/modelcontextprotocol/php-sdk/zipball/a6f415578fa789783d010274d11c922e97659a8a", + "reference": "a6f415578fa789783d010274d11c922e97659a8a", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "opis/json-schema": "^2.4", + "php": "^8.1", + "php-http/discovery": "^1.20", + "phpdocumentor/reflection-docblock": "^5.6 || ^6.0", + "psr/clock": "^1.0", + "psr/container": "^1.0 || ^2.0", + "psr/event-dispatcher": "^1.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.1", + "psr/http-message": "^1.1 || ^2.0", + "psr/http-server-handler": "^1.0", + "psr/http-server-middleware": "^1.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "symfony/uid": "^5.4 || ^6.4 || ^7.3 || ^8.0" + }, + "require-dev": { + "composer/semver": "^3.0", + "ext-openssl": "*", + "firebase/php-jwt": "^6.10 || ^7.0", + "laminas/laminas-httphandlerrunner": "^2.12", + "nyholm/psr7": "^1.8", + "nyholm/psr7-server": "^1.1", + "phar-io/composer-distributor": "^1.0.2", + "php-cs-fixer/shim": "^3.91", + "phpdocumentor/shim": "^3", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^10.5", + "psr/simple-cache": "^2.0 || ^3.0", + "symfony/cache": "^5.4 || ^6.4 || ^7.3 || ^8.0", + "symfony/console": "^5.4 || ^6.4 || ^7.3 || ^8.0", + "symfony/finder": "^5.4 || ^6.4 || ^7.3 || ^8.0", + "symfony/http-client": "^5.4 || ^6.4 || ^7.3 || ^8.0", + "symfony/process": "^5.4 || ^6.4 || ^7.3 || ^8.0" + }, + "suggest": { + "symfony/finder": "Required for file-based discovery." + }, + "type": "library", + "autoload": { + "psr-4": { + "Mcp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Christopher Hertel", + "email": "mail@christopher-hertel.de" + }, + { + "name": "Kyrian Obikwelu", + "email": "koshnawaza@gmail.com" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + } + ], + "description": "Model Context Protocol SDK for Client and Server applications in PHP", + "support": { + "issues": "https://github.com/modelcontextprotocol/php-sdk/issues", + "source": "https://github.com/modelcontextprotocol/php-sdk/tree/v0.7.0" + }, + "time": "2026-07-14T22:58:00+00:00" }, { "name": "mf2/mf2", @@ -7747,16 +7911,16 @@ }, { "name": "nette/utils", - "version": "v4.1.4", + "version": "v4.1.5", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7" + "reference": "b043439dbdf954e6c28b5ea7e34b0100f83165e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/7da6c396d7ebe142bc857c20479d5e70a5e1aac7", - "reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7", + "url": "https://api.github.com/repos/nette/utils/zipball/b043439dbdf954e6c28b5ea7e34b0100f83165e0", + "reference": "b043439dbdf954e6c28b5ea7e34b0100f83165e0", "shasum": "" }, "require": { @@ -7776,7 +7940,7 @@ }, "suggest": { "ext-gd": "to use Image", - "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-iconv": "to use Strings::chr(), ord() and reverse()", "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", "ext-json": "to use Nette\\Utils\\Json", "ext-mbstring": "to use Strings::lower() etc...", @@ -7832,9 +7996,9 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v4.1.4" + "source": "https://github.com/nette/utils/tree/v4.1.5" }, - "time": "2026-05-11T20:49:54+00:00" + "time": "2026-07-17T23:02:45+00:00" }, { "name": "nikolaposa/version", @@ -8160,6 +8324,196 @@ ], "time": "2026-05-07T22:38:04+00:00" }, + { + "name": "opis/json-schema", + "version": "2.6.0", + "source": { + "type": "git", + "url": "https://github.com/opis/json-schema.git", + "reference": "8458763e0dd0b6baa310e04f1829fc73da4e8c8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opis/json-schema/zipball/8458763e0dd0b6baa310e04f1829fc73da4e8c8a", + "reference": "8458763e0dd0b6baa310e04f1829fc73da4e8c8a", + "shasum": "" + }, + "require": { + "ext-json": "*", + "opis/string": "^2.1", + "opis/uri": "^1.0", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "ext-bcmath": "*", + "ext-intl": "*", + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Opis\\JsonSchema\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Sorin Sarca", + "email": "sarca_sorin@hotmail.com" + }, + { + "name": "Marius Sarca", + "email": "marius.sarca@gmail.com" + } + ], + "description": "Json Schema Validator for PHP", + "homepage": "https://opis.io/json-schema", + "keywords": [ + "json", + "json-schema", + "schema", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/opis/json-schema/issues", + "source": "https://github.com/opis/json-schema/tree/2.6.0" + }, + "time": "2025-10-17T12:46:48+00:00" + }, + { + "name": "opis/string", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/opis/string.git", + "reference": "3e4d2aaff518ac518530b89bb26ed40f4503635e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opis/string/zipball/3e4d2aaff518ac518530b89bb26ed40f4503635e", + "reference": "3e4d2aaff518ac518530b89bb26ed40f4503635e", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "ext-json": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Opis\\String\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Marius Sarca", + "email": "marius.sarca@gmail.com" + }, + { + "name": "Sorin Sarca", + "email": "sarca_sorin@hotmail.com" + } + ], + "description": "Multibyte strings as objects", + "homepage": "https://opis.io/string", + "keywords": [ + "multi-byte", + "opis", + "string", + "string manipulation", + "utf-8" + ], + "support": { + "issues": "https://github.com/opis/string/issues", + "source": "https://github.com/opis/string/tree/2.1.0" + }, + "time": "2025-10-17T12:38:41+00:00" + }, + { + "name": "opis/uri", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/opis/uri.git", + "reference": "0f3ca49ab1a5e4a6681c286e0b2cc081b93a7d5a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opis/uri/zipball/0f3ca49ab1a5e4a6681c286e0b2cc081b93a7d5a", + "reference": "0f3ca49ab1a5e4a6681c286e0b2cc081b93a7d5a", + "shasum": "" + }, + "require": { + "opis/string": "^2.0", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Opis\\Uri\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Marius Sarca", + "email": "marius.sarca@gmail.com" + }, + { + "name": "Sorin Sarca", + "email": "sarca_sorin@hotmail.com" + } + ], + "description": "Build, parse and validate URIs and URI-templates", + "homepage": "https://opis.io", + "keywords": [ + "URI Template", + "parse url", + "punycode", + "uri", + "uri components", + "url", + "validate uri" + ], + "support": { + "issues": "https://github.com/opis/uri/issues", + "source": "https://github.com/opis/uri/tree/1.1.0" + }, + "time": "2021-05-22T15:57:08+00:00" + }, { "name": "oskarstark/enum-helper", "version": "1.8.4", @@ -8960,16 +9314,16 @@ }, { "name": "phpoffice/phpspreadsheet", - "version": "5.8.0", + "version": "5.9.0", "source": { "type": "git", "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", - "reference": "01964d92536edf1a3a874b9580a52824bebf6fbb" + "reference": "05e99ebf61238a70227b4d9cc02d0030d34f6339" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/01964d92536edf1a3a874b9580a52824bebf6fbb", - "reference": "01964d92536edf1a3a874b9580a52824bebf6fbb", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/05e99ebf61238a70227b4d9cc02d0030d34f6339", + "reference": "05e99ebf61238a70227b4d9cc02d0030d34f6339", "shasum": "" }, "require": { @@ -8991,7 +9345,7 @@ "maennchen/zipstream-php": "^2.1 || ^3.0", "markbaker/complex": "^3.0", "markbaker/matrix": "^3.0", - "php": "^8.1", + "php": "^8.2", "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" }, "require-dev": { @@ -9005,7 +9359,7 @@ "phpstan/phpstan": "^1.1 || ^2.0", "phpstan/phpstan-deprecation-rules": "^1.0 || ^2.0", "phpstan/phpstan-phpunit": "^1.0 || ^2.0", - "phpunit/phpunit": "^10.5", + "phpunit/phpunit": "^10.5 || ^11.0", "squizlabs/php_codesniffer": "^3.7", "tecnickcom/tcpdf": "^6.5" }, @@ -9063,22 +9417,22 @@ ], "support": { "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", - "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/5.8.0" + "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/5.9.0" }, - "time": "2026-06-07T03:51:10+00:00" + "time": "2026-07-12T19:17:39+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "2.3.2", + "version": "2.3.3", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a" + "reference": "fb19eedd2bb67ff8cf7a5502ad329e701d6398a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a004701b11273a26cd7955a61d67a7f1e525a45a", - "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fb19eedd2bb67ff8cf7a5502ad329e701d6398a3", + "reference": "fb19eedd2bb67ff8cf7a5502ad329e701d6398a3", "shasum": "" }, "require": { @@ -9110,9 +9464,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.2" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.3" }, - "time": "2026-01-25T14:56:51+00:00" + "time": "2026-07-08T07:01:06+00:00" }, { "name": "psr/cache", @@ -9474,6 +9828,119 @@ }, "time": "2023-04-04T09:50:52+00:00" }, + { + "name": "psr/http-server-handler", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-handler.git", + "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/84c4fb66179be4caaf8e97bd239203245302e7d4", + "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side request handler", + "keywords": [ + "handler", + "http", + "http-interop", + "psr", + "psr-15", + "psr-7", + "request", + "response", + "server" + ], + "support": { + "source": "https://github.com/php-fig/http-server-handler/tree/1.0.2" + }, + "time": "2023-04-10T20:06:20+00:00" + }, + { + "name": "psr/http-server-middleware", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-middleware.git", + "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/c1481f747daaa6a0782775cd6a8c26a1bf4a3829", + "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0 || ^2.0", + "psr/http-server-handler": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side middleware", + "keywords": [ + "http", + "http-interop", + "middleware", + "psr", + "psr-15", + "psr-7", + "request", + "response" + ], + "support": { + "issues": "https://github.com/php-fig/http-server-middleware/issues", + "source": "https://github.com/php-fig/http-server-middleware/tree/1.0.2" + }, + "time": "2023-04-11T06:14:47+00:00" + }, { "name": "psr/link", "version": "2.0.1", @@ -9999,16 +10466,16 @@ }, { "name": "sabberworm/php-css-parser", - "version": "v9.3.0", + "version": "v9.4.0", "source": { "type": "git", "url": "https://github.com/MyIntervals/PHP-CSS-Parser.git", - "reference": "88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949" + "reference": "fd3bf9fb173e0df649bc4e3e0d088a1b2417c08f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949", - "reference": "88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949", + "url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/fd3bf9fb173e0df649bc4e3e0d088a1b2417c08f", + "reference": "fd3bf9fb173e0df649bc4e3e0d088a1b2417c08f", "shasum": "" }, "require": { @@ -10019,15 +10486,15 @@ "require-dev": { "php-parallel-lint/php-parallel-lint": "1.4.0", "phpstan/extension-installer": "1.4.3", - "phpstan/phpstan": "1.12.32 || 2.1.32", - "phpstan/phpstan-phpunit": "1.4.2 || 2.0.8", - "phpstan/phpstan-strict-rules": "1.6.2 || 2.0.7", + "phpstan/phpstan": "1.12.33 || 2.2.2", + "phpstan/phpstan-phpunit": "1.4.2 || 2.0.16", + "phpstan/phpstan-strict-rules": "1.6.2 || 2.0.11", "phpunit/phpunit": "8.5.52", "rawr/phpunit-data-provider": "3.3.1", - "rector/rector": "1.2.10 || 2.2.8", - "rector/type-perfect": "1.0.0 || 2.1.0", + "rector/rector": "1.2.10 || 2.4.6", + "rector/type-perfect": "1.0.0 || 2.1.3", "squizlabs/php_codesniffer": "4.0.1", - "thecodingmachine/phpstan-safe-rule": "1.2.0 || 1.4.1" + "thecodingmachine/phpstan-safe-rule": "1.2.0 || 1.4.3" }, "suggest": { "ext-mbstring": "for parsing UTF-8 CSS" @@ -10035,7 +10502,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "9.4.x-dev" + "dev-main": "9.5.x-dev" } }, "autoload": { @@ -10073,9 +10540,9 @@ ], "support": { "issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues", - "source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v9.3.0" + "source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v9.4.0" }, - "time": "2026-03-03T17:31:43+00:00" + "time": "2026-06-18T15:10:53+00:00" }, { "name": "sabre/uri", @@ -10140,16 +10607,16 @@ }, { "name": "scheb/2fa-backup-code", - "version": "v7.13.1", + "version": "v7.14.0", "source": { "type": "git", "url": "https://github.com/scheb/2fa-backup-code.git", - "reference": "35f1ace4be7be2c10158d2bb8284208499111db8" + "reference": "73946182322f43cf82c5ee8c5481481a570ade40" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/scheb/2fa-backup-code/zipball/35f1ace4be7be2c10158d2bb8284208499111db8", - "reference": "35f1ace4be7be2c10158d2bb8284208499111db8", + "url": "https://api.github.com/repos/scheb/2fa-backup-code/zipball/73946182322f43cf82c5ee8c5481481a570ade40", + "reference": "73946182322f43cf82c5ee8c5481481a570ade40", "shasum": "" }, "require": { @@ -10183,22 +10650,22 @@ "two-step" ], "support": { - "source": "https://github.com/scheb/2fa-backup-code/tree/v7.13.1" + "source": "https://github.com/scheb/2fa-backup-code/tree/v7.14.0" }, - "time": "2025-11-20T13:35:24+00:00" + "time": "2026-01-24T13:47:32+00:00" }, { "name": "scheb/2fa-bundle", - "version": "v7.13.1", + "version": "v7.14.0", "source": { "type": "git", "url": "https://github.com/scheb/2fa-bundle.git", - "reference": "edcc14456b508aab37ec792cfc36793d04226784" + "reference": "65fa9eb61d1205f2024f14c8c38b53e7bb20927c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/scheb/2fa-bundle/zipball/edcc14456b508aab37ec792cfc36793d04226784", - "reference": "edcc14456b508aab37ec792cfc36793d04226784", + "url": "https://api.github.com/repos/scheb/2fa-bundle/zipball/65fa9eb61d1205f2024f14c8c38b53e7bb20927c", + "reference": "65fa9eb61d1205f2024f14c8c38b53e7bb20927c", "shasum": "" }, "require": { @@ -10251,22 +10718,22 @@ "two-step" ], "support": { - "source": "https://github.com/scheb/2fa-bundle/tree/v7.13.1" + "source": "https://github.com/scheb/2fa-bundle/tree/v7.14.0" }, - "time": "2025-12-18T15:29:07+00:00" + "time": "2026-06-12T18:31:07+00:00" }, { "name": "scheb/2fa-google-authenticator", - "version": "v7.13.1", + "version": "v7.14.0", "source": { "type": "git", "url": "https://github.com/scheb/2fa-google-authenticator.git", - "reference": "7ad34bbde343a0770571464127ee072aacb70a58" + "reference": "23b0bda5b924b8752047c4994f10e1c9e1532179" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/scheb/2fa-google-authenticator/zipball/7ad34bbde343a0770571464127ee072aacb70a58", - "reference": "7ad34bbde343a0770571464127ee072aacb70a58", + "url": "https://api.github.com/repos/scheb/2fa-google-authenticator/zipball/23b0bda5b924b8752047c4994f10e1c9e1532179", + "reference": "23b0bda5b924b8752047c4994f10e1c9e1532179", "shasum": "" }, "require": { @@ -10304,22 +10771,22 @@ "two-step" ], "support": { - "source": "https://github.com/scheb/2fa-google-authenticator/tree/v7.13.1" + "source": "https://github.com/scheb/2fa-google-authenticator/tree/v7.14.0" }, - "time": "2025-12-04T15:55:14+00:00" + "time": "2026-01-24T13:53:55+00:00" }, { "name": "scheb/2fa-trusted-device", - "version": "v7.13.1", + "version": "v7.14.0", "source": { "type": "git", "url": "https://github.com/scheb/2fa-trusted-device.git", - "reference": "ae3a5819faccbf151af078f432e4e6c97bb44ebf" + "reference": "590f400bac58bff70af24adbc702e57a30e493a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/scheb/2fa-trusted-device/zipball/ae3a5819faccbf151af078f432e4e6c97bb44ebf", - "reference": "ae3a5819faccbf151af078f432e4e6c97bb44ebf", + "url": "https://api.github.com/repos/scheb/2fa-trusted-device/zipball/590f400bac58bff70af24adbc702e57a30e493a3", + "reference": "590f400bac58bff70af24adbc702e57a30e493a3", "shasum": "" }, "require": { @@ -10355,9 +10822,9 @@ "two-step" ], "support": { - "source": "https://github.com/scheb/2fa-trusted-device/tree/v7.13.1" + "source": "https://github.com/scheb/2fa-trusted-device/tree/v7.14.0" }, - "time": "2025-12-01T15:40:59+00:00" + "time": "2026-01-24T13:47:32+00:00" }, { "name": "shivas/versioning-bundle", @@ -10484,20 +10951,20 @@ }, { "name": "spomky-labs/cbor-php", - "version": "3.2.3", + "version": "3.3.0", "source": { "type": "git", "url": "https://github.com/Spomky-Labs/cbor-php.git", - "reference": "dd6eb84e6d92f7b8bd0da56b4b4dd7235aed0c32" + "reference": "013d13da69cf28b1ae501887daceccc850ca1c76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Spomky-Labs/cbor-php/zipball/dd6eb84e6d92f7b8bd0da56b4b4dd7235aed0c32", - "reference": "dd6eb84e6d92f7b8bd0da56b4b4dd7235aed0c32", + "url": "https://api.github.com/repos/Spomky-Labs/cbor-php/zipball/013d13da69cf28b1ae501887daceccc850ca1c76", + "reference": "013d13da69cf28b1ae501887daceccc850ca1c76", "shasum": "" }, "require": { - "brick/math": "^0.9|^0.10|^0.11|^0.12|^0.13|^0.14|^0.15|^0.16|^0.17", + "brick/math": "^0.9|^0.10|^0.11|^0.12|^0.13|^0.14|^0.15|^0.16|^0.17|^0.18", "ext-mbstring": "*", "php": ">=8.0" }, @@ -10539,7 +11006,7 @@ ], "support": { "issues": "https://github.com/Spomky-Labs/cbor-php/issues", - "source": "https://github.com/Spomky-Labs/cbor-php/tree/3.2.3" + "source": "https://github.com/Spomky-Labs/cbor-php/tree/3.3.0" }, "funding": [ { @@ -10551,7 +11018,7 @@ "type": "patreon" } ], - "time": "2026-04-01T12:15:20+00:00" + "time": "2026-07-15T18:56:27+00:00" }, { "name": "spomky-labs/otphp", @@ -10625,41 +11092,40 @@ }, { "name": "spomky-labs/pki-framework", - "version": "1.4.2", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/Spomky-Labs/pki-framework.git", - "reference": "aa576cbd07128075bef97ac2f8af9854e67513d8" + "reference": "e0d61661962560c1cedfef02b51b431e720aae78" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Spomky-Labs/pki-framework/zipball/aa576cbd07128075bef97ac2f8af9854e67513d8", - "reference": "aa576cbd07128075bef97ac2f8af9854e67513d8", + "url": "https://api.github.com/repos/Spomky-Labs/pki-framework/zipball/e0d61661962560c1cedfef02b51b431e720aae78", + "reference": "e0d61661962560c1cedfef02b51b431e720aae78", "shasum": "" }, "require": { - "brick/math": "^0.10|^0.11|^0.12|^0.13|^0.14|^0.15|^0.16|^0.17", + "brick/math": "^0.10|^0.11|^0.12|^0.13|^0.14|^0.15|^0.16|^0.17|^0.18", "ext-mbstring": "*", - "php": ">=8.1", - "psr/clock": "^1.0" + "php": ">=8.1" }, "require-dev": { "ekino/phpstan-banned-code": "^1.0|^2.0|^3.0", "ext-gmp": "*", "ext-openssl": "*", - "infection/infection": "^0.28|^0.29|^0.31|^0.32", + "infection/infection": "^0.28|^0.29|^0.31", "php-parallel-lint/php-parallel-lint": "^1.3", "phpstan/extension-installer": "^1.3|^2.0", "phpstan/phpstan": "^1.8|^2.0", "phpstan/phpstan-deprecation-rules": "^1.0|^2.0", "phpstan/phpstan-phpunit": "^1.1|^2.0", "phpstan/phpstan-strict-rules": "^1.3|^2.0", - "phpunit/phpunit": "^10.1|^11.0|^12.0|^13.0", + "phpunit/phpunit": "^10.1|^11.0|^12.0", "rector/rector": "^1.0|^2.0", "roave/security-advisories": "dev-latest", "symfony/string": "^6.4|^7.0|^8.0", "symfony/var-dumper": "^6.4|^7.0|^8.0", - "symplify/easy-coding-standard": "^12.0|^13.0" + "symplify/easy-coding-standard": "^12.0 || ^13.0" }, "suggest": { "ext-bcmath": "For better performance (or GMP)", @@ -10719,7 +11185,7 @@ ], "support": { "issues": "https://github.com/Spomky-Labs/pki-framework/issues", - "source": "https://github.com/Spomky-Labs/pki-framework/tree/1.4.2" + "source": "https://github.com/Spomky-Labs/pki-framework/tree/1.5.0" }, "funding": [ { @@ -10731,25 +11197,25 @@ "type": "patreon" } ], - "time": "2026-03-23T22:56:56+00:00" + "time": "2026-07-16T10:28:45+00:00" }, { "name": "symfony/ai-bundle", - "version": "v0.9.0", + "version": "v0.12.0", "source": { "type": "git", "url": "https://github.com/symfony/ai-bundle.git", - "reference": "77fd1b513174770acf49abd68effa995fa518f7c" + "reference": "df2a6e8c3cf79a93dc8d9d9efd516ea981973420" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/ai-bundle/zipball/77fd1b513174770acf49abd68effa995fa518f7c", - "reference": "77fd1b513174770acf49abd68effa995fa518f7c", + "url": "https://api.github.com/repos/symfony/ai-bundle/zipball/df2a6e8c3cf79a93dc8d9d9efd516ea981973420", + "reference": "df2a6e8c3cf79a93dc8d9d9efd516ea981973420", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/ai-platform": "^0.9", + "symfony/ai-platform": "^0.12", "symfony/clock": "^7.3|^8.0", "symfony/config": "^7.3|^8.0", "symfony/console": "^7.3|^8.0", @@ -10764,74 +11230,76 @@ "phpstan/phpstan-phpunit": "^2.0", "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpunit": "^11.5.53", - "symfony/ai-agent": "^0.9", - "symfony/ai-ai-ml-api-platform": "^0.9", - "symfony/ai-albert-platform": "^0.9", - "symfony/ai-amazee-ai-platform": "^0.9", - "symfony/ai-anthropic-platform": "^0.9", - "symfony/ai-azure-platform": "^0.9", - "symfony/ai-azure-search-store": "^0.9", - "symfony/ai-bedrock-platform": "^0.9", - "symfony/ai-cache-message-store": "^0.9", - "symfony/ai-cache-platform": "^0.9", - "symfony/ai-cache-store": "^0.9", - "symfony/ai-cartesia-platform": "^0.9", - "symfony/ai-cerebras-platform": "^0.9", - "symfony/ai-chat": "^0.9", - "symfony/ai-chroma-db-store": "^0.9", - "symfony/ai-click-house-store": "^0.9", - "symfony/ai-cloudflare-message-store": "^0.9", - "symfony/ai-cloudflare-store": "^0.9", - "symfony/ai-cohere-platform": "^0.9", - "symfony/ai-decart-platform": "^0.9", - "symfony/ai-deep-seek-platform": "^0.9", - "symfony/ai-docker-model-runner-platform": "^0.9", - "symfony/ai-doctrine-message-store": "^0.9", - "symfony/ai-elasticsearch-store": "^0.9", - "symfony/ai-eleven-labs-platform": "^0.9", - "symfony/ai-failover-platform": "^0.9", - "symfony/ai-gemini-platform": "^0.9", - "symfony/ai-generic-platform": "^0.9", - "symfony/ai-hugging-face-platform": "^0.9", - "symfony/ai-lm-studio-platform": "^0.9", - "symfony/ai-manticore-search-store": "^0.9", - "symfony/ai-maria-db-store": "^0.9", - "symfony/ai-meilisearch-message-store": "^0.9", - "symfony/ai-meilisearch-store": "^0.9", - "symfony/ai-meta-platform": "^0.9", - "symfony/ai-milvus-store": "^0.9", - "symfony/ai-mistral-platform": "^0.9", - "symfony/ai-mongo-db-message-store": "^0.9", - "symfony/ai-mongo-db-store": "^0.9", - "symfony/ai-neo4j-store": "^0.9", - "symfony/ai-ollama-platform": "^0.9", - "symfony/ai-open-ai-platform": "^0.9", - "symfony/ai-open-responses-platform": "^0.9", - "symfony/ai-open-router-platform": "^0.9", - "symfony/ai-open-search-store": "^0.9", - "symfony/ai-ovh-platform": "^0.9", - "symfony/ai-perplexity-platform": "^0.9", - "symfony/ai-pinecone-store": "^0.9", - "symfony/ai-pogocache-message-store": "^0.9", - "symfony/ai-postgres-store": "^0.9", - "symfony/ai-qdrant-store": "^0.9", - "symfony/ai-redis-message-store": "^0.9", - "symfony/ai-redis-store": "^0.9", - "symfony/ai-replicate-platform": "^0.9", - "symfony/ai-s3vectors-store": "^0.9", - "symfony/ai-scaleway-platform": "^0.9", - "symfony/ai-session-message-store": "^0.9", - "symfony/ai-sqlite-store": "^0.9", - "symfony/ai-store": "^0.9", - "symfony/ai-supabase-store": "^0.9", - "symfony/ai-surreal-db-message-store": "^0.9", - "symfony/ai-surreal-db-store": "^0.9", - "symfony/ai-transformers-php-platform": "^0.9", - "symfony/ai-typesense-store": "^0.9", - "symfony/ai-vektor-store": "^0.9", - "symfony/ai-vertex-ai-platform": "^0.9", - "symfony/ai-voyage-platform": "^0.9", - "symfony/ai-weaviate-store": "^0.9", + "symfony/ai-agent": "^0.12", + "symfony/ai-ai-ml-api-platform": "^0.12", + "symfony/ai-albert-platform": "^0.12", + "symfony/ai-amazee-ai-platform": "^0.12", + "symfony/ai-anthropic-platform": "^0.12", + "symfony/ai-azure-platform": "^0.12", + "symfony/ai-azure-search-store": "^0.12", + "symfony/ai-bedrock-platform": "^0.12", + "symfony/ai-cache-message-store": "^0.12", + "symfony/ai-cache-platform": "^0.12", + "symfony/ai-cache-store": "^0.12", + "symfony/ai-cartesia-platform": "^0.12", + "symfony/ai-cerebras-platform": "^0.12", + "symfony/ai-chat": "^0.12", + "symfony/ai-chroma-db-store": "^0.12", + "symfony/ai-click-house-store": "^0.12", + "symfony/ai-cloudflare-message-store": "^0.12", + "symfony/ai-cloudflare-store": "^0.12", + "symfony/ai-cohere-platform": "^0.12", + "symfony/ai-decart-platform": "^0.12", + "symfony/ai-deep-seek-platform": "^0.12", + "symfony/ai-deepgram-platform": "^0.12", + "symfony/ai-docker-model-runner-platform": "^0.12", + "symfony/ai-doctrine-message-store": "^0.12", + "symfony/ai-elasticsearch-store": "^0.12", + "symfony/ai-eleven-labs-platform": "^0.12", + "symfony/ai-failover-platform": "^0.12", + "symfony/ai-gemini-platform": "^0.12", + "symfony/ai-generic-platform": "^0.12", + "symfony/ai-hugging-face-platform": "^0.12", + "symfony/ai-lm-studio-platform": "^0.12", + "symfony/ai-manticore-search-store": "^0.12", + "symfony/ai-maria-db-store": "^0.12", + "symfony/ai-meilisearch-message-store": "^0.12", + "symfony/ai-meilisearch-store": "^0.12", + "symfony/ai-meta-platform": "^0.12", + "symfony/ai-milvus-store": "^0.12", + "symfony/ai-mini-max-platform": "^0.12", + "symfony/ai-mistral-platform": "^0.12", + "symfony/ai-mongo-db-message-store": "^0.12", + "symfony/ai-mongo-db-store": "^0.12", + "symfony/ai-neo4j-store": "^0.12", + "symfony/ai-ollama-platform": "^0.12", + "symfony/ai-open-ai-platform": "^0.12", + "symfony/ai-open-responses-platform": "^0.12", + "symfony/ai-open-router-platform": "^0.12", + "symfony/ai-open-search-store": "^0.12", + "symfony/ai-ovh-platform": "^0.12", + "symfony/ai-perplexity-platform": "^0.12", + "symfony/ai-pinecone-store": "^0.12", + "symfony/ai-pogocache-message-store": "^0.12", + "symfony/ai-postgres-store": "^0.12", + "symfony/ai-qdrant-store": "^0.12", + "symfony/ai-redis-message-store": "^0.12", + "symfony/ai-redis-store": "^0.12", + "symfony/ai-replicate-platform": "^0.12", + "symfony/ai-s3vectors-store": "^0.12", + "symfony/ai-scaleway-platform": "^0.12", + "symfony/ai-session-message-store": "^0.12", + "symfony/ai-sqlite-store": "^0.12", + "symfony/ai-store": "^0.12", + "symfony/ai-supabase-store": "^0.12", + "symfony/ai-surreal-db-message-store": "^0.12", + "symfony/ai-surreal-db-store": "^0.12", + "symfony/ai-transformers-php-platform": "^0.12", + "symfony/ai-typesense-store": "^0.12", + "symfony/ai-vektor-store": "^0.12", + "symfony/ai-vertex-ai-platform": "^0.12", + "symfony/ai-voyage-platform": "^0.12", + "symfony/ai-weaviate-store": "^0.12", "symfony/expression-language": "^7.3|^8.0", "symfony/security-core": "^7.3|^8.0", "symfony/translation": "^7.3|^8.0", @@ -10869,7 +11337,7 @@ ], "description": "Integration bundle for Symfony AI components", "support": { - "source": "https://github.com/symfony/ai-bundle/tree/v0.9.0" + "source": "https://github.com/symfony/ai-bundle/tree/v0.12.0" }, "funding": [ { @@ -10889,25 +11357,25 @@ "type": "tidelift" } ], - "time": "2026-05-16T08:40:45+00:00" + "time": "2026-07-21T23:38:06+00:00" }, { "name": "symfony/ai-generic-platform", - "version": "v0.9.0", + "version": "v0.12.0", "source": { "type": "git", "url": "https://github.com/symfony/ai-generic-platform.git", - "reference": "8887d12b8ea97d079c5c97de4aebb19f42c58dc5" + "reference": "df2cc5cebb6d4196b67a9d73ef630b552af581c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/ai-generic-platform/zipball/8887d12b8ea97d079c5c97de4aebb19f42c58dc5", - "reference": "8887d12b8ea97d079c5c97de4aebb19f42c58dc5", + "url": "https://api.github.com/repos/symfony/ai-generic-platform/zipball/df2cc5cebb6d4196b67a9d73ef630b552af581c7", + "reference": "df2cc5cebb6d4196b67a9d73ef630b552af581c7", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/ai-platform": "^0.9", + "symfony/ai-platform": "^0.12", "symfony/http-client": "^7.3|^8.0" }, "require-dev": { @@ -10954,7 +11422,7 @@ "platform" ], "support": { - "source": "https://github.com/symfony/ai-generic-platform/tree/v0.9.0" + "source": "https://github.com/symfony/ai-generic-platform/tree/v0.12.0" }, "funding": [ { @@ -10974,26 +11442,26 @@ "type": "tidelift" } ], - "time": "2026-05-16T01:01:33+00:00" + "time": "2026-07-21T23:38:06+00:00" }, { "name": "symfony/ai-lm-studio-platform", - "version": "v0.9.0", + "version": "v0.12.0", "source": { "type": "git", "url": "https://github.com/symfony/ai-lm-studio-platform.git", - "reference": "9e53e56c8c3a04dddb955088b40904e747ec3981" + "reference": "7594a3a305d8ea5c1cfe7493bb2352e7fdf5afed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/ai-lm-studio-platform/zipball/9e53e56c8c3a04dddb955088b40904e747ec3981", - "reference": "9e53e56c8c3a04dddb955088b40904e747ec3981", + "url": "https://api.github.com/repos/symfony/ai-lm-studio-platform/zipball/7594a3a305d8ea5c1cfe7493bb2352e7fdf5afed", + "reference": "7594a3a305d8ea5c1cfe7493bb2352e7fdf5afed", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/ai-generic-platform": "^0.9", - "symfony/ai-platform": "^0.9", + "symfony/ai-generic-platform": "^0.12", + "symfony/ai-platform": "^0.12", "symfony/http-client": "^7.3|^8.0" }, "require-dev": { @@ -11041,7 +11509,7 @@ "platform" ], "support": { - "source": "https://github.com/symfony/ai-lm-studio-platform/tree/v0.9.0" + "source": "https://github.com/symfony/ai-lm-studio-platform/tree/v0.12.0" }, "funding": [ { @@ -11061,26 +11529,112 @@ "type": "tidelift" } ], - "time": "2026-05-16T01:01:33+00:00" + "time": "2026-07-21T23:38:06+00:00" }, { - "name": "symfony/ai-open-router-platform", - "version": "v0.9.0", + "name": "symfony/ai-ollama-platform", + "version": "v0.12.0", "source": { "type": "git", - "url": "https://github.com/symfony/ai-open-router-platform.git", - "reference": "7e2b560c86f618cd5d33f9f0c581d83bebc9802f" + "url": "https://github.com/symfony/ai-ollama-platform.git", + "reference": "17b050999d417e47e28f76f7054164b3c69e1f70" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/ai-open-router-platform/zipball/7e2b560c86f618cd5d33f9f0c581d83bebc9802f", - "reference": "7e2b560c86f618cd5d33f9f0c581d83bebc9802f", + "url": "https://api.github.com/repos/symfony/ai-ollama-platform/zipball/17b050999d417e47e28f76f7054164b3c69e1f70", + "reference": "17b050999d417e47e28f76f7054164b3c69e1f70", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/ai-generic-platform": "^0.9", - "symfony/ai-platform": "^0.9", + "symfony/ai-platform": "^0.12", + "symfony/http-client": "^7.3|^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^11.5.53" + }, + "type": "symfony-ai-platform", + "extra": { + "thanks": { + "url": "https://github.com/symfony/ai", + "name": "symfony/ai" + } + }, + "autoload": { + "psr-4": { + "Symfony\\AI\\Platform\\Bridge\\Ollama\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christopher Hertel", + "email": "mail@christopher-hertel.de" + }, + { + "name": "Oskar Stark", + "email": "oskarstark@googlemail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Ollama platform bridge for Symfony AI", + "keywords": [ + "Bridge", + "ai", + "local", + "ollama", + "platform" + ], + "support": { + "source": "https://github.com/symfony/ai-ollama-platform/tree/v0.12.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-21T23:38:06+00:00" + }, + { + "name": "symfony/ai-open-router-platform", + "version": "v0.12.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/ai-open-router-platform.git", + "reference": "6a424947c2583b55d8886ec29e65d0455c01b4b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/ai-open-router-platform/zipball/6a424947c2583b55d8886ec29e65d0455c01b4b3", + "reference": "6a424947c2583b55d8886ec29e65d0455c01b4b3", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/ai-generic-platform": "^0.12", + "symfony/ai-platform": "^0.12", "symfony/http-client": "^7.3|^8.0" }, "require-dev": { @@ -11128,7 +11682,7 @@ "platform" ], "support": { - "source": "https://github.com/symfony/ai-open-router-platform/tree/v0.9.0" + "source": "https://github.com/symfony/ai-open-router-platform/tree/v0.12.0" }, "funding": [ { @@ -11148,20 +11702,20 @@ "type": "tidelift" } ], - "time": "2026-05-16T01:01:33+00:00" + "time": "2026-07-21T23:38:06+00:00" }, { "name": "symfony/ai-platform", - "version": "v0.9.0", + "version": "v0.12.0", "source": { "type": "git", "url": "https://github.com/symfony/ai-platform.git", - "reference": "fb55ebdf20bbe30af6752a0ce6a25abc56b2b625" + "reference": "af04cc80651271a35d38f20018603a9d837c5fda" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/ai-platform/zipball/fb55ebdf20bbe30af6752a0ce6a25abc56b2b625", - "reference": "fb55ebdf20bbe30af6752a0ce6a25abc56b2b625", + "url": "https://api.github.com/repos/symfony/ai-platform/zipball/af04cc80651271a35d38f20018603a9d837c5fda", + "reference": "af04cc80651271a35d38f20018603a9d837c5fda", "shasum": "" }, "require": { @@ -11260,7 +11814,7 @@ "voyage" ], "support": { - "source": "https://github.com/symfony/ai-platform/tree/v0.9.0" + "source": "https://github.com/symfony/ai-platform/tree/v0.12.0" }, "funding": [ { @@ -11280,7 +11834,7 @@ "type": "tidelift" } ], - "time": "2026-05-15T19:15:50+00:00" + "time": "2026-07-21T01:26:51+00:00" }, { "name": "symfony/apache-pack", @@ -11383,16 +11937,16 @@ }, { "name": "symfony/cache", - "version": "v7.4.13", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "4c09e18a92cce126cc0d1155825279fca8cd0673" + "reference": "9adfcb2a7fc3924473b09f5a0b058dcc2cc7be9a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/4c09e18a92cce126cc0d1155825279fca8cd0673", - "reference": "4c09e18a92cce126cc0d1155825279fca8cd0673", + "url": "https://api.github.com/repos/symfony/cache/zipball/9adfcb2a7fc3924473b09f5a0b058dcc2cc7be9a", + "reference": "9adfcb2a7fc3924473b09f5a0b058dcc2cc7be9a", "shasum": "" }, "require": { @@ -11463,7 +12017,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v7.4.13" + "source": "https://github.com/symfony/cache/tree/v7.4.14" }, "funding": [ { @@ -11483,20 +12037,20 @@ "type": "tidelift" } ], - "time": "2026-05-24T08:43:14+00:00" + "time": "2026-06-17T14:44:48+00:00" }, { "name": "symfony/cache-contracts", - "version": "v3.7.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/cache-contracts.git", - "reference": "225e8a254166bd3442e370c6f50145465db63831" + "reference": "9789738bc19af1106dc54d6afba9a0b467516cf2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/225e8a254166bd3442e370c6f50145465db63831", - "reference": "225e8a254166bd3442e370c6f50145465db63831", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/9789738bc19af1106dc54d6afba9a0b467516cf2", + "reference": "9789738bc19af1106dc54d6afba9a0b467516cf2", "shasum": "" }, "require": { @@ -11543,7 +12097,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/cache-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/cache-contracts/tree/v3.7.1" }, "funding": [ { @@ -11563,7 +12117,7 @@ "type": "tidelift" } ], - "time": "2026-05-05T15:33:14+00:00" + "time": "2026-06-05T06:23:12+00:00" }, { "name": "symfony/clock", @@ -11645,16 +12199,16 @@ }, { "name": "symfony/config", - "version": "v7.4.10", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57" + "reference": "7b665e443381ea7c4db03eb03b4bf79ea2b020eb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57", - "reference": "d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57", + "url": "https://api.github.com/repos/symfony/config/zipball/7b665e443381ea7c4db03eb03b4bf79ea2b020eb", + "reference": "7b665e443381ea7c4db03eb03b4bf79ea2b020eb", "shasum": "" }, "require": { @@ -11700,7 +12254,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v7.4.10" + "source": "https://github.com/symfony/config/tree/v7.4.14" }, "funding": [ { @@ -11720,20 +12274,20 @@ "type": "tidelift" } ], - "time": "2026-05-03T14:20:49+00:00" + "time": "2026-06-09T07:51:57+00:00" }, { "name": "symfony/console", - "version": "v7.4.13", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "85095d2573eaefaf35e40b9513a9bf09f72cd217" + "reference": "92f58bc4bf97a92ed1b9f367f0cd44f20bde0e87" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/85095d2573eaefaf35e40b9513a9bf09f72cd217", - "reference": "85095d2573eaefaf35e40b9513a9bf09f72cd217", + "url": "https://api.github.com/repos/symfony/console/zipball/92f58bc4bf97a92ed1b9f367f0cd44f20bde0e87", + "reference": "92f58bc4bf97a92ed1b9f367f0cd44f20bde0e87", "shasum": "" }, "require": { @@ -11798,7 +12352,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.13" + "source": "https://github.com/symfony/console/tree/v7.4.14" }, "funding": [ { @@ -11818,7 +12372,7 @@ "type": "tidelift" } ], - "time": "2026-05-24T08:56:14+00:00" + "time": "2026-06-16T11:50:14+00:00" }, { "name": "symfony/css-selector", @@ -11891,16 +12445,16 @@ }, { "name": "symfony/dependency-injection", - "version": "v7.4.13", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "f299e20ce983be6c0744952533c6dfeaaa1448e2" + "reference": "2c8c64a33e2e6911579e1ff79a8e06c27d48d402" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/f299e20ce983be6c0744952533c6dfeaaa1448e2", - "reference": "f299e20ce983be6c0744952533c6dfeaaa1448e2", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/2c8c64a33e2e6911579e1ff79a8e06c27d48d402", + "reference": "2c8c64a33e2e6911579e1ff79a8e06c27d48d402", "shasum": "" }, "require": { @@ -11951,7 +12505,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v7.4.13" + "source": "https://github.com/symfony/dependency-injection/tree/v7.4.14" }, "funding": [ { @@ -11971,20 +12525,20 @@ "type": "tidelift" } ], - "time": "2026-05-20T14:07:29+00:00" + "time": "2026-06-24T07:41:05+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.7.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", - "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d", "shasum": "" }, "require": { @@ -12022,7 +12576,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1" }, "funding": [ { @@ -12042,20 +12596,20 @@ "type": "tidelift" } ], - "time": "2026-04-13T15:52:40+00:00" + "time": "2026-06-05T06:23:12+00:00" }, { "name": "symfony/doctrine-bridge", - "version": "v7.4.9", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/doctrine-bridge.git", - "reference": "7a87c85853f3069e3657a823c62b02952de46b0a" + "reference": "f65262a834d1117617d6e072cb180a0b79428789" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/7a87c85853f3069e3657a823c62b02952de46b0a", - "reference": "7a87c85853f3069e3657a823c62b02952de46b0a", + "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/f65262a834d1117617d6e072cb180a0b79428789", + "reference": "f65262a834d1117617d6e072cb180a0b79428789", "shasum": "" }, "require": { @@ -12135,7 +12689,7 @@ "description": "Provides integration for Doctrine with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/doctrine-bridge/tree/v7.4.9" + "source": "https://github.com/symfony/doctrine-bridge/tree/v7.4.14" }, "funding": [ { @@ -12155,7 +12709,7 @@ "type": "tidelift" } ], - "time": "2026-04-29T14:19:39+00:00" + "time": "2026-06-11T07:31:44+00:00" }, { "name": "symfony/dom-crawler", @@ -12231,16 +12785,16 @@ }, { "name": "symfony/dotenv", - "version": "v7.4.11", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/dotenv.git", - "reference": "82e9b1355c68ef7b96397dbd34cc75a92eebae7c" + "reference": "9b9c7a00e565238857eea0040dc9745e3576402e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dotenv/zipball/82e9b1355c68ef7b96397dbd34cc75a92eebae7c", - "reference": "82e9b1355c68ef7b96397dbd34cc75a92eebae7c", + "url": "https://api.github.com/repos/symfony/dotenv/zipball/9b9c7a00e565238857eea0040dc9745e3576402e", + "reference": "9b9c7a00e565238857eea0040dc9745e3576402e", "shasum": "" }, "require": { @@ -12285,7 +12839,7 @@ "environment" ], "support": { - "source": "https://github.com/symfony/dotenv/tree/v7.4.11" + "source": "https://github.com/symfony/dotenv/tree/v7.4.14" }, "funding": [ { @@ -12305,20 +12859,20 @@ "type": "tidelift" } ], - "time": "2026-05-11T13:02:51+00:00" + "time": "2026-06-05T06:22:21+00:00" }, { "name": "symfony/error-handler", - "version": "v7.4.8", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa" + "reference": "4e1a093b481f323e6e326451f9760c3868430673" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa", - "reference": "8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/4e1a093b481f323e6e326451f9760c3868430673", + "reference": "4e1a093b481f323e6e326451f9760c3868430673", "shasum": "" }, "require": { @@ -12367,7 +12921,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v7.4.8" + "source": "https://github.com/symfony/error-handler/tree/v7.4.14" }, "funding": [ { @@ -12387,20 +12941,20 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-06-05T06:22:21+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v7.4.9", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "e4a2e29753c7801f7a8340e066cfa788f3bc8101" + "reference": "51fe3d170227be8d1772214b82ae506e15ed78ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/e4a2e29753c7801f7a8340e066cfa788f3bc8101", - "reference": "e4a2e29753c7801f7a8340e066cfa788f3bc8101", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/51fe3d170227be8d1772214b82ae506e15ed78ff", + "reference": "51fe3d170227be8d1772214b82ae506e15ed78ff", "shasum": "" }, "require": { @@ -12452,7 +13006,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.9" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.14" }, "funding": [ { @@ -12472,20 +13026,20 @@ "type": "tidelift" } ], - "time": "2026-04-18T13:18:21+00:00" + "time": "2026-06-06T11:10:32+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.7.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32" + "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/ccba7060602b7fed0b03c85bf025257f76d9ef32", - "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/c7de7a00ffb67842132da02ea92988a39ccd9f4e", + "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e", "shasum": "" }, "require": { @@ -12532,7 +13086,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.1" }, "funding": [ { @@ -12552,20 +13106,20 @@ "type": "tidelift" } ], - "time": "2026-01-05T13:30:16+00:00" + "time": "2026-06-05T06:23:12+00:00" }, { "name": "symfony/expression-language", - "version": "v7.4.8", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/expression-language.git", - "reference": "87ff95687748f4af65e4d5a6e917d448ec52aa83" + "reference": "bd5763f92959201816ecc31defdf352d2ea473be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/expression-language/zipball/87ff95687748f4af65e4d5a6e917d448ec52aa83", - "reference": "87ff95687748f4af65e4d5a6e917d448ec52aa83", + "url": "https://api.github.com/repos/symfony/expression-language/zipball/bd5763f92959201816ecc31defdf352d2ea473be", + "reference": "bd5763f92959201816ecc31defdf352d2ea473be", "shasum": "" }, "require": { @@ -12600,7 +13154,7 @@ "description": "Provides an engine that can compile and evaluate expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/expression-language/tree/v7.4.8" + "source": "https://github.com/symfony/expression-language/tree/v7.4.14" }, "funding": [ { @@ -12620,7 +13174,7 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-06-08T20:24:16+00:00" }, { "name": "symfony/filesystem", @@ -12694,16 +13248,16 @@ }, { "name": "symfony/finder", - "version": "v7.4.8", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "e0be088d22278583a82da281886e8c3592fbf149" + "reference": "13b38720174286f55d1761152b575a8d1436fc25" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/e0be088d22278583a82da281886e8c3592fbf149", - "reference": "e0be088d22278583a82da281886e8c3592fbf149", + "url": "https://api.github.com/repos/symfony/finder/zipball/13b38720174286f55d1761152b575a8d1436fc25", + "reference": "13b38720174286f55d1761152b575a8d1436fc25", "shasum": "" }, "require": { @@ -12738,7 +13292,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.4.8" + "source": "https://github.com/symfony/finder/tree/v7.4.14" }, "funding": [ { @@ -12758,7 +13312,7 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-06-27T08:31:18+00:00" }, { "name": "symfony/flex", @@ -12835,16 +13389,16 @@ }, { "name": "symfony/form", - "version": "v7.4.9", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/form.git", - "reference": "b6c107af659106abec1771d9d7d22da528644d3a" + "reference": "355e9567b60254deef62392829a1784e66322041" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/form/zipball/b6c107af659106abec1771d9d7d22da528644d3a", - "reference": "b6c107af659106abec1771d9d7d22da528644d3a", + "url": "https://api.github.com/repos/symfony/form/zipball/355e9567b60254deef62392829a1784e66322041", + "reference": "355e9567b60254deef62392829a1784e66322041", "shasum": "" }, "require": { @@ -12914,7 +13468,7 @@ "description": "Allows to easily create, process and reuse HTML forms", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/form/tree/v7.4.9" + "source": "https://github.com/symfony/form/tree/v7.4.14" }, "funding": [ { @@ -12934,20 +13488,20 @@ "type": "tidelift" } ], - "time": "2026-04-29T14:39:19+00:00" + "time": "2026-06-16T15:54:05+00:00" }, { "name": "symfony/framework-bundle", - "version": "v7.4.13", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/framework-bundle.git", - "reference": "8be39c7bf9e6f58fe49c07927572a9df7c961c95" + "reference": "4d11fd50d0a3d2c43b400154ad7ec35b84ea3e5b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/8be39c7bf9e6f58fe49c07927572a9df7c961c95", - "reference": "8be39c7bf9e6f58fe49c07927572a9df7c961c95", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/4d11fd50d0a3d2c43b400154ad7ec35b84ea3e5b", + "reference": "4d11fd50d0a3d2c43b400154ad7ec35b84ea3e5b", "shasum": "" }, "require": { @@ -12997,7 +13551,7 @@ "symfony/twig-bundle": "<6.4", "symfony/validator": "<6.4", "symfony/web-profiler-bundle": "<6.4", - "symfony/webhook": "<7.2", + "symfony/webhook": "<7.4", "symfony/workflow": "<7.4" }, "require-dev": { @@ -13041,7 +13595,7 @@ "symfony/uid": "^6.4|^7.0|^8.0", "symfony/validator": "^7.4|^8.0", "symfony/web-link": "^6.4|^7.0|^8.0", - "symfony/webhook": "^7.2|^8.0", + "symfony/webhook": "^7.4|^8.0", "symfony/workflow": "^7.4|^8.0", "symfony/yaml": "^7.3|^8.0", "twig/twig": "^3.12" @@ -13072,7 +13626,7 @@ "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/framework-bundle/tree/v7.4.13" + "source": "https://github.com/symfony/framework-bundle/tree/v7.4.14" }, "funding": [ { @@ -13092,20 +13646,94 @@ "type": "tidelift" } ], - "time": "2026-05-23T18:04:28+00:00" + "time": "2026-06-27T08:31:38+00:00" }, { - "name": "symfony/http-client", - "version": "v7.4.13", + "name": "symfony/html-sanitizer", + "version": "v7.4.14", "source": { "type": "git", - "url": "https://github.com/symfony/http-client.git", - "reference": "e8a112b8415707265a7e614278136a9d92989a6a" + "url": "https://github.com/symfony/html-sanitizer.git", + "reference": "c328df69f5b6f44a0d031d757903d955bebb23b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/e8a112b8415707265a7e614278136a9d92989a6a", - "reference": "e8a112b8415707265a7e614278136a9d92989a6a", + "url": "https://api.github.com/repos/symfony/html-sanitizer/zipball/c328df69f5b6f44a0d031d757903d955bebb23b3", + "reference": "c328df69f5b6f44a0d031d757903d955bebb23b3", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "league/uri": "^6.5|^7.0", + "masterminds/html5": "^2.7.2", + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HtmlSanitizer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Titouan Galopin", + "email": "galopintitouan@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to sanitize untrusted HTML input for safe insertion into a document's DOM.", + "homepage": "https://symfony.com", + "keywords": [ + "Purifier", + "html", + "sanitizer" + ], + "support": { + "source": "https://github.com/symfony/html-sanitizer/tree/v7.4.14" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-06T11:10:32+00:00" + }, + { + "name": "symfony/http-client", + "version": "v7.4.14", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "f6bc6b5a54ff5afac4725cacec9bf2f52eb15920" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/f6bc6b5a54ff5afac4725cacec9bf2f52eb15920", + "reference": "f6bc6b5a54ff5afac4725cacec9bf2f52eb15920", "shasum": "" }, "require": { @@ -13173,7 +13801,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.4.13" + "source": "https://github.com/symfony/http-client/tree/v7.4.14" }, "funding": [ { @@ -13193,20 +13821,20 @@ "type": "tidelift" } ], - "time": "2026-05-24T09:57:54+00:00" + "time": "2026-06-16T11:50:14+00:00" }, { "name": "symfony/http-client-contracts", - "version": "v3.7.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "4a2d00c37651c0bdc2b9e1c773487a8bf4edb12d" + "reference": "41fc42d276aeff21192465331ebbab7d83a743c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/4a2d00c37651c0bdc2b9e1c773487a8bf4edb12d", - "reference": "4a2d00c37651c0bdc2b9e1c773487a8bf4edb12d", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/41fc42d276aeff21192465331ebbab7d83a743c0", + "reference": "41fc42d276aeff21192465331ebbab7d83a743c0", "shasum": "" }, "require": { @@ -13255,7 +13883,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/http-client-contracts/tree/v3.7.1" }, "funding": [ { @@ -13275,20 +13903,20 @@ "type": "tidelift" } ], - "time": "2026-03-06T13:17:50+00:00" + "time": "2026-06-05T06:23:12+00:00" }, { "name": "symfony/http-foundation", - "version": "v7.4.13", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "bc354f47c62301e990b7874fa662326368508e2c" + "reference": "06db5ae1552177bf8572f8908839f12e3c06aed3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/bc354f47c62301e990b7874fa662326368508e2c", - "reference": "bc354f47c62301e990b7874fa662326368508e2c", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/06db5ae1552177bf8572f8908839f12e3c06aed3", + "reference": "06db5ae1552177bf8572f8908839f12e3c06aed3", "shasum": "" }, "require": { @@ -13337,7 +13965,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.4.13" + "source": "https://github.com/symfony/http-foundation/tree/v7.4.14" }, "funding": [ { @@ -13357,20 +13985,20 @@ "type": "tidelift" } ], - "time": "2026-05-24T11:20:33+00:00" + "time": "2026-06-11T07:31:44+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.4.13", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "9df847980c436451f4f51d1284491bb4356dd989" + "reference": "e99af79b1e776646eda0e1c23b7b45c184ff99be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/9df847980c436451f4f51d1284491bb4356dd989", - "reference": "9df847980c436451f4f51d1284491bb4356dd989", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/e99af79b1e776646eda0e1c23b7b45c184ff99be", + "reference": "e99af79b1e776646eda0e1c23b7b45c184ff99be", "shasum": "" }, "require": { @@ -13456,7 +14084,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.4.13" + "source": "https://github.com/symfony/http-kernel/tree/v7.4.14" }, "funding": [ { @@ -13476,20 +14104,20 @@ "type": "tidelift" } ], - "time": "2026-05-27T08:31:43+00:00" + "time": "2026-06-27T09:14:35+00:00" }, { "name": "symfony/intl", - "version": "v7.4.8", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/intl.git", - "reference": "7cfb7792d580dea833647420afd5f2f98df8457b" + "reference": "02d77a81a198788444a8371658ef98e2e20c8c2b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/intl/zipball/7cfb7792d580dea833647420afd5f2f98df8457b", - "reference": "7cfb7792d580dea833647420afd5f2f98df8457b", + "url": "https://api.github.com/repos/symfony/intl/zipball/02d77a81a198788444a8371658ef98e2e20c8c2b", + "reference": "02d77a81a198788444a8371658ef98e2e20c8c2b", "shasum": "" }, "require": { @@ -13546,7 +14174,7 @@ "localization" ], "support": { - "source": "https://github.com/symfony/intl/tree/v7.4.8" + "source": "https://github.com/symfony/intl/tree/v7.4.14" }, "funding": [ { @@ -13566,20 +14194,20 @@ "type": "tidelift" } ], - "time": "2026-03-30T12:55:43+00:00" + "time": "2026-06-05T06:22:21+00:00" }, { "name": "symfony/mailer", - "version": "v7.4.12", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "5cefb712a25f320579615ba9e1942abaeade7dff" + "reference": "f88ce03ae73e3edb5c176ce1f337709996e88495" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/5cefb712a25f320579615ba9e1942abaeade7dff", - "reference": "5cefb712a25f320579615ba9e1942abaeade7dff", + "url": "https://api.github.com/repos/symfony/mailer/zipball/f88ce03ae73e3edb5c176ce1f337709996e88495", + "reference": "f88ce03ae73e3edb5c176ce1f337709996e88495", "shasum": "" }, "require": { @@ -13630,7 +14258,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.4.12" + "source": "https://github.com/symfony/mailer/tree/v7.4.14" }, "funding": [ { @@ -13650,7 +14278,94 @@ "type": "tidelift" } ], - "time": "2026-05-20T07:20:23+00:00" + "time": "2026-06-13T08:51:35+00:00" + }, + { + "name": "symfony/mcp-bundle", + "version": "v0.12.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/mcp-bundle.git", + "reference": "7ffd7bfab9d315a52a8224e35b32bd0db1765d44" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mcp-bundle/zipball/7ffd7bfab9d315a52a8224e35b32bd0db1765d44", + "reference": "7ffd7bfab9d315a52a8224e35b32bd0db1765d44", + "shasum": "" + }, + "require": { + "mcp/sdk": "^0.7", + "php-http/discovery": "^1.20", + "symfony/config": "^7.3|^8.0", + "symfony/console": "^7.3|^8.0", + "symfony/dependency-injection": "^7.3|^8.0", + "symfony/finder": "^7.3|^8.0", + "symfony/framework-bundle": "^7.3|^8.0", + "symfony/http-foundation": "^7.3|^8.0", + "symfony/http-kernel": "^7.3|^8.0", + "symfony/psr-http-message-bridge": "^7.3|^8.0", + "symfony/routing": "^7.3|^8.0", + "symfony/service-contracts": "^2.5|^3" + }, + "require-dev": { + "nyholm/psr7": "^1.8", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^11.5.53", + "symfony/monolog-bundle": "^3.10 || ^4.0", + "symfony/twig-bundle": "^7.3|^8.0" + }, + "type": "symfony-bundle", + "extra": { + "thanks": { + "url": "https://github.com/symfony/ai", + "name": "symfony/ai" + } + }, + "autoload": { + "psr-4": { + "Symfony\\AI\\McpBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christopher Hertel", + "email": "mail@christopher-hertel.de" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony integration bundle for Model Context Protocol (via official mcp/sdk)", + "support": { + "source": "https://github.com/symfony/mcp-bundle/tree/v0.12.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-21T01:04:45+00:00" }, { "name": "symfony/mime", @@ -13903,6 +14618,79 @@ ], "time": "2026-04-02T18:27:21+00:00" }, + { + "name": "symfony/object-mapper", + "version": "v7.4.14", + "source": { + "type": "git", + "url": "https://github.com/symfony/object-mapper.git", + "reference": "ce13a5bce3c8ddf5cf17f0f557e294f7e0532add" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/object-mapper/zipball/ce13a5bce3c8ddf5cf17f0f557e294f7e0532add", + "reference": "ce13a5bce3c8ddf5cf17f0f557e294f7e0532add", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/container": "^2.0" + }, + "conflict": { + "symfony/property-access": "<7.2" + }, + "require-dev": { + "symfony/property-access": "^7.2|^8.0", + "symfony/var-exporter": "^7.2|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ObjectMapper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a way to map an object to another object", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/object-mapper/tree/v7.4.14" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-16T15:47:47+00:00" + }, { "name": "symfony/options-resolver", "version": "v7.4.8", @@ -14477,16 +15265,16 @@ }, { "name": "symfony/polyfill-php83", - "version": "v1.38.1", + "version": "v1.38.2", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "8339098cae28673c15cce00d80734af0453054e2" + "reference": "796a26abb75ce49f3a84433cd81bf1009d73d5f8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/8339098cae28673c15cce00d80734af0453054e2", - "reference": "8339098cae28673c15cce00d80734af0453054e2", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/796a26abb75ce49f3a84433cd81bf1009d73d5f8", + "reference": "796a26abb75ce49f3a84433cd81bf1009d73d5f8", "shasum": "" }, "require": { @@ -14533,7 +15321,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.38.1" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.38.2" }, "funding": [ { @@ -14553,7 +15341,7 @@ "type": "tidelift" } ], - "time": "2026-05-26T12:51:13+00:00" + "time": "2026-05-27T06:51:48+00:00" }, { "name": "symfony/polyfill-php84", @@ -15124,16 +15912,16 @@ }, { "name": "symfony/rate-limiter", - "version": "v7.4.13", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/rate-limiter.git", - "reference": "8b162768544e5a8895c52161d63c999aca91f4a9" + "reference": "5c0af3fdc93e6115d9b806a6ea73e7de040e711d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/rate-limiter/zipball/8b162768544e5a8895c52161d63c999aca91f4a9", - "reference": "8b162768544e5a8895c52161d63c999aca91f4a9", + "url": "https://api.github.com/repos/symfony/rate-limiter/zipball/5c0af3fdc93e6115d9b806a6ea73e7de040e711d", + "reference": "5c0af3fdc93e6115d9b806a6ea73e7de040e711d", "shasum": "" }, "require": { @@ -15174,7 +15962,7 @@ "rate-limiter" ], "support": { - "source": "https://github.com/symfony/rate-limiter/tree/v7.4.13" + "source": "https://github.com/symfony/rate-limiter/tree/v7.4.14" }, "funding": [ { @@ -15194,7 +15982,7 @@ "type": "tidelift" } ], - "time": "2026-05-23T16:05:06+00:00" + "time": "2026-06-08T20:24:16+00:00" }, { "name": "symfony/routing", @@ -15283,16 +16071,16 @@ }, { "name": "symfony/runtime", - "version": "v7.4.13", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/runtime.git", - "reference": "1a24cf8aab3a9378117718b35525c4126ad3adec" + "reference": "15497f743dd714f3167cb6a56509b9d42e6417b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/runtime/zipball/1a24cf8aab3a9378117718b35525c4126ad3adec", - "reference": "1a24cf8aab3a9378117718b35525c4126ad3adec", + "url": "https://api.github.com/repos/symfony/runtime/zipball/15497f743dd714f3167cb6a56509b9d42e6417b3", + "reference": "15497f743dd714f3167cb6a56509b9d42e6417b3", "shasum": "" }, "require": { @@ -15300,7 +16088,8 @@ "php": ">=8.2" }, "conflict": { - "symfony/dotenv": "<6.4" + "symfony/dotenv": "<6.4", + "symfony/http-foundation": "<6.4" }, "require-dev": { "composer/composer": "^2.6", @@ -15343,7 +16132,7 @@ "runtime" ], "support": { - "source": "https://github.com/symfony/runtime/tree/v7.4.13" + "source": "https://github.com/symfony/runtime/tree/v7.4.14" }, "funding": [ { @@ -15363,20 +16152,20 @@ "type": "tidelift" } ], - "time": "2026-05-23T18:04:28+00:00" + "time": "2026-06-05T06:22:21+00:00" }, { "name": "symfony/security-bundle", - "version": "v7.4.13", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/security-bundle.git", - "reference": "0cbc6528aa583795ab44e43b4e92a09acf927c6f" + "reference": "6ec147e67262c6f5ac5dbb8b2ccbb34af14c4d79" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-bundle/zipball/0cbc6528aa583795ab44e43b4e92a09acf927c6f", - "reference": "0cbc6528aa583795ab44e43b4e92a09acf927c6f", + "url": "https://api.github.com/repos/symfony/security-bundle/zipball/6ec147e67262c6f5ac5dbb8b2ccbb34af14c4d79", + "reference": "6ec147e67262c6f5ac5dbb8b2ccbb34af14c4d79", "shasum": "" }, "require": { @@ -15455,7 +16244,7 @@ "description": "Provides a tight integration of the Security component into the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-bundle/tree/v7.4.13" + "source": "https://github.com/symfony/security-bundle/tree/v7.4.14" }, "funding": [ { @@ -15475,20 +16264,20 @@ "type": "tidelift" } ], - "time": "2026-05-23T16:05:06+00:00" + "time": "2026-06-16T15:54:05+00:00" }, { "name": "symfony/security-core", - "version": "v7.4.13", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/security-core.git", - "reference": "25db686fcf2a3fe00e1cf6dcab1fcb7aac71ba9b" + "reference": "880bb18eff6188d55115e795f06e4185373c35fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-core/zipball/25db686fcf2a3fe00e1cf6dcab1fcb7aac71ba9b", - "reference": "25db686fcf2a3fe00e1cf6dcab1fcb7aac71ba9b", + "url": "https://api.github.com/repos/symfony/security-core/zipball/880bb18eff6188d55115e795f06e4185373c35fd", + "reference": "880bb18eff6188d55115e795f06e4185373c35fd", "shasum": "" }, "require": { @@ -15546,7 +16335,7 @@ "description": "Symfony Security Component - Core Library", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-core/tree/v7.4.13" + "source": "https://github.com/symfony/security-core/tree/v7.4.14" }, "funding": [ { @@ -15566,7 +16355,7 @@ "type": "tidelift" } ], - "time": "2026-05-23T16:05:06+00:00" + "time": "2026-06-09T07:51:57+00:00" }, { "name": "symfony/security-csrf", @@ -15644,16 +16433,16 @@ }, { "name": "symfony/security-http", - "version": "v7.4.13", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/security-http.git", - "reference": "da3c28025a664e6a88e1af104a74457d99301161" + "reference": "148e038b91c8cc3e42aee177f8b0117437077c9b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-http/zipball/da3c28025a664e6a88e1af104a74457d99301161", - "reference": "da3c28025a664e6a88e1af104a74457d99301161", + "url": "https://api.github.com/repos/symfony/security-http/zipball/148e038b91c8cc3e42aee177f8b0117437077c9b", + "reference": "148e038b91c8cc3e42aee177f8b0117437077c9b", "shasum": "" }, "require": { @@ -15712,7 +16501,7 @@ "description": "Symfony Security Component - HTTP Integration", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-http/tree/v7.4.13" + "source": "https://github.com/symfony/security-http/tree/v7.4.14" }, "funding": [ { @@ -15732,20 +16521,20 @@ "type": "tidelift" } ], - "time": "2026-05-25T06:06:12+00:00" + "time": "2026-06-19T08:40:54+00:00" }, { "name": "symfony/serializer", - "version": "v7.4.10", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", - "reference": "268c5aa6c4bd675eddd89348e7ecac292a843ddd" + "reference": "55acb01b9c8a5211dfbaf68c314d90d0ed2cc3d1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/268c5aa6c4bd675eddd89348e7ecac292a843ddd", - "reference": "268c5aa6c4bd675eddd89348e7ecac292a843ddd", + "url": "https://api.github.com/repos/symfony/serializer/zipball/55acb01b9c8a5211dfbaf68c314d90d0ed2cc3d1", + "reference": "55acb01b9c8a5211dfbaf68c314d90d0ed2cc3d1", "shasum": "" }, "require": { @@ -15816,7 +16605,7 @@ "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/serializer/tree/v7.4.10" + "source": "https://github.com/symfony/serializer/tree/v7.4.14" }, "funding": [ { @@ -15836,20 +16625,20 @@ "type": "tidelift" } ], - "time": "2026-05-03T13:03:28+00:00" + "time": "2026-06-27T08:31:18+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.7.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a" + "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a", - "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/c0a284bab1ed8aa0417e3d69250ab437739563a0", + "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0", "shasum": "" }, "require": { @@ -15903,7 +16692,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.7.1" }, "funding": [ { @@ -15923,7 +16712,7 @@ "type": "tidelift" } ], - "time": "2026-03-28T09:44:51+00:00" + "time": "2026-06-16T09:55:08+00:00" }, { "name": "symfony/stimulus-bundle", @@ -16157,16 +16946,16 @@ }, { "name": "symfony/translation", - "version": "v7.4.10", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "ada7578c30dd5feaa8259cff3e885069ea81ddde" + "reference": "a1af4dacb24eb7ef4f1ca71b94da8ddbce572281" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/ada7578c30dd5feaa8259cff3e885069ea81ddde", - "reference": "ada7578c30dd5feaa8259cff3e885069ea81ddde", + "url": "https://api.github.com/repos/symfony/translation/zipball/a1af4dacb24eb7ef4f1ca71b94da8ddbce572281", + "reference": "a1af4dacb24eb7ef4f1ca71b94da8ddbce572281", "shasum": "" }, "require": { @@ -16233,7 +17022,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.4.10" + "source": "https://github.com/symfony/translation/tree/v7.4.14" }, "funding": [ { @@ -16253,20 +17042,20 @@ "type": "tidelift" } ], - "time": "2026-05-06T11:19:24+00:00" + "time": "2026-06-06T09:33:19+00:00" }, { "name": "symfony/translation-contracts", - "version": "v3.7.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d" + "reference": "ccb206b98faccc511ebae8e5fad50f2dc0b30621" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/0ab302977a952b42fd51475c4ebac81f8da0a95d", - "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/ccb206b98faccc511ebae8e5fad50f2dc0b30621", + "reference": "ccb206b98faccc511ebae8e5fad50f2dc0b30621", "shasum": "" }, "require": { @@ -16315,7 +17104,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/translation-contracts/tree/v3.7.1" }, "funding": [ { @@ -16335,20 +17124,20 @@ "type": "tidelift" } ], - "time": "2026-01-05T13:30:16+00:00" + "time": "2026-06-05T06:23:12+00:00" }, { "name": "symfony/twig-bridge", - "version": "v7.4.12", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/twig-bridge.git", - "reference": "81663873d946531129c76c65e80b681ce99c0e89" + "reference": "e4574ab4d5411a7c495d4189b15a8ecfbc720332" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/81663873d946531129c76c65e80b681ce99c0e89", - "reference": "81663873d946531129c76c65e80b681ce99c0e89", + "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/e4574ab4d5411a7c495d4189b15a8ecfbc720332", + "reference": "e4574ab4d5411a7c495d4189b15a8ecfbc720332", "shasum": "" }, "require": { @@ -16430,7 +17219,7 @@ "description": "Provides integration for Twig with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/twig-bridge/tree/v7.4.12" + "source": "https://github.com/symfony/twig-bridge/tree/v7.4.14" }, "funding": [ { @@ -16450,20 +17239,20 @@ "type": "tidelift" } ], - "time": "2026-04-29T17:13:54+00:00" + "time": "2026-06-17T13:16:29+00:00" }, { "name": "symfony/twig-bundle", - "version": "v7.4.8", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/twig-bundle.git", - "reference": "ba1e06d7ff1ebb1d1799b6608d925f4eaba88d95" + "reference": "11b69c64efdd0c3465403bb2747bf4585add1ec7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/twig-bundle/zipball/ba1e06d7ff1ebb1d1799b6608d925f4eaba88d95", - "reference": "ba1e06d7ff1ebb1d1799b6608d925f4eaba88d95", + "url": "https://api.github.com/repos/symfony/twig-bundle/zipball/11b69c64efdd0c3465403bb2747bf4585add1ec7", + "reference": "11b69c64efdd0c3465403bb2747bf4585add1ec7", "shasum": "" }, "require": { @@ -16520,7 +17309,7 @@ "description": "Provides a tight integration of Twig into the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/twig-bundle/tree/v7.4.8" + "source": "https://github.com/symfony/twig-bundle/tree/v7.4.14" }, "funding": [ { @@ -16540,7 +17329,7 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-06-05T06:22:21+00:00" }, { "name": "symfony/type-info", @@ -16889,16 +17678,16 @@ }, { "name": "symfony/validator", - "version": "v7.4.10", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "c76458623af9a3fe3b2e5b09b36453f334c2a361" + "reference": "306d904336166d001751759351d40d5e82312596" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/c76458623af9a3fe3b2e5b09b36453f334c2a361", - "reference": "c76458623af9a3fe3b2e5b09b36453f334c2a361", + "url": "https://api.github.com/repos/symfony/validator/zipball/306d904336166d001751759351d40d5e82312596", + "reference": "306d904336166d001751759351d40d5e82312596", "shasum": "" }, "require": { @@ -16969,7 +17758,7 @@ "description": "Provides tools to validate values", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/validator/tree/v7.4.10" + "source": "https://github.com/symfony/validator/tree/v7.4.14" }, "funding": [ { @@ -16989,20 +17778,20 @@ "type": "tidelift" } ], - "time": "2026-05-05T15:30:56+00:00" + "time": "2026-06-27T06:16:12+00:00" }, { "name": "symfony/var-dumper", - "version": "v7.4.8", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "9510c3966f749a1d1ff0059e1eabef6cc621e7fd" + "reference": "9a3a56a4a1e65a5cb4f8d13801fe8ab0a170e358" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/9510c3966f749a1d1ff0059e1eabef6cc621e7fd", - "reference": "9510c3966f749a1d1ff0059e1eabef6cc621e7fd", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/9a3a56a4a1e65a5cb4f8d13801fe8ab0a170e358", + "reference": "9a3a56a4a1e65a5cb4f8d13801fe8ab0a170e358", "shasum": "" }, "require": { @@ -17056,7 +17845,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.4.8" + "source": "https://github.com/symfony/var-dumper/tree/v7.4.14" }, "funding": [ { @@ -17076,20 +17865,20 @@ "type": "tidelift" } ], - "time": "2026-03-30T13:44:50+00:00" + "time": "2026-06-08T20:24:16+00:00" }, { "name": "symfony/var-exporter", - "version": "v7.4.9", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "22e03a49c95ef054a43601cd159b222bfab1c701" + "reference": "0118811b1d59f323bf131250b3fb919febfece28" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/22e03a49c95ef054a43601cd159b222bfab1c701", - "reference": "22e03a49c95ef054a43601cd159b222bfab1c701", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/0118811b1d59f323bf131250b3fb919febfece28", + "reference": "0118811b1d59f323bf131250b3fb919febfece28", "shasum": "" }, "require": { @@ -17137,7 +17926,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v7.4.9" + "source": "https://github.com/symfony/var-exporter/tree/v7.4.14" }, "funding": [ { @@ -17157,7 +17946,7 @@ "type": "tidelift" } ], - "time": "2026-04-18T13:18:21+00:00" + "time": "2026-06-27T08:41:53+00:00" }, { "name": "symfony/web-link", @@ -17248,16 +18037,16 @@ }, { "name": "symfony/webpack-encore-bundle", - "version": "v2.4.0", + "version": "v2.4.1", "source": { "type": "git", "url": "https://github.com/symfony/webpack-encore-bundle.git", - "reference": "5b932e0feddd81aaf0ecd7d5fcd2e450e5a7817e" + "reference": "cac8d6c722999c8add9272f9de6e8079628df4f5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/webpack-encore-bundle/zipball/5b932e0feddd81aaf0ecd7d5fcd2e450e5a7817e", - "reference": "5b932e0feddd81aaf0ecd7d5fcd2e450e5a7817e", + "url": "https://api.github.com/repos/symfony/webpack-encore-bundle/zipball/cac8d6c722999c8add9272f9de6e8079628df4f5", + "reference": "cac8d6c722999c8add9272f9de6e8079628df4f5", "shasum": "" }, "require": { @@ -17300,7 +18089,7 @@ "description": "Integration of your Symfony app with Webpack Encore", "support": { "issues": "https://github.com/symfony/webpack-encore-bundle/issues", - "source": "https://github.com/symfony/webpack-encore-bundle/tree/v2.4.0" + "source": "https://github.com/symfony/webpack-encore-bundle/tree/v2.4.1" }, "funding": [ { @@ -17320,20 +18109,20 @@ "type": "tidelift" } ], - "time": "2025-11-27T13:41:46+00:00" + "time": "2026-06-24T07:21:58+00:00" }, { "name": "symfony/yaml", - "version": "v7.4.13", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "a7ec3b1156faf8815db7683ec7c1e7338e6f977c" + "reference": "f8f328665ace2370d1e10645b807ba1646dc7dcc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/a7ec3b1156faf8815db7683ec7c1e7338e6f977c", - "reference": "a7ec3b1156faf8815db7683ec7c1e7338e6f977c", + "url": "https://api.github.com/repos/symfony/yaml/zipball/f8f328665ace2370d1e10645b807ba1646dc7dcc", + "reference": "f8f328665ace2370d1e10645b807ba1646dc7dcc", "shasum": "" }, "require": { @@ -17376,7 +18165,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.4.13" + "source": "https://github.com/symfony/yaml/tree/v7.4.14" }, "funding": [ { @@ -17396,20 +18185,20 @@ "type": "tidelift" } ], - "time": "2026-05-25T06:06:12+00:00" + "time": "2026-06-08T20:24:16+00:00" }, { "name": "symplify/easy-coding-standard", - "version": "13.1.5", + "version": "13.2.17", "source": { "type": "git", - "url": "https://github.com/easy-coding-standard/ecs.git", - "reference": "96294d652c17ccffabb7d36f9d116dd5f0e6b84e" + "url": "https://github.com/ecsphp/ecs.git", + "reference": "f5f8131520fb7d3efcf348b72205372ea619fe0e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/easy-coding-standard/ecs/zipball/96294d652c17ccffabb7d36f9d116dd5f0e6b84e", - "reference": "96294d652c17ccffabb7d36f9d116dd5f0e6b84e", + "url": "https://api.github.com/repos/ecsphp/ecs/zipball/f5f8131520fb7d3efcf348b72205372ea619fe0e", + "reference": "f5f8131520fb7d3efcf348b72205372ea619fe0e", "shasum": "" }, "require": { @@ -17444,36 +18233,33 @@ "static analysis" ], "support": { - "source": "https://github.com/easy-coding-standard/ecs/tree/13.1.5" + "source": "https://github.com/ecsphp/ecs/tree/13.2.17" }, - "time": "2026-05-30T09:12:26+00:00" + "time": "2026-07-22T20:02:43+00:00" }, { "name": "tecnickcom/tc-lib-barcode", - "version": "2.7.0", + "version": "2.12.0", "source": { "type": "git", "url": "https://github.com/tecnickcom/tc-lib-barcode.git", - "reference": "4e53047a4ba4ed592ae677b3729ce9bfeae1cfbb" + "reference": "0614e905c903600c2491d3a90174a9a87a2fab9b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tecnickcom/tc-lib-barcode/zipball/4e53047a4ba4ed592ae677b3729ce9bfeae1cfbb", - "reference": "4e53047a4ba4ed592ae677b3729ce9bfeae1cfbb", + "url": "https://api.github.com/repos/tecnickcom/tc-lib-barcode/zipball/0614e905c903600c2491d3a90174a9a87a2fab9b", + "reference": "0614e905c903600c2491d3a90174a9a87a2fab9b", "shasum": "" }, "require": { "ext-bcmath": "*", - "ext-date": "*", "ext-gd": "*", - "ext-pcre": "*", "php": ">=8.2", - "tecnickcom/tc-lib-color": "^2.7" + "tecnickcom/tc-lib-color": "^2.13" }, "require-dev": { "pdepend/pdepend": "^2.16", - "phpcompatibility/php-compatibility": "^10.0.0@dev", - "phpunit/phpunit": "^13.1 || ^12.5 || ^11.5" + "phpunit/phpunit": "^11.5 || ^12.5 || ^13.2" }, "type": "library", "autoload": { @@ -17545,30 +18331,28 @@ "type": "github" } ], - "time": "2026-05-22T07:09:18+00:00" + "time": "2026-07-17T19:05:47+00:00" }, { "name": "tecnickcom/tc-lib-color", - "version": "2.8.0", + "version": "2.13.1", "source": { "type": "git", "url": "https://github.com/tecnickcom/tc-lib-color.git", - "reference": "6947cc9fffe23a21642279b8ab73a43f3311c5f9" + "reference": "38303066464928bbcfe8e54d006c828b02e4c62f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tecnickcom/tc-lib-color/zipball/6947cc9fffe23a21642279b8ab73a43f3311c5f9", - "reference": "6947cc9fffe23a21642279b8ab73a43f3311c5f9", + "url": "https://api.github.com/repos/tecnickcom/tc-lib-color/zipball/38303066464928bbcfe8e54d006c828b02e4c62f", + "reference": "38303066464928bbcfe8e54d006c828b02e4c62f", "shasum": "" }, "require": { - "ext-pcre": "*", "php": ">=8.2" }, "require-dev": { "pdepend/pdepend": "^2.16", - "phpcompatibility/php-compatibility": "^10.0.0@dev", - "phpunit/phpunit": "^13.1 || ^12.5 || ^11.5" + "phpunit/phpunit": "^11.5 || ^12.5 || ^13.2" }, "type": "library", "autoload": { @@ -17613,7 +18397,7 @@ "type": "github" } ], - "time": "2026-05-22T06:55:57+00:00" + "time": "2026-07-17T19:02:53+00:00" }, { "name": "thecodingmachine/safe", @@ -18006,16 +18790,16 @@ }, { "name": "twig/html-extra", - "version": "v3.24.0", + "version": "v3.28.0", "source": { "type": "git", "url": "https://github.com/twigphp/html-extra.git", - "reference": "313900fb98b371b006a55b1a29241a192634be13" + "reference": "760893ed7bdd0a381e4e00004c6f6e26ad3881d7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/html-extra/zipball/313900fb98b371b006a55b1a29241a192634be13", - "reference": "313900fb98b371b006a55b1a29241a192634be13", + "url": "https://api.github.com/repos/twigphp/html-extra/zipball/760893ed7bdd0a381e4e00004c6f6e26ad3881d7", + "reference": "760893ed7bdd0a381e4e00004c6f6e26ad3881d7", "shasum": "" }, "require": { @@ -18058,7 +18842,7 @@ "twig" ], "support": { - "source": "https://github.com/twigphp/html-extra/tree/v3.24.0" + "source": "https://github.com/twigphp/html-extra/tree/v3.28.0" }, "funding": [ { @@ -18070,7 +18854,7 @@ "type": "tidelift" } ], - "time": "2026-03-17T07:24:08+00:00" + "time": "2026-06-25T06:50:01+00:00" }, { "name": "twig/inky-extra", @@ -18208,16 +18992,16 @@ }, { "name": "twig/markdown-extra", - "version": "v3.26.0", + "version": "v3.28.0", "source": { "type": "git", "url": "https://github.com/twigphp/markdown-extra.git", - "reference": "e3f3fd0836eb6c39457da22c8a76abaac62692b9" + "reference": "5f7b27e41a382fc988fffa6e588d8f9d55b9d896" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/markdown-extra/zipball/e3f3fd0836eb6c39457da22c8a76abaac62692b9", - "reference": "e3f3fd0836eb6c39457da22c8a76abaac62692b9", + "url": "https://api.github.com/repos/twigphp/markdown-extra/zipball/5f7b27e41a382fc988fffa6e588d8f9d55b9d896", + "reference": "5f7b27e41a382fc988fffa6e588d8f9d55b9d896", "shasum": "" }, "require": { @@ -18264,7 +19048,7 @@ "twig" ], "support": { - "source": "https://github.com/twigphp/markdown-extra/tree/v3.26.0" + "source": "https://github.com/twigphp/markdown-extra/tree/v3.28.0" }, "funding": [ { @@ -18276,7 +19060,7 @@ "type": "tidelift" } ], - "time": "2026-05-15T13:14:02+00:00" + "time": "2026-06-05T19:47:22+00:00" }, { "name": "twig/string-extra", @@ -18347,16 +19131,16 @@ }, { "name": "twig/twig", - "version": "v3.27.1", + "version": "v3.28.0", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "ae2071bffb38f04847fc0864d730c94b9cb8ab74" + "reference": "597c12ed286fb9d1701a36684ce6e0cbe28ebc8b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/ae2071bffb38f04847fc0864d730c94b9cb8ab74", - "reference": "ae2071bffb38f04847fc0864d730c94b9cb8ab74", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/597c12ed286fb9d1701a36684ce6e0cbe28ebc8b", + "reference": "597c12ed286fb9d1701a36684ce6e0cbe28ebc8b", "shasum": "" }, "require": { @@ -18411,7 +19195,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.27.1" + "source": "https://github.com/twigphp/Twig/tree/v3.28.0" }, "funding": [ { @@ -18423,7 +19207,7 @@ "type": "tidelift" } ], - "time": "2026-05-30T17:09:26+00:00" + "time": "2026-07-03T20:44:34+00:00" }, { "name": "ua-parser/uap-php", @@ -18490,20 +19274,20 @@ }, { "name": "web-auth/cose-lib", - "version": "4.5.2", + "version": "4.6.0", "source": { "type": "git", "url": "https://github.com/web-auth/cose-lib.git", - "reference": "5b38660f90070a8e45f3dbc9528ade3b608dd77d" + "reference": "3afe04df137baf97c5c3e28c5ee6f05536405148" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/web-auth/cose-lib/zipball/5b38660f90070a8e45f3dbc9528ade3b608dd77d", - "reference": "5b38660f90070a8e45f3dbc9528ade3b608dd77d", + "url": "https://api.github.com/repos/web-auth/cose-lib/zipball/3afe04df137baf97c5c3e28c5ee6f05536405148", + "reference": "3afe04df137baf97c5c3e28c5ee6f05536405148", "shasum": "" }, "require": { - "brick/math": "^0.9|^0.10|^0.11|^0.12|^0.13|^0.14|^0.15|^0.16|^0.17", + "brick/math": "^0.9|^0.10|^0.11|^0.12|^0.13|^0.14|^0.15|^0.16|^0.17|^0.18", "ext-json": "*", "ext-openssl": "*", "php": ">=8.1", @@ -18545,7 +19329,7 @@ ], "support": { "issues": "https://github.com/web-auth/cose-lib/issues", - "source": "https://github.com/web-auth/cose-lib/tree/4.5.2" + "source": "https://github.com/web-auth/cose-lib/tree/4.6.0" }, "funding": [ { @@ -18557,7 +19341,7 @@ "type": "patreon" } ], - "time": "2026-05-03T09:49:50+00:00" + "time": "2026-07-16T10:19:49+00:00" }, { "name": "web-auth/webauthn-lib", @@ -18730,16 +19514,16 @@ }, { "name": "webmozart/assert", - "version": "2.4.0", + "version": "2.4.1", "source": { "type": "git", "url": "https://github.com/webmozarts/assert.git", - "reference": "9007ea6f45ecf352a9422b36644e4bfc039b9155" + "reference": "2ccb7c2e821038c03a3e6e1700c570c158c55f70" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/9007ea6f45ecf352a9422b36644e4bfc039b9155", - "reference": "9007ea6f45ecf352a9422b36644e4bfc039b9155", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/2ccb7c2e821038c03a3e6e1700c570c158c55f70", + "reference": "2ccb7c2e821038c03a3e6e1700c570c158c55f70", "shasum": "" }, "require": { @@ -18790,9 +19574,9 @@ ], "support": { "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/2.4.0" + "source": "https://github.com/webmozarts/assert/tree/2.4.1" }, - "time": "2026-05-20T13:07:01+00:00" + "time": "2026-06-15T15:31:57+00:00" }, { "name": "willdurand/negotiation", @@ -19205,20 +19989,19 @@ }, { "name": "nikic/php-parser", - "version": "v5.7.0", + "version": "v5.8.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", - "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/044a6a392ff8ad0d61f14370a5fbbd0a0107152f", + "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f", "shasum": "" }, "require": { - "ext-ctype": "*", "ext-json": "*", "ext-tokenizer": "*", "php": ">=7.4" @@ -19257,9 +20040,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.8.0" }, - "time": "2025-12-06T11:56:16+00:00" + "time": "2026-07-04T14:30:18+00:00" }, { "name": "phar-io/manifest", @@ -19429,11 +20212,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.2.2", + "version": "2.2.6", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e5cc34d491a90e79c216d824f60fe21fd4d93bd6", - "reference": "e5cc34d491a90e79c216d824f60fe21fd4d93bd6", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/a6e9b5a9420f6109c091e87d82683bd1a80b87ed", + "reference": "a6e9b5a9420f6109c091e87d82683bd1a80b87ed", "shasum": "" }, "require": { @@ -19489,25 +20272,25 @@ "type": "github" } ], - "time": "2026-06-05T09:00:01+00:00" + "time": "2026-07-26T21:22:49+00:00" }, { "name": "phpstan/phpstan-doctrine", - "version": "2.0.25", + "version": "2.0.28", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-doctrine.git", - "reference": "e20e8bf3223ae6eba9c4b5987c391d922e094b3c" + "reference": "b4623954d5ffee6311e4ddbaef65c074ae0f781a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/e20e8bf3223ae6eba9c4b5987c391d922e094b3c", - "reference": "e20e8bf3223ae6eba9c4b5987c391d922e094b3c", + "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/b4623954d5ffee6311e4ddbaef65c074ae0f781a", + "reference": "b4623954d5ffee6311e4ddbaef65c074ae0f781a", "shasum": "" }, "require": { "php": "^7.4 || ^8.0", - "phpstan/phpstan": "^2.1.34" + "phpstan/phpstan": "^2.2.2" }, "conflict": { "doctrine/collections": "<1.0", @@ -19564,33 +20347,34 @@ ], "support": { "issues": "https://github.com/phpstan/phpstan-doctrine/issues", - "source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.25" + "source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.28" }, - "time": "2026-06-02T20:27:36+00:00" + "time": "2026-07-13T08:43:43+00:00" }, { "name": "phpstan/phpstan-strict-rules", - "version": "2.0.11", + "version": "2.0.12", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-strict-rules.git", - "reference": "9b000a578b85b32945b358b172c7b20e91189024" + "reference": "2bc5ae19ae965663b62ac907ee6342c3903ec93b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/9b000a578b85b32945b358b172c7b20e91189024", - "reference": "9b000a578b85b32945b358b172c7b20e91189024", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/2bc5ae19ae965663b62ac907ee6342c3903ec93b", + "reference": "2bc5ae19ae965663b62ac907ee6342c3903ec93b", "shasum": "" }, "require": { "php": "^7.4 || ^8.0", - "phpstan/phpstan": "^2.1.39" + "phpstan/phpstan": "^2.1.52" }, "require-dev": { "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/phpstan-deprecation-rules": "^2.0", "phpstan/phpstan-phpunit": "^2.0", - "phpunit/phpunit": "^9.6" + "phpunit/phpunit": "^9.6", + "shipmonk/name-collision-detector": "^2.1" }, "type": "phpstan-extension", "extra": { @@ -19615,22 +20399,22 @@ ], "support": { "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", - "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.11" + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.12" }, - "time": "2026-05-02T06:54:10+00:00" + "time": "2026-07-19T07:24:06+00:00" }, { "name": "phpstan/phpstan-symfony", - "version": "2.0.19", + "version": "2.0.20", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-symfony.git", - "reference": "546071ed7f80a89ec30909346eb7cc741800740a" + "reference": "53f1a6462dbe71fad36ce054caf5e1b725b740fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/546071ed7f80a89ec30909346eb7cc741800740a", - "reference": "546071ed7f80a89ec30909346eb7cc741800740a", + "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/53f1a6462dbe71fad36ce054caf5e1b725b740fd", + "reference": "53f1a6462dbe71fad36ce054caf5e1b725b740fd", "shasum": "" }, "require": { @@ -19689,9 +20473,9 @@ ], "support": { "issues": "https://github.com/phpstan/phpstan-symfony/issues", - "source": "https://github.com/phpstan/phpstan-symfony/tree/2.0.19" + "source": "https://github.com/phpstan/phpstan-symfony/tree/2.0.20" }, - "time": "2026-05-29T12:52:44+00:00" + "time": "2026-06-16T09:17:35+00:00" }, { "name": "phpunit/php-code-coverage", @@ -20042,24 +20826,24 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.55", + "version": "11.5.56", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00" + "reference": "5f83edffa6967c3db468d48a695ec7bcb02e9256" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/adc7262fccc12de2b30f12a8aa0b33775d814f00", - "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/5f83edffa6967c3db468d48a695ec7bcb02e9256", + "reference": "5f83edffa6967c3db468d48a695ec7bcb02e9256", "shasum": "" }, "require": { "ext-dom": "*", + "ext-filter": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", - "ext-xml": "*", "ext-xmlwriter": "*", "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", @@ -20124,49 +20908,33 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.55" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.56" }, "funding": [ { - "url": "https://phpunit.de/sponsors.html", - "type": "custom" - }, - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - }, - { - "url": "https://liberapay.com/sebastianbergmann", - "type": "liberapay" - }, - { - "url": "https://thanks.dev/u/gh/sebastianbergmann", - "type": "thanks_dev" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", - "type": "tidelift" + "url": "https://phpunit.de/sponsoring.html", + "type": "other" } ], - "time": "2026-02-18T12:37:06+00:00" + "time": "2026-07-06T14:52:39+00:00" }, { "name": "rector/rector", - "version": "2.4.5", + "version": "2.5.8", "source": { "type": "git", "url": "https://github.com/rectorphp/rector.git", - "reference": "cbd86024be5014d3c14d9f0b3f7aae8ecbffd62c" + "reference": "861c77fd2a6d45317a401f4e0b617f947e8b6f96" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/cbd86024be5014d3c14d9f0b3f7aae8ecbffd62c", - "reference": "cbd86024be5014d3c14d9f0b3f7aae8ecbffd62c", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/861c77fd2a6d45317a401f4e0b617f947e8b6f96", + "reference": "861c77fd2a6d45317a401f4e0b617f947e8b6f96", "shasum": "" }, "require": { "php": "^7.4|^8.0", - "phpstan/phpstan": "^2.1.56" + "phpstan/phpstan": "^2.2.6" }, "conflict": { "rector/rector-doctrine": "*", @@ -20200,7 +20968,7 @@ ], "support": { "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/2.4.5" + "source": "https://github.com/rectorphp/rector/tree/2.5.8" }, "funding": [ { @@ -20208,7 +20976,7 @@ "type": "github" } ], - "time": "2026-05-26T21:03:22+00:00" + "time": "2026-07-27T06:19:16+00:00" }, { "name": "roave/security-advisories", @@ -20216,18 +20984,19 @@ "source": { "type": "git", "url": "https://github.com/Roave/SecurityAdvisories.git", - "reference": "b000295a5fd02609ee7ad31d084cfa904af8a69a" + "reference": "e7d396822cefa7207fd352f61f68660811e108a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/b000295a5fd02609ee7ad31d084cfa904af8a69a", - "reference": "b000295a5fd02609ee7ad31d084cfa904af8a69a", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/e7d396822cefa7207fd352f61f68660811e108a4", + "reference": "e7d396822cefa7207fd352f61f68660811e108a4", "shasum": "" }, "conflict": { "3f/pygmentize": "<1.2", "adaptcms/adaptcms": "<=1.3", - "admidio/admidio": "<=5.0.9", + "adawolfa/isdoc": "<1.4.3|>=1.5,<1.5.1|>=1.6,<1.6.1", + "admidio/admidio": "<=5.0.11", "adodb/adodb-php": "<=5.22.9", "aheinze/cockpit": "<2.2", "aimeos/ai-admin-graphql": ">=2022.04.1,<2022.10.10|>=2023.04.1,<2023.10.6|>=2024.04.1,<2024.07.2", @@ -20238,6 +21007,7 @@ "aimeos/aimeos-core": ">=2022.04.1,<2022.10.17|>=2023.04.1,<2023.10.17|>=2024.04.1,<2024.04.7", "aimeos/aimeos-laravel": "==2021.10", "aimeos/aimeos-typo3": "<19.10.12|>=20,<20.10.5", + "aimeos/pagible": "<0.10.4", "airesvsg/acf-to-rest-api": "<=3.1", "akaunting/akaunting": "<2.1.13", "akeneo/pim-community-dev": "<5.0.119|>=6,<6.0.53", @@ -20260,8 +21030,10 @@ "aoe/restler": "<1.7.1", "apache-solr-for-typo3/solr": "<2.8.3", "apereo/phpcas": "<1.6", - "api-platform/core": "<3.4.17|>=4,<4.0.22|>=4.1,<4.1.5", + "api-platform/core": "<4.1.29|>=4.2,<4.2.25|>=4.3,<4.3.8", "api-platform/graphql": "<3.4.17|>=4,<4.0.22|>=4.1,<4.1.5", + "api-platform/hal": ">=4,<4.1.29|>=4.2,<4.2.25|>=4.3,<4.3.8", + "api-platform/json-api": ">=4,<4.1.29|>=4.2,<4.2.25|>=4.3,<4.3.8", "appwrite/server-ce": "<=1.2.1", "arc/web": "<3", "area17/twill": "<1.2.5|>=2,<2.5.3", @@ -20274,7 +21046,7 @@ "austintoddj/canvas": "<=3.4.2", "auth0/auth0-php": ">=3.3,<=8.18", "auth0/login": "<=7.20", - "auth0/symfony": "<=5.7", + "auth0/symfony": "<=5.8", "auth0/wordpress": "<=5.5", "automad/automad": "<=2.0.0.0-beta27", "automattic/jetpack": "<9.8", @@ -20322,13 +21094,14 @@ "bytefury/crater": "<6.0.2", "cachethq/cachet": "<2.5.1", "cadmium-org/cadmium-cms": "<=0.4.9", - "cakephp/cakephp": "<3.10.3|>=4,<4.0.10|>=4.1,<4.1.4|>=4.2,<4.2.12|>=4.3,<4.3.11|>=4.4,<4.4.10|>=5.2.10,<5.2.12|==5.3", + "cakephp/authentication": "<3.3.6|>=4,<4.1.1", + "cakephp/cakephp": "<4.5.11|>=4.6,<4.6.4|>=5,<5.1.7|>=5.2,<5.2.13|>=5.3,<5.3.6", "cakephp/database": ">=4.2,<4.2.12|>=4.3,<4.3.11|>=4.4,<4.4.10", "cardgate/magento2": "<2.0.33", "cardgate/woocommerce": "<=3.1.15", - "cart2quote/module-quotation": ">=4.1.6,<=4.4.5|>=5,<5.4.4", + "cart2quote/module-quotation": ">=4.1.6,<4.4.6|>=5,<5.4.4", "cart2quote/module-quotation-encoded": ">=4.1.6,<=4.4.5|>=5,<5.4.4", - "cartalyst/sentry": "<=2.1.6", + "cartalyst/sentry": "<2.1.7", "catfan/medoo": "<1.7.5", "causal/oidc": "<4", "cecil/cecil": "<7.47.1", @@ -20343,35 +21116,36 @@ "clickstorm/cs-seo": ">=6,<6.8|>=7,<7.5|>=8,<8.4|>=9,<9.3", "co-stack/fal_sftp": "<0.2.6", "cockpit-hq/cockpit": "<=2.14", - "code16/sharp": "<9.22", + "code16/sharp": "<9.22.3", "codeception/codeception": "<3.1.3|>=4,<4.1.22", "codeigniter/framework": "<3.1.10", - "codeigniter4/framework": "<4.6.2", + "codeigniter4/framework": "<4.7.2", "codeigniter4/shield": "<1.0.0.0-beta8", "codiad/codiad": "<=2.8.4", "codingms/additional-tca": ">=1.7,<1.15.17|>=1.16,<1.16.9", "codingms/modules": "<4.3.11|>=5,<5.7.4|>=6,<6.4.2|>=7,<7.5.5", "commerceteam/commerce": ">=0.9.6,<0.9.9", "components/jquery": ">=1.0.3,<3.5", - "composer/composer": "<2.2.28|>=2.3,<2.9.8", - "concrete5/concrete5": "<9.4.8", + "composer/composer": "<2.2.29|>=2.3,<2.10.2", + "concrete5/concrete5": "<9.5.2", "concrete5/core": "<8.5.8|>=9,<9.1", "contao-components/mediaelement": ">=2.14.2,<2.21.1", "contao/comments-bundle": ">=2,<4.13.40|>=5.0.0.0-RC1-dev,<5.3.4", - "contao/contao": ">=3,<3.5.37|>=4,<4.4.56|>=4.5,<4.13.56|>=5,<5.3.38|>=5.4.0.0-RC1-dev,<5.6.1", + "contao/contao": ">=3,<3.5.37|>=4,<4.4.56|>=4.5,<5.3.48|>=5.4,<5.7.9", "contao/core": "<3.5.39", - "contao/core-bundle": "<4.13.57|>=5,<5.3.42|>=5.4,<5.6.5", + "contao/core-bundle": "<5.3.48|>=5.4,<5.7.9", "contao/listing-bundle": ">=3,<=3.5.30|>=4,<4.4.8", "contao/managed-edition": "<=1.5", "coreshop/core-shop": "<4.1.9|==5", "corveda/phpsandbox": "<1.3.5", "cosenary/instagram": "<=2.3", + "cotonti/cotonti": "<=1", "couleurcitron/tarteaucitron-wp": "<0.3", "cpsit/typo3-mailqueue": "<0.4.5|>=0.5,<0.5.2", "craftcms/aws-s3": ">=2.0.2,<=2.2.4", "craftcms/azure-blob": ">=2.0.0.0-beta1,<=2.1", - "craftcms/cms": "<4.17.12|>=5,<5.9.18", - "craftcms/commerce": ">=4,<4.11|>=5,<5.6", + "craftcms/cms": "<4.18|>=5,<5.10", + "craftcms/commerce": ">=4,<=4.11.1|>=5,<=5.6.4", "craftcms/composer": ">=4.0.0.0-RC1-dev,<=4.10|>=5.0.0.0-RC1-dev,<=5.5.1", "craftcms/craft": ">=3.5,<=4.16.17|>=5.0.0.0-RC1-dev,<=5.8.21", "craftcms/google-cloud": ">=2.0.0.0-beta1,<=2.2", @@ -20412,7 +21186,7 @@ "doctrine/mongodb-odm-bundle": "<3.0.1", "doctrine/orm": ">=1,<1.2.4|>=2,<2.4.8|>=2.5,<2.5.1|>=2.8.3,<2.8.4", "dolibarr/dolibarr": "<=23.0.2", - "dompdf/dompdf": "<2.0.4", + "dompdf/dompdf": "<3.1.6", "doublethreedigital/guest-entries": "<3.1.2", "dreamfactory/df-core": "<1.0.4", "drupal-pattern-lab/unified-twig-extensions": "<=0.1", @@ -20426,7 +21200,7 @@ "drupal/commerce_alphabank_redirect": "<1.0.3", "drupal/commerce_eurobank_redirect": "<2.1.1", "drupal/config_split": "<1.10|>=2,<2.0.2", - "drupal/core": ">=6,<6.38|>=7,<7.103|>=8,<10.5.9|>=10.6,<10.6.7|>=11,<11.2.11|>=11.3,<11.3.7", + "drupal/core": ">=6,<6.38|>=7,<7.103|>=8,<10.5.10|>=10.6,<10.6.9|>=11,<11.2.12|>=11.3,<11.3.10", "drupal/core-recommended": ">=7,<7.102|>=8,<10.2.11|>=10.3,<10.3.9|>=11,<11.0.8", "drupal/currency": "<3.5", "drupal/drupal": ">=5,<5.11|>=6,<6.38|>=7,<7.102|>=8,<10.2.11|>=10.3,<10.3.9|>=11,<11.0.8", @@ -20457,7 +21231,7 @@ "ec-cube/ec-cube": "<2.4.4|>=2.11,<=2.17.1|>=3,<=3.0.18.0-patch4|>=4,<=4.3.1", "ecodev/newsletter": "<=4", "ectouch/ectouch": "<=2.7.2", - "egroupware/egroupware": "<23.1.20260113|>=26.0.20251208,<26.0.20260113", + "egroupware/egroupware": "<23.1.20260601|>=26.0.20251208,<26.5.20260507", "elefant/cms": "<2.0.7", "elgg/elgg": "<3.3.24|>=4,<4.0.5", "elijaa/phpmemcacheadmin": "<=1.3", @@ -20492,15 +21266,16 @@ "ezsystems/repository-forms": ">=2.3,<2.3.2.1-dev|>=2.5,<2.5.15", "ezyang/htmlpurifier": "<=4.2", "facade/ignition": "<1.16.15|>=2,<2.4.2|>=2.5,<2.5.2", - "facturascripts/facturascripts": "<=2025.92|>=2026,<=2026.1", + "facturascripts/facturascripts": "<=2026.2", "fastly/magento2": "<1.2.26", "feehi/cms": "<=2.1.1", "feehi/feehicms": "<=2.1.1", "fenom/fenom": "<=2.12.1", - "filament/actions": ">=3.2,<3.2.123", - "filament/filament": ">=4,<4.3.1", - "filament/infolists": ">=3,<3.2.115", - "filament/tables": ">=3,<3.2.115|>=4,<4.8.5|>=5,<5.3.5", + "filament/actions": ">=3.2,<3.2.123|>=4,<=4.11.3|>=5,<=5.6.3", + "filament/filament": ">=3,<=3.3.51|>=4,<4.11.5|>=5,<5.6.5", + "filament/forms": ">=3,<=3.3.52", + "filament/infolists": ">=3,<3.2.115|>=4,<=4.11.4|>=5,<=5.6.4", + "filament/tables": ">=3,<=3.3.50|>=4,<=4.11.4|>=5,<=5.6.4", "filegator/filegator": "<7.8", "filp/whoops": "<2.1.13", "fineuploader/php-traditional-server": "<=1.2.2", @@ -20546,10 +21321,10 @@ "georgringer/news": "<10.0.4|>=11,<11.4.4|>=12,<12.3.2|>=13,<13.0.2|>=14,<14.0.3", "geshi/geshi": "<=1.0.9.1", "getformwork/formwork": "<=2.3.3", - "getgrav/grav": "<=2.0.0.0-RC1", + "getgrav/grav": "<=2.0.0.0-RC8", "getgrav/grav-plugin-api": "<1.0.0.0-beta15", "getgrav/grav-plugin-form": "<9.1", - "getkirby/cms": "<=4.9|>=5,<=5.4", + "getkirby/cms": "<=4.9.3|>=5,<=5.4.3", "getkirby/kirby": "<3.9.8.3-dev|>=3.10,<3.10.1.2-dev|>=4,<4.7.1", "getkirby/panel": "<2.5.14", "getkirby/starterkit": "<=3.7.0.2", @@ -20564,11 +21339,12 @@ "gp247/core": "<1.1.24", "gree/jose": "<2.2.1", "gregwar/rst": "<1.0.3", - "grumpydictator/firefly-iii": "<6.1.17|>=6.4.23,<=6.5", + "grumpydictator/firefly-iii": "<=6.6.2", "gugoan/economizzer": "<=0.9.0.0-beta1", - "guzzlehttp/guzzle": "<6.5.8|>=7,<7.4.5", + "guzzlehttp/guzzle": "<7.15.1", + "guzzlehttp/guzzle-services": "<1.5.4", "guzzlehttp/oauth-subscriber": "<0.8.1", - "guzzlehttp/psr7": "<1.9.1|>=2,<2.4.5", + "guzzlehttp/psr7": "<2.12.3", "haffner/jh_captcha": "<=2.1.3|>=3,<=3.0.2", "handcraftedinthealps/goodby-csv": "<1.4.3", "harvesthq/chosen": "<1.8.7", @@ -20611,7 +21387,7 @@ "inter-mediator/inter-mediator": "==5.5", "intercom/intercom-php": "==5.0.2", "invoiceninja/invoiceninja": "<5.13.4", - "ipl/web": "<=0.13", + "ipl/web": "<=0.10.2|>=0.11,<=0.13", "islandora/crayfish": "<4.1", "islandora/islandora": ">=2,<2.4.1", "ivankristianto/phpwhois": "<=4.3", @@ -20623,6 +21399,7 @@ "jasig/phpcas": "<1.3.3", "jbartels/wec-map": "<3.0.3", "jcbrand/converse.js": "<3.3.3", + "jleehr/canto-saas-api": "<=2", "joedolson/my-calendar": "<3.7.7", "joelbutcher/socialstream": "<5.6|>=6,<6.2", "johnbillion/query-monitor": "<3.20.4", @@ -20649,7 +21426,7 @@ "kelvinmo/simplexrd": "<3.1.1", "kevinpapst/kimai2": "<1.16.7", "khodakhah/nodcms": "<=3.4.1", - "kimai/kimai": "<=2.55", + "kimai/kimai": "<2.59", "kitodo/presentation": "<3.2.3|>=3.3,<3.3.4", "klaviyo/magento2-extension": ">=1,<3", "knplabs/knp-snappy": "<=1.7", @@ -20666,7 +21443,7 @@ "lara-zeus/artemis": ">=1,<=1.0.6", "lara-zeus/dynamic-dashboard": ">=3,<=3.0.1", "laravel/fortify": "<1.11.1", - "laravel/framework": "<12.60|>=13,<13.10", + "laravel/framework": "<12.61.1|>=13,<13.12", "laravel/laravel": ">=5.4,<5.4.22", "laravel/passport": ">=13,<13.7.1", "laravel/pulse": "<1.3.1", @@ -20708,13 +21485,13 @@ "maikuolan/phpmussel": ">=1,<1.6", "mainwp/mainwp": "<=4.4.3.3", "manogi/nova-tiptap": "<=3.2.6", - "mantisbt/mantisbt": "<2.28.2", + "mantisbt/mantisbt": "<=2.28.3", "marcwillmann/turn": "<0.3.3", "markhuot/craftql": "<=1.3.7", "marshmallow/nova-tiptap": "<5.7", "matomo/matomo": "<1.11", "matyhtf/framework": "<3.0.6", - "mautic/core": "<5.2.10|>=6,<6.0.8|>=7.0.0.0-alpha,<7.0.1", + "mautic/core": "<5.2.11|>=6,<6.0.9|>=7,<7.1.2", "mautic/core-lib": ">=1.0.0.0-beta,<4.4.13|>=5.0.0.0-alpha,<5.1.1", "mautic/grapes-js-builder-bundle": ">=4,<4.4.18|>=5,<5.2.9|>=6,<6.0.7", "maximebf/debugbar": "<1.19", @@ -20724,6 +21501,7 @@ "mediawiki/cargo": "<3.8.3", "mediawiki/core": "<1.39.5|==1.40", "mediawiki/data-transfer": ">=1.39,<1.39.11|>=1.41,<1.41.3|>=1.42,<1.42.2", + "mediawiki/maps": "<12.1.3", "mediawiki/matomo": "<2.4.3", "mediawiki/semantic-media-wiki": "<4.0.2", "mehrwert/phpmyadmin": "<3.2", @@ -20757,6 +21535,7 @@ "movim/moxl": ">=0.8,<=0.10", "movingbytes/social-network": "<=1.2.1", "mpdf/mpdf": "<=7.1.7", + "mtdowling/jmespath.php": "<2.9.1", "munkireport/comment": "<4", "munkireport/managedinstalls": "<2.6", "munkireport/munki_facts": "<1.5", @@ -20784,11 +21563,11 @@ "nilsteampassnet/teampass": "<3.1.3.1-dev", "nitsan/ns-backup": "<13.0.1", "nonfiction/nterchange": "<4.1.1", - "notrinos/notrinos-erp": "<=0.7", + "notrinos/notrinos-erp": "<=1", "noumo/easyii": "<=0.9", "novaksolutions/infusionsoft-php-sdk": "<1", "novosga/novosga": "<=2.2.12", - "nukeviet/nukeviet": "<4.5.02", + "nukeviet/nukeviet": "<4.6.00", "nyholm/psr7": "<1.6.1", "nystudio107/craft-seomatic": "<3.4.12", "nzedb/nzedb": "<0.8", @@ -20826,6 +21605,7 @@ "paragonie/random_compat": "<2", "paragonie/sodium_compat": "<1.24|>=2,<2.5", "passbolt/passbolt_api": "<4.6.2", + "paymenter/paymenter": "<=1.5.4", "paypal/adaptivepayments-sdk-php": "<=3.9.2", "paypal/invoice-sdk-php": "<=3.9", "paypal/merchant-sdk-php": "<3.12", @@ -20838,23 +21618,26 @@ "pegasus/google-for-jobs": "<1.5.1|>=2,<2.1.1", "personnummer/personnummer": "<3.0.2", "ph7software/ph7builder": "<=17.9.1", - "phanan/koel": "<=9.3.4", + "phanan/koel": "<=9.7", + "pheditor/pheditor": "<2.0.8", "phenx/php-svg-lib": "<0.5.2", "php-censor/php-censor": "<2.0.13|>=2.1,<2.1.5", "php-mod/curl": "<2.3.2", - "phpbb/phpbb": "<3.3.11", + "php-standard-library/h2": ">=6.1,<6.1.2|>=6.2,<6.2.1", + "php-standard-library/php-standard-library": ">=6.1,<6.1.2|>=6.2,<6.2.1", + "phpbb/phpbb": "<3.3.16|==4.0.0.0-alpha1", "phpems/phpems": ">=6,<=6.1.3", "phpfastcache/phpfastcache": "<6.1.5|>=7,<7.1.2|>=8,<8.0.7", "phpmailer/phpmailer": "<6.5", "phpmussel/phpmussel": ">=1,<1.6", "phpmyadmin/phpmyadmin": "<5.2.2", - "phpmyfaq/phpmyfaq": "<4.1.3", + "phpmyfaq/phpmyfaq": "<4.1.4", "phpoffice/common": "<0.2.9", "phpoffice/math": "<=0.2", "phpoffice/phpexcel": "<=1.8.2", - "phpoffice/phpspreadsheet": "<=1.30.3|>=2,<=2.1.15|>=2.2,<=2.4.4|>=3,<=3.10.4|>=4,<=5.6", + "phpoffice/phpspreadsheet": "<=1.30.5|>=2,<=2.1.17|>=2.2,<=2.4.6|>=3,<=3.10.6|>=4,<=5.8", "phppgadmin/phppgadmin": "<=7.13", - "phpseclib/phpseclib": "<=2.0.53|>=3,<=3.0.51", + "phpseclib/phpseclib": "<=2.0.54|>=3,<=3.0.53", "phpservermon/phpservermon": "<3.6", "phpsysinfo/phpsysinfo": "<3.4.3", "phpunit/phpunit": "<8.5.52|>=9,<9.6.33|>=10,<10.5.62|>=11,<11.5.50|>=12,<12.5.8|>=12.5.21,<12.5.22|>=13.1.5,<13.1.6", @@ -20863,14 +21646,14 @@ "phpxmlrpc/phpxmlrpc": "<4.9.2", "phraseanet/phraseanet": "==4.0.3", "pi/pi": "<=2.5", - "pimcore/admin-ui-classic-bundle": "<=2.3.5", + "pimcore/admin-ui-classic-bundle": "<1.7.18|>=2.0.0.0-RC1-dev,<=2.3.5", "pimcore/customer-management-framework-bundle": "<4.2.1", "pimcore/data-hub": "<1.2.4", "pimcore/data-importer": "<1.8.9|>=1.9,<1.9.3", "pimcore/demo": "<10.3", "pimcore/ecommerce-framework-bundle": "<1.0.10", "pimcore/perspective-editor": "<1.5.1", - "pimcore/pimcore": "<=12.3.6", + "pimcore/pimcore": "<=12.3.8|>=2026.1,<2026.1.3", "pimcore/web2print-tools-bundle": "<=5.2.1|>=6.0.0.0-RC1-dev,<=6.1", "piwik/piwik": "<1.11", "pixelfed/pixelfed": "<0.12.5", @@ -20878,6 +21661,8 @@ "pocketmine/bedrock-protocol": "<8.0.2", "pocketmine/pocketmine-mp": "<5.42.1", "pocketmine/raklib": ">=0.14,<0.14.6|>=0.15,<0.15.1", + "pontedilana/php-weasyprint": "<=2.5.1", + "poweradmin/poweradmin": "<4.2.5|>=4.3,<4.3.4", "pressbooks/pressbooks": "<5.18", "prestashop/autoupgrade": ">=4,<4.10.1", "prestashop/blockreassurance": "<=5.1.3", @@ -20889,12 +21674,12 @@ "prestashop/ps_checkout": "<5.3", "prestashop/ps_contactinfo": "<=3.3.2", "prestashop/ps_emailsubscription": "<2.6.1", - "prestashop/ps_facetedsearch": "<3.4.1", + "prestashop/ps_facetedsearch": "<4.0.4", "prestashop/ps_linklist": "<3.1", "privatebin/privatebin": "<1.4|>=1.5,<1.7.4|>=1.7.7,<2.0.3", "processwire/processwire": "<=3.0.255", - "propel/propel": ">=2.0.0.0-alpha1,<=2.0.0.0-alpha7", - "propel/propel1": ">=1,<=1.7.1", + "propel/propel": ">=2.0.0.0-alpha1,<2.0.0.0-alpha8", + "propel/propel1": ">=1,<1.7.2", "psy/psysh": "<=0.11.22|>=0.12,<=0.12.18", "pterodactyl/panel": "<1.12.3", "ptheofan/yii2-statemachine": ">=2.0.0.0-RC1-dev,<=2", @@ -20947,7 +21732,7 @@ "shopware/core": "<6.6.10.18-dev|>=6.7,<6.7.10.1-dev", "shopware/platform": "<6.6.10.18-dev|>=6.7,<6.7.10.1-dev", "shopware/production": "<=6.3.5.2", - "shopware/shopware": "<=5.7.17|>=6.4.6,<6.6.10.10-dev|>=6.7,<6.7.6.1-dev", + "shopware/shopware": "<=6.3.5.2|>=6.4.6,<6.6.10.10-dev|>=6.7,<6.7.6.1-dev", "shopware/storefront": "<6.6.10.10-dev|>=6.7,<6.7.5.1-dev", "shopxo/shopxo": "<=6.4", "showdoc/showdoc": "<3.8.1", @@ -20955,10 +21740,10 @@ "silverstripe-australia/advancedreports": ">=1,<=2", "silverstripe/admin": "<1.13.19|>=2,<2.1.8", "silverstripe/assets": "<2.4.5|>=3,<3.1.3", - "silverstripe/cms": "<4.11.3", + "silverstripe/cms": "<6.2.1", "silverstripe/comments": ">=1.3,<3.1.1", - "silverstripe/forum": "<=0.6.1|>=0.7,<=0.7.3", - "silverstripe/framework": "<5.3.23", + "silverstripe/forum": "<0.6.2|>=0.7,<0.7.4", + "silverstripe/framework": "<6.2.2", "silverstripe/graphql": ">=2,<2.0.5|>=3,<3.8.2|>=4,<4.3.7|>=5,<5.1.3", "silverstripe/hybridsessions": ">=1,<2.4.1|>=2.5,<2.5.1", "silverstripe/recipe-cms": ">=4.5,<4.5.3", @@ -20968,13 +21753,14 @@ "silverstripe/silverstripe-omnipay": "<2.5.2|>=3,<3.0.2|>=3.1,<3.1.4|>=3.2,<3.2.1", "silverstripe/subsites": ">=2,<2.6.1", "silverstripe/taxonomy": ">=1.3,<1.3.1|>=2,<2.0.1", - "silverstripe/userforms": "<3|>=5,<5.4.2", + "silverstripe/userforms": "<6.4.9|>=7,<7.0.7|>=7.1,<7.1.1", + "silverstripe/versioned": "<3.2.1", "silverstripe/versioned-admin": ">=1,<1.11.1", "simogeo/filemanager": "<=2.5", "simple-updates/phpwhois": "<=1", - "simplesamlphp/saml2": "<=4.16.15|>=5.0.0.0-alpha1,<=5.0.0.0-alpha19", - "simplesamlphp/saml2-legacy": "<=4.16.15", - "simplesamlphp/simplesamlphp": "<1.18.6", + "simplesamlphp/saml2": "<=4.20.2|>=5,<5.0.6|>=6,<6.2.1", + "simplesamlphp/saml2-legacy": "<=4.20.2", + "simplesamlphp/simplesamlphp": "<=2.4.6|>=2.5,<=2.5.1", "simplesamlphp/simplesamlphp-module-casserver": "<=7.0.2", "simplesamlphp/simplesamlphp-module-infocard": "<1.0.1", "simplesamlphp/simplesamlphp-module-openid": "<1", @@ -20987,20 +21773,23 @@ "sjbr/sr-freecap": "<2.4.6|>=2.5,<2.5.3", "sjbr/static-info-tables": "<2.3.1", "slim/psr7": "<1.4.1|>=1.5,<1.5.1|>=1.6,<1.6.1", - "slim/slim": "<2.6", + "slim/slim": "<2.6|>=4.4,<=4.15.1", "slub/slub-events": "<3.0.3", "smarty/smarty": "<4.5.3|>=5,<5.1.1", - "snipe/snipe-it": "<8.4.1", + "snipe/snipe-it": "<=8.6.1", "socalnick/scn-social-auth": "<1.15.2", "socialiteproviders/steam": "<1.1", + "solidinvoice/solidinvoice": "<=2.3.15", "solspace/craft-freeform": "<4.1.29|>=5,<=5.14.6", "soosyze/soosyze": "<=2", "spatie/browsershot": "<5.0.5", "spatie/image-optimizer": "<1.7.3", + "spatie/laravel-medialibrary": "<11.23", "spatie/schema-org": ">=3.23.1,<3.23.2|>=4,<4.0.2", "spencer14420/sp-php-email-handler": "<1", "spipu/html2pdf": "<5.2.8", "spiral/roadrunner": "<2025.1", + "spomky-labs/otphp": "<11.4.3", "spoon/library": "<1.4.1", "spoonity/tcpdf": "<6.2.22", "squizlabs/php_codesniffer": ">=1,<2.8.1|>=3,<3.0.1", @@ -21009,7 +21798,7 @@ "starcitizentools/short-description": ">=4,<4.0.1", "starcitizentools/tabber-neue": ">=1.9.1,<2.7.2|>=3,<3.1.1", "starcitizenwiki/embedvideo": "<=4", - "statamic/cms": "<5.73.22|>=6,<6.18.1", + "statamic/cms": "<5.74|>=6,<6.20.3", "stormpath/sdk": "<9.9.99", "studio-42/elfinder": "<=2.1.67", "studiomitte/friendlycaptcha": "<0.1.4", @@ -21028,7 +21817,8 @@ "sylius/grid-bundle": "<1.10.1", "sylius/paypal-plugin": "<1.6.2|>=1.7,<1.7.2|>=2,<2.0.2", "sylius/resource-bundle": ">=1,<1.3.14|>=1.4,<1.4.7|>=1.5,<1.5.2|>=1.6,<1.6.4", - "sylius/sylius": "<1.9.12|>=1.10,<1.10.16|>=1.11,<1.11.17|>=1.12,<=1.12.22|>=1.13,<=1.13.14|>=1.14,<=1.14.17|>=2,<=2.0.15|>=2.1,<=2.1.11|>=2.2,<=2.2.2", + "sylius/sylius": "<1.9.12|>=1.10,<1.10.16|>=1.11,<1.11.17|>=1.12,<=1.12.22|>=1.13,<=1.13.14|>=1.14,<=1.14.17|>=2,<2.0.18|>=2.1,<2.1.15|>=2.2,<2.2.6", + "symbiote/silverstripe-advancedworkflow": "<6.4.5|>=7,<7.1.3|>=7.2,<7.2.1", "symbiote/silverstripe-multivaluefield": ">=3,<3.1", "symbiote/silverstripe-queuedjobs": ">=3,<3.0.2|>=3.1,<3.1.4|>=4,<4.0.7|>=4.1,<4.1.2|>=4.2,<4.2.4|>=4.3,<4.3.3|>=4.4,<4.4.3|>=4.5,<4.5.1|>=4.6,<4.6.4", "symbiote/silverstripe-seed": "<6.0.3", @@ -21074,7 +21864,9 @@ "symfony/twig-bridge": ">=2,<4.4.51|>=5,<5.4.31|>=6,<6.3.8|>=6.4.24,<6.4.40", "symfony/twilio-notifier": ">=6.4,<6.4.40|>=7,<7.4.12|>=8,<8.0.12", "symfony/ux-autocomplete": "<2.36|>=3,<3.1", + "symfony/ux-icons": ">=2.17,<2.36.1|>=3,<3.2", "symfony/ux-live-component": "<2.36|>=3,<3.1", + "symfony/ux-toolkit": ">=2.32,<2.36.1|>=3,<3.2", "symfony/ux-twig-component": "<2.25.1", "symfony/validator": "<5.4.43|>=6,<6.4.11|>=7,<7.1.4", "symfony/var-exporter": ">=4.2,<4.2.12|>=4.3,<4.3.8", @@ -21091,10 +21883,10 @@ "tecnickcom/tcpdf": "<6.8", "terminal42/contao-tablelookupwizard": "<3.3.5", "thelia/backoffice-default-template": ">=2.1,<2.1.2", - "thelia/thelia": ">=2.1,<2.1.3", + "thelia/thelia": ">=2.0.0.0-beta1,<2.1.3", "theonedemon/phpwhois": "<=4.2.5", "thinkcmf/thinkcmf": "<6.0.8", - "thorsten/phpmyfaq": "<4.1.3", + "thorsten/phpmyfaq": "<4.1.4", "tikiwiki/tiki-manager": "<=17.1", "timber/timber": ">=0.16.6,<1.23.1|>=1.24,<1.24.1|>=2,<2.1", "tinymce/tinymce": "<7.9.3|>=8,<8.5.1", @@ -21116,24 +21908,25 @@ "twig/intl-extra": "<3.26", "twig/markdown-extra": "<3.26", "twig/twig": "<3.27", - "typicms/core": "<16.1.7", + "typicms/core": "<12.0.5|>=13,<13.0.9|>=14,<14.0.27|>=15,<15.0.29|>=16,<16.1.7", "typo3/cms": "<9.5.29|>=10,<10.4.35|>=11,<11.5.23|>=12,<12.2", - "typo3/cms-backend": "<4.1.14|>=4.2,<4.2.15|>=4.3,<4.3.7|>=4.4,<4.4.4|>=7,<=7.6.50|>=8,<=8.7.39|>=9,<9.5.55|>=10,<=10.4.54|>=11,<=11.5.48|>=12,<=12.4.40|>=13,<=13.4.22|>=14,<=14.0.1|==14.2", + "typo3/cms-backend": "<10.4.57|>=11,<11.5.51|>=12,<12.4.46|>=13,<13.4.31|>=14,<14.3.3", "typo3/cms-belog": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2", "typo3/cms-beuser": ">=9,<9.5.55|>=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18", - "typo3/cms-core": "<=8.7.56|>=9,<9.5.55|>=10,<=10.4.54|>=11,<=11.5.48|>=12,<=12.4.40|>=13,<=13.4.22|>=14,<=14.0.1", + "typo3/cms-core": "<10.4.57|>=11,<11.5.51|>=12,<12.4.46|>=13,<13.4.31|>=14,<14.3.3", "typo3/cms-dashboard": ">=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18", "typo3/cms-extbase": "<6.2.24|>=7,<7.6.8|==8.1.1", "typo3/cms-extensionmanager": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2", "typo3/cms-felogin": ">=4.2,<4.2.3", - "typo3/cms-fluid": "<4.3.4|>=4.4,<4.4.1", - "typo3/cms-form": ">=8,<=8.7.39|>=9,<=9.5.24|>=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2", + "typo3/cms-filelist": ">=11,<11.5.51|>=12,<12.4.46|>=13,<13.4.31|>=14,<14.3.3", + "typo3/cms-fluid": "<4.3.4|>=4.4,<4.4.1|>=8,<8.7.23|>=9,<9.5.4", + "typo3/cms-form": "<10.4.57|>=11,<11.5.51|>=12,<12.4.46|>=13,<13.4.31|>=14,<14.3.5", "typo3/cms-frontend": "<4.3.9|>=4.4,<4.4.5", - "typo3/cms-indexed-search": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2", + "typo3/cms-indexed-search": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<13.4.31|>=14,<14.3.3", "typo3/cms-install": "<4.1.14|>=4.2,<4.2.16|>=4.3,<4.3.9|>=4.4,<4.4.5|>=12.2,<12.4.8|==13.4.2", "typo3/cms-lowlevel": ">=11,<=11.5.41", "typo3/cms-recordlist": ">=11,<11.5.48", - "typo3/cms-recycler": ">=9,<9.5.55|>=10,<=10.4.54|>=11,<=11.5.48|>=12,<=12.4.40|>=13,<=13.4.22|>=14,<=14.0.1", + "typo3/cms-recycler": "<10.4.57|>=11,<11.5.51|>=12,<12.4.46|>=13,<13.4.31|>=14,<14.3.3", "typo3/cms-redirects": ">=10,<=10.4.54|>=11,<=11.5.48|>=12,<=12.4.40|>=13,<=13.4.22|>=14,<=14.0.1", "typo3/cms-rte-ckeditor": ">=9.5,<9.5.42|>=10,<10.4.39|>=11,<11.5.30", "typo3/cms-scheduler": ">=11,<=11.5.41", @@ -21141,7 +21934,7 @@ "typo3/cms-webhooks": ">=12,<=12.4.30|>=13,<=13.4.11", "typo3/cms-workspaces": ">=9,<9.5.55|>=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18", "typo3/flow": ">=1,<1.0.4|>=1.1,<1.1.1|>=2,<2.0.1|>=2.3,<2.3.16|>=3,<3.0.12|>=3.1,<3.1.10|>=3.2,<3.2.13|>=3.3,<3.3.13|>=4,<4.0.6", - "typo3/html-sanitizer": ">=1,<=1.5.2|>=2,<=2.1.3", + "typo3/html-sanitizer": "<2.3.2", "typo3/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4|>=2.3,<2.3.99|>=3,<3.0.20|>=3.1,<3.1.18|>=3.2,<3.2.14|>=3.3,<3.3.23|>=4,<4.0.17|>=4.1,<4.1.16|>=4.2,<4.2.12|>=4.3,<4.3.3", "typo3/phar-stream-wrapper": ">=1,<2.1.1|>=3,<3.1.1", "typo3/swiftmailer": ">=4.1,<4.1.99|>=5.4,<5.4.5", @@ -21157,7 +21950,7 @@ "uvdesk/core-framework": "<=1.1.1", "vanilla/safecurl": "<0.9.2", "verbb/comments": "<1.5.5", - "verbb/formie": "<2.2.21|>=3,<3.1.26", + "verbb/formie": "<3.1.28", "verbb/image-resizer": "<2.0.9", "verbb/knock-knock": "<1.2.8", "verot/class.upload.php": "<=2.1.6", @@ -21172,9 +21965,13 @@ "wanglelecc/laracms": "<=1.0.3", "wapplersystems/a21glossary": "<=0.4.10", "web-auth/webauthn-framework": ">=3.3,<3.3.4|>=4.5,<4.9|>=5.2,<5.2.4|>=5.3,<5.3.1", - "web-auth/webauthn-lib": ">=4.5,<4.9|>=5.2,<5.2.4", - "web-auth/webauthn-symfony-bundle": ">=5.2,<5.2.4", + "web-auth/webauthn-lib": ">=4.5,<5.3.5", + "web-auth/webauthn-symfony-bundle": "<5.3.4", "web-feet/coastercms": "==5.5", + "web-token/jwt-bundle": "<3.4.10|>=4,<4.0.7|>=4.1,<4.1.7", + "web-token/jwt-experimental": "<4.1.7", + "web-token/jwt-framework": "<4.1.7", + "web-token/jwt-library": "<3.4.10|>=4,<4.0.7|>=4.1,<4.1.7", "web-tp3/wec_map": "<3.0.3", "webbuilders-group/silverstripe-kapost-bridge": "<0.4", "webcoast/deferred-image-processing": "<1.0.2", @@ -21192,6 +21989,7 @@ "winter/wn-system-module": "<1.2.4", "wintercms/winter": "<=1.2.3", "wireui/wireui": "<1.19.3|>=2,<2.1.3", + "wnx/laravel-backup-restore": "<=1.9.3", "woocommerce/woocommerce": "<6.6|>=8.8,<8.8.5|>=8.9,<8.9.3", "wp-cli/wp-cli": ">=0.12,<2.5", "wp-graphql/wp-graphql": "<=1.14.5", @@ -21205,7 +22003,7 @@ "xpressengine/xpressengine": "<3.0.15", "yab/quarx": "<2.4.5", "yansongda/pay": "<=3.7.19", - "yeswiki/yeswiki": "<4.6.4", + "yeswiki/yeswiki": "<4.6.6", "yetiforce/yetiforce-crm": "<6.5", "yidashi/yii2cmf": "<=2", "yii2mod/yii2-cms": "<1.9.2", @@ -21300,7 +22098,7 @@ "type": "tidelift" } ], - "time": "2026-06-05T20:39:10+00:00" + "time": "2026-07-24T23:00:42+00:00" }, { "name": "sebastian/cli-parser", @@ -22342,16 +23140,16 @@ }, { "name": "symfony/browser-kit", - "version": "v7.4.8", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", - "reference": "41850d8f8ddef9a9cd7314fa9f4902cf48885521" + "reference": "bb28e8761a6c33975972948010f00d4a10f0a634" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/41850d8f8ddef9a9cd7314fa9f4902cf48885521", - "reference": "41850d8f8ddef9a9cd7314fa9f4902cf48885521", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/bb28e8761a6c33975972948010f00d4a10f0a634", + "reference": "bb28e8761a6c33975972948010f00d4a10f0a634", "shasum": "" }, "require": { @@ -22391,7 +23189,7 @@ "description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/browser-kit/tree/v7.4.8" + "source": "https://github.com/symfony/browser-kit/tree/v7.4.14" }, "funding": [ { @@ -22411,7 +23209,7 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-06-08T20:24:16+00:00" }, { "name": "symfony/debug-bundle", @@ -22589,16 +23387,16 @@ }, { "name": "symfony/phpunit-bridge", - "version": "v7.4.8", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/phpunit-bridge.git", - "reference": "140bbbe1cd1c21a084494ccddeee33f3c3381d7d" + "reference": "11eeee9d109963145e66f5b1919e5cf5411da58b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/140bbbe1cd1c21a084494ccddeee33f3c3381d7d", - "reference": "140bbbe1cd1c21a084494ccddeee33f3c3381d7d", + "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/11eeee9d109963145e66f5b1919e5cf5411da58b", + "reference": "11eeee9d109963145e66f5b1919e5cf5411da58b", "shasum": "" }, "require": { @@ -22650,7 +23448,7 @@ "testing" ], "support": { - "source": "https://github.com/symfony/phpunit-bridge/tree/v7.4.8" + "source": "https://github.com/symfony/phpunit-bridge/tree/v7.4.14" }, "funding": [ { @@ -22670,20 +23468,20 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-06-08T20:24:16+00:00" }, { "name": "symfony/web-profiler-bundle", - "version": "v7.4.13", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/web-profiler-bundle.git", - "reference": "153076bb3f0690fff0e95e55cc06358b22f236a5" + "reference": "5dead36a9202a6008b54b95308bce7ab97a41fe0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/153076bb3f0690fff0e95e55cc06358b22f236a5", - "reference": "153076bb3f0690fff0e95e55cc06358b22f236a5", + "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/5dead36a9202a6008b54b95308bce7ab97a41fe0", + "reference": "5dead36a9202a6008b54b95308bce7ab97a41fe0", "shasum": "" }, "require": { @@ -22740,7 +23538,7 @@ "dev" ], "support": { - "source": "https://github.com/symfony/web-profiler-bundle/tree/v7.4.13" + "source": "https://github.com/symfony/web-profiler-bundle/tree/v7.4.14" }, "funding": [ { @@ -22760,7 +23558,7 @@ "type": "tidelift" } ], - "time": "2026-05-23T16:05:06+00:00" + "time": "2026-06-05T06:22:21+00:00" }, { "name": "theseer/tokenizer", @@ -22813,7 +23611,14 @@ "time": "2025-11-17T20:03:58+00:00" } ], - "aliases": [], + "aliases": [ + { + "package": "mcp/sdk", + "version": "0.7.0.0", + "alias": "v0.6.0", + "alias_normalized": "0.6.0.0" + } + ], "minimum-stability": "stable", "stability-flags": { "roave/security-advisories": 20 diff --git a/config/bundles.php b/config/bundles.php index 000c58a1..830bab91 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -34,4 +34,5 @@ return [ Jbtronics\TranslationEditorBundle\JbtronicsTranslationEditorBundle::class => ['dev' => true], ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true], Symfony\AI\AiBundle\AiBundle::class => ['all' => true], + Symfony\AI\McpBundle\McpBundle::class => ['all' => true], ]; diff --git a/config/packages/ai_lm_studio_platform.yaml b/config/packages/ai_lm_studio_platform.yaml index 0e4287e0..1d832763 100644 --- a/config/packages/ai_lm_studio_platform.yaml +++ b/config/packages/ai_lm_studio_platform.yaml @@ -2,3 +2,4 @@ ai: platform: lmstudio: host_url: '%env(string:settings:ai_lmstudio:hostURL)%' + http_client: 'app.http_client.ai_lmstudio' diff --git a/config/packages/ai_ollama_platform.yaml b/config/packages/ai_ollama_platform.yaml new file mode 100644 index 00000000..67ebe190 --- /dev/null +++ b/config/packages/ai_ollama_platform.yaml @@ -0,0 +1,6 @@ +ai: + platform: + ollama: + endpoint: '%env(string:settings:ai_ollama:endpoint)%' + api_key: '%env(string:settings:ai_ollama:apiKey)%' + http_client: 'app.http_client.ai_ollama' diff --git a/config/packages/ai_open_router_platform.yaml b/config/packages/ai_open_router_platform.yaml index d34de592..53eb20b9 100644 --- a/config/packages/ai_open_router_platform.yaml +++ b/config/packages/ai_open_router_platform.yaml @@ -2,3 +2,4 @@ ai: platform: openrouter: api_key: '%env(string:settings:ai_openrouter:apiKey)%' + http_client: 'app.http_client.ai_openrouter' diff --git a/config/packages/api_platform.yaml b/config/packages/api_platform.yaml index 1b679cd1..d706f40e 100644 --- a/config/packages/api_platform.yaml +++ b/config/packages/api_platform.yaml @@ -38,3 +38,7 @@ api_platform: serializer: # Change this to false later, to remove the hydra prefix on the API hydra_prefix: true + + mcp: + enabled: true # default: true + format: jsonld # default: 'jsonld' diff --git a/config/packages/dev/easy_log_handler.yaml b/config/packages/dev/easy_log_handler.yaml deleted file mode 100644 index 27bfc608..00000000 --- a/config/packages/dev/easy_log_handler.yaml +++ /dev/null @@ -1,16 +0,0 @@ -services: - EasyCorp\EasyLog\EasyLogHandler: - public: false - arguments: ['%kernel.logs_dir%/%kernel.environment%.log'] - -#// FIXME: How to add this configuration automatically without messing up with the monolog configuration? -#monolog: -# handlers: -# buffered: -# type: buffer -# handler: easylog -# channels: ['!event'] -# level: debug -# easylog: -# type: service -# id: EasyCorp\EasyLog\EasyLogHandler diff --git a/config/packages/doctrine.php b/config/packages/doctrine.php index e5be011f..f62cdcfb 100644 --- a/config/packages/doctrine.php +++ b/config/packages/doctrine.php @@ -20,16 +20,16 @@ declare(strict_types=1); -use Symfony\Config\DoctrineConfig; - /** - * This class extends the default doctrine ORM configuration to enable native lazy objects on PHP 8.4+. + * This file enables native lazy objects on PHP 8.4+. * We have to do this in a PHP file, because the yaml file does not support conditionals on PHP version. + * + * TODO: Remove this file when we drop support for PHP < 8.4 */ -return static function(DoctrineConfig $doctrine) { - //On PHP 8.4+ we can use native lazy objects, which are much more efficient than proxies. - if (PHP_VERSION_ID >= 80400) { - $doctrine->orm()->enableNativeLazyObjects(true); - } -}; +// On PHP 8.4+ we can use native lazy objects, which are much more efficient than proxies. +if (PHP_VERSION_ID >= 80400) { + return ['doctrine' => ['orm' => ['enable_native_lazy_objects' => true]]]; +} + +return []; diff --git a/config/packages/mcp.yaml b/config/packages/mcp.yaml new file mode 100644 index 00000000..4c2a9ea9 --- /dev/null +++ b/config/packages/mcp.yaml @@ -0,0 +1,18 @@ +mcp: + app: "Part-DB" + version: "0.1.0" + description: "Part-DB is a inventory management database for electronic parts." + instructions: | + This server provides inventory information for your current user. It is mostly used for electronic parts, + but can be used for any kind of inventory. The stored objects are called parts. + If you miss some information from an search endpoint, try to retrieve more details using the ID of an entity. + + client_transports: + http: true + stdio: false + http: + path: "/mcp" + session: + store: "file" + directory: "%kernel.cache_dir%/mcp" + ttl: 3600 diff --git a/config/packages/monolog.yaml b/config/packages/monolog.yaml index 387d71ad..17f8f4c2 100644 --- a/config/packages/monolog.yaml +++ b/config/packages/monolog.yaml @@ -51,6 +51,7 @@ when@prod: type: stream channels: [deprecation] path: "%kernel.logs_dir%/%kernel.environment%_deprecations.log" + level: "%env(DEPRECATION_LOG_LEVEL)%" when@docker: monolog: @@ -75,3 +76,4 @@ when@docker: type: stream channels: [deprecation] path: "%kernel.logs_dir%/%kernel.environment%_deprecations.log" + level: "%env(DEPRECATION_LOG_LEVEL)%" diff --git a/config/packages/security.yaml b/config/packages/security.yaml index e7a44e0c..d9bf0400 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -73,3 +73,6 @@ security: - { path: "^/api", allow_if: 'is_granted("@api.access_api") and is_authenticated()' } # Restrict access to KICAD to users, which has API access permission - { path: "^/kicad-api", allow_if: 'is_granted("@api.access_api") and is_authenticated()' } + + # Restrict MCP access to users, which has the MCP access permission + - { path: "^/mcp", allow_if: 'is_granted("@api.use_mcp") and is_authenticated()' } diff --git a/config/parameters.yaml b/config/parameters.yaml index b1aa5314..64c791c1 100644 --- a/config/parameters.yaml +++ b/config/parameters.yaml @@ -8,7 +8,7 @@ parameters: # This is used as workaround for places where we can not access the settings directly (like the 2FA application names) partdb.title: '%env(string:settings:customization:instanceName)%' # The title shown inside of Part-DB (e.g. in the navbar and on homepage) - partdb.locale_menu: ['en', 'de', 'it', 'fr', 'ru', 'ja', 'cs', 'da', 'zh', 'pl', 'hu'] # The languages that are shown in user drop down menu + partdb.locale_menu: ['en', 'de', 'it', 'fr', 'ru', 'ja', 'cs', 'da', 'zh', 'pl', 'hu', 'ko'] # The languages that are shown in user drop down menu partdb.default_uri: '%env(addSlash:string:DEFAULT_URI)%' # The default URI to use for the Part-DB instance (e.g. https://part-db.example.com/). This is used for generating links in emails @@ -53,6 +53,7 @@ parameters: # Themes commented here by default, are not really usable, because of display problems. Enable them at your own risk! partdb.available_themes: - bootstrap + - brite - cerulean - cosmo - cyborg diff --git a/config/permissions.yaml b/config/permissions.yaml index 39e91b57..704da880 100644 --- a/config/permissions.yaml +++ b/config/permissions.yaml @@ -381,7 +381,12 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co access_api: label: "perm.api.access_api" apiTokenRole: ROLE_API_READ_ONLY + use_mcp: + label: "perm.api.use_mcp" + alsoSet: [ 'access_api' ] + apiTokenRole: ROLE_API_READ_ONLY manage_tokens: label: "perm.api.manage_tokens" alsoSet: ['access_api'] apiTokenRole: ROLE_API_FULL + diff --git a/config/reference.php b/config/reference.php index 49c16f65..0d2478a1 100644 --- a/config/reference.php +++ b/config/reference.php @@ -121,7 +121,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * } * @psalm-type ServicesConfig = array{ * _defaults?: DefaultsType, - * _instanceof?: InstanceofType, + * _instanceof?: array, * ... * } * @psalm-type ExtensionType = array @@ -653,7 +653,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * time_based_uuid_node?: scalar|Param|null, * }, * html_sanitizer?: bool|array{ // HtmlSanitizer configuration - * enabled?: bool|Param, // Default: false + * enabled?: bool|Param, // Default: true * sanitizers?: array, @@ -3185,6 +3199,37 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * store?: string|Param, // Service name of store // Default: "Symfony\\AI\\Store\\StoreInterface" * }>, * } + * @psalm-type McpConfig = array{ + * app?: scalar|Param|null, // Default: "app" + * version?: scalar|Param|null, // Default: "0.0.1" + * description?: scalar|Param|null, // Default: null + * icons?: list, + * }>, + * website_url?: scalar|Param|null, // Default: null + * pagination_limit?: int|Param, // Default: 50 + * instructions?: scalar|Param|null, // Default: null + * client_transports?: array{ + * stdio?: bool|Param, // Default: false + * http?: bool|Param, // Default: false + * }, + * apps?: array{ // MCP Apps support (interactive HTML UI resources). Apps are registered with the #[AsMcpApp] attribute. + * enabled?: bool|Param|null, // Default: null + * }, + * http?: array{ + * path?: scalar|Param|null, // Default: "/_mcp" + * allowed_hosts?: mixed, // DNS rebinding protection hosts (without port). Leave unset to keep the SDK default (localhost only), set an array of hostnames to expose a public MCP server, or false to disable the protection entirely. // Default: null + * session?: array{ + * store?: "file"|"memory"|"cache"|"framework"|Param, // Default: "file" + * directory?: scalar|Param|null, // Default: "%kernel.cache_dir%/mcp-sessions" + * cache_pool?: scalar|Param|null, // Default: "cache.mcp.sessions" + * prefix?: scalar|Param|null, // Default: "mcp-" + * ttl?: int|Param, // Default: 3600 + * }, + * }, + * } * @psalm-type ConfigType = array{ * imports?: ImportsConfig, * parameters?: ParametersConfig, @@ -3215,6 +3260,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * jbtronics_settings?: JbtronicsSettingsConfig, * api_platform?: ApiPlatformConfig, * ai?: AiConfig, + * mcp?: McpConfig, * "when@dev"?: array{ * imports?: ImportsConfig, * parameters?: ParametersConfig, @@ -3249,6 +3295,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * jbtronics_translation_editor?: JbtronicsTranslationEditorConfig, * api_platform?: ApiPlatformConfig, * ai?: AiConfig, + * mcp?: McpConfig, * }, * "when@docker"?: array{ * imports?: ImportsConfig, @@ -3280,6 +3327,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * jbtronics_settings?: JbtronicsSettingsConfig, * api_platform?: ApiPlatformConfig, * ai?: AiConfig, + * mcp?: McpConfig, * }, * "when@prod"?: array{ * imports?: ImportsConfig, @@ -3311,6 +3359,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * jbtronics_settings?: JbtronicsSettingsConfig, * api_platform?: ApiPlatformConfig, * ai?: AiConfig, + * mcp?: McpConfig, * }, * "when@test"?: array{ * imports?: ImportsConfig, @@ -3345,6 +3394,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * jbtronics_settings?: JbtronicsSettingsConfig, * api_platform?: ApiPlatformConfig, * ai?: AiConfig, + * mcp?: McpConfig, * }, * ... This feature was added recently and might still change in future versions. + +Part-DB ships a [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server, which allows AI assistants and +agents (like Claude, ChatGPT, or AI-powered coding tools) to directly interact with your Part-DB inventory: they can +search for parts, look up categories, footprints, manufacturers, storage locations, suppliers, and projects, and even +query external info providers like Digikey, Mouser or LCSC, all using natural language, without you having to write +any code against the [REST API]({% link api/intro.md %}). + +MCP is a standardized, widely supported protocol, so once your Part-DB MCP endpoint is set up, you can connect it to +many different AI clients and applications. + +{: .warning } +> The MCP integration is currently **read-only**: an AI assistant can look up data, but it can not create, change or +> delete anything in your Part-DB instance via MCP. +> Still, giving an AI assistant access to your inventory means it can read everything the connected user account is +> allowed to see, so only connect trusted AI clients and keep your API token secret, just like you would for the +> [REST API]({% link api/authentication.md %}). + +## Enabling the MCP server + +The MCP server is disabled by default and has to be enabled by an administrator first: + +1. Open the system settings and go to the **AI** tab. +2. In the **MCP (Model Context Protocol) Server** section, enable the **Enable MCP endpoint** checkbox. + +This can also be controlled via the `MCP_ENABLED` environment variable. + +Once enabled, the MCP server is reachable under the `/mcp` path of your Part-DB instance (e.g. +`https://your-part-db.local/mcp`). Unlike most other Part-DB pages, this path is **not** locale-prefixed (so it is +`/mcp`, not `/en/mcp`). + +## Permissions + +Users which should be allowed to use the MCP tools additionally need the **Use MCP tools (for AI agents)** permission +(under the **API** permission group). Granting it automatically also grants the base **Access API** permission. + +Like the REST API, authentication against the MCP endpoint is done using an [API token]({% link +api/authentication.md %}). A token with the **Read-Only** scope is enough to use all currently available MCP tools, +as they only read data. + +## Connecting an AI client + +To connect an AI client to Part-DB, you need two things: + +* The **MCP endpoint URL**, e.g. `https://your-part-db.local/mcp`. Once you have the required permission, you can + also find it on the **API** panel of your user settings page, under "MCP endpoint", together with a + copy-to-clipboard button. +* An **API token**. Create one on the same **API** panel of your user settings page (see + [Authentication]({% link api/authentication.md %}) for details about tokens and scopes). A token with the + **Read-Only** scope is sufficient. + +The client has to send this token as a bearer token in the `Authorization` header of every request: +`Authorization: Bearer tcp_`. How exactly you configure this depends on the AI client you use; some +examples for common clients are shown below. + +{: .note } +> Part-DB's MCP server only supports the **Streamable HTTP** transport (no stdio, no plain SSE). Most modern MCP +> clients support this transport directly. Clients that only support local, stdio-based MCP servers can be bridged to +> a remote HTTP server with a small proxy tool like [`mcp-remote`](https://www.npmjs.com/package/mcp-remote), as shown +> in the Claude Desktop example below. + +MCP client configuration formats change frequently, so if the examples below don't quite match what you see in your +client, check the client's own documentation for how to add a remote MCP server with a custom `Authorization` header. + +### Claude Code + +Add the server with the Claude Code CLI: + +```bash +claude mcp add part-db https://your-part-db.local/mcp \ + --transport http \ + --header "Authorization: Bearer tcp_" +``` + +### Claude Desktop + +Claude Desktop currently only launches local (stdio) MCP servers directly from its config file, so a remote server +like Part-DB's has to be bridged with the [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) proxy. Open +Claude Desktop's configuration file (**Settings → Developer → Edit Config**) and add: + +```json +{ + "mcpServers": { + "part-db": { + "command": "npx", + "args": [ + "-y", + "mcp-remote", + "https://your-part-db.local/mcp", + "--header", + "Authorization: Bearer tcp_" + ] + } + } +} +``` + + +### Google Antigravity + +Open **Manage MCP Servers** and add the server via its JSON configuration: + +```json +{ + "mcpServers": { + "part-db": { + "serverUrl": "https://your-part-db.local/mcp", + "headers": { + "Authorization": "Bearer tcp_" + } + } + } +} +``` + +### Cursor + +Add the following to your `.cursor/mcp.json` (project-specific) or global Cursor MCP settings: + +```json +{ + "mcpServers": { + "part-db": { + "url": "https://your-part-db.local/mcp", + "headers": { + "Authorization": "Bearer tcp_" + } + } + } +} +``` + +### VS Code (MCP support / GitHub Copilot) + +Add the following to your `.vscode/mcp.json` (or use the **MCP: Add Server** command from the command palette): + +```json +{ + "servers": { + "part-db": { + "type": "http", + "url": "https://your-part-db.local/mcp", + "headers": { + "Authorization": "Bearer tcp_" + } + } + } +} +``` + +### Other clients + +Any MCP client that supports the Streamable HTTP transport with custom headers can connect to Part-DB, you generally +just need to provide: + +* **URL**: `https://your-part-db.local/mcp` +* **Transport**: Streamable HTTP +* **Header**: `Authorization: Bearer tcp_` + +## Available tools + +The following MCP tools are currently available. All of them are read-only. + +### Parts + +* **search_parts** – Search for parts by a keyword, with toggles to control which fields are searched (name, + description, comment, tags, storage location, supplier order number, MPN, IPN, supplier, manufacturer, footprint, + category, database ID), and an optional regex mode. +* **get_part_details** – Get full details about a specific part by its database ID, including stock, prices, + order details, attachments, parameters and EDA info. + +### Master data + +Categories, footprints, manufacturers, storage locations, measurement units, suppliers and part custom states all +expose the same pair of tools: + +* **list_categories** / **get_category_details** +* **list_footprints** / **get_footprint_details** +* **list_manufacturers** / **get_manufacturer_details** +* **list_storage_locations** / **get_storage_location_details** +* **list_measurement_units** / **get_measurement_unit_details** +* **list_suppliers** / **get_supplier_details** +* **list_part_custom_states** / **get_part_custom_state_details** + +Each `list_*` tool accepts an optional `keyword`, matched against the name and comment. Without a keyword, all +elements are returned in hierarchical tree order; with a keyword, matching results are sorted by their full path, so +parent/child relationships can still be derived from the flat list. Each `get_*_details` tool takes the element's +database `id` and returns its full details. + +### Projects + +* **list_projects** / **get_project_details** – Same behavior as the master data tools above. + `get_project_details` additionally returns the project's BOM entries, status, description and associated build + part. + +### Info Provider System + +These tools query external part information providers (e.g. Digikey, Mouser, LCSC), see the +[Information provider system]({% link usage/information_provider_system.md %}) page for background: + +* **list_info_providers** – List the info providers that are currently active and can be used with the two tools + below. +* **search_info_providers** – Search one or more external info providers (or the configured default providers, if + none are specified) for parts matching a keyword. +* **get_info_provider_part_details** – Get full details (datasheets, images, parameters, prices, ...) for a specific + search result, identified by the `provider_key` and `provider_id` returned by `search_info_providers`. diff --git a/docs/configuration.md b/docs/configuration.md index ea86f3a6..d6309351 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -262,7 +262,21 @@ See the [information providers]({% link usage/information_provider_system.md %}) * `TRUSTED_PROXIES` (env only): Set the IP addresses (or IP blocks) of trusted reverse proxies here. This is needed to get correct IP information (see [here](https://symfony.com/doc/current/deployment/proxies.html) for more info). * `TRUSTED_HOSTS` (env only): To prevent `HTTP Host header attacks` you can set a regex containing all host names via which Part-DB - should be accessible. If accessed via the wrong hostname, an error will be shown. + should be accessible. If accessed via a hostname not matching the regex, an error page will be shown instead. By default this + is empty, meaning Part-DB accepts requests for any host name, which is not recommended for production use. + + For example, if Part-DB should only be reachable under `part-db.example.invalid`, set the following in your + `.env.local` file (the value must be wrapped in single quotes here): + ``` + TRUSTED_HOSTS='^(part-db\.example\.invalid)$' + ``` + If you set `TRUSTED_HOSTS` as an environment variable in your `docker-compose.yaml` instead, the value must + **not** be quoted: + ``` + TRUSTED_HOSTS=^(part-db\.example\.invalid)$ + ``` + You can specify multiple host names separated by `|`, e.g. `^(localhost|part-db\.example\.invalid)$`. + Part-DB displays a warning on the homepage (visible to administrators only) as long as this value is not set. * `DEMO_MODE` (env only): Set Part-DB into demo mode, which forbids users to change their passwords and settings. Used for the demo instance. This should not be needed for normal installations. * `NO_URL_REWRITE_AVAILABLE` (allowed values `true` or `false`) (env only): Set this value to true, if your webserver does not @@ -279,9 +293,13 @@ See the [information providers]({% link usage/information_provider_system.md %}) * `BANNER`: You can configure the text that should be shown as the banner on the homepage. Useful especially for docker containers. In all other applications you can just change the `config/banner.md` file. * `DISABLE_YEAR2038_BUG_CHECK` (env only): If set to `1`, the year 2038 bug check is disabled on 32-bit systems, and dates after -2038 are no longer forbidden. However this will lead to 500 error messages when rendering dates after 2038 as all current +2038 are no longer forbidden. However, this will lead to 500 error messages when rendering dates after 2038 as all current 32-bit PHP versions can not format these dates correctly. This setting is for the case that future PHP versions will handle this correctly on 32-bit systems. 64-bit systems are not affected by this bug, and the check is always disabled. +* `DEPRECATION_LOG_LEVEL` (default `emergency`) (env only): In the `prod` and `docker` environments, PHP/Symfony + deprecation notices are written to their own `var/log/_deprecations.log` file. This option sets the minimum log + level a deprecation notice must have to be written there. Since deprecation notices are logged with level `info`, + the default value of `emergency` effectively disables this dedicated deprecation log. Set it to `debug` to enable it. ## Banner diff --git a/docs/installation/installation_docker.md b/docs/installation/installation_docker.md index 97b1e662..1331f2a3 100644 --- a/docs/installation/installation_docker.md +++ b/docs/installation/installation_docker.md @@ -84,7 +84,12 @@ services: # If you use a reverse proxy in front of Part-DB, you must configure the trusted proxies IP addresses here (see reverse proxy documentation for more information): # - TRUSTED_PROXIES=127.0.0.0/8,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 - + + # To prevent HTTP Host header attacks, set this to a regex matching all host names Part-DB should be reachable under. + # By default (unset) Part-DB accepts requests for any host name, which is not recommended for production use. + # IMPORTANT: Unlike in .env files, the value here must NOT be wrapped in quotes. + # - TRUSTED_HOSTS=^(part-db\.example\.invalid)$ + # If you need to install additional composer packages (e.g., for specific mailer transports), you can specify them here: # The packages will be installed automatically when the container starts # - COMPOSER_EXTRA_PACKAGES=symfony/mailgun-mailer symfony/sendgrid-mailer @@ -96,7 +101,11 @@ services: ```bash openssl rand -hex 32 ``` -6. Inside the folder, run +6. Uncomment and set the `TRUSTED_HOSTS` variable to a regex matching the host name(s) Part-DB should be reachable under + (e.g. `^(part-db\.example\.invalid)$`), to prevent HTTP Host header attacks. Note that, unlike in `.env` files, the + value must **not** be quoted in `docker-compose.yaml`. See [Configuration]({% link configuration.md %}) + for more information. +7. Inside the folder, run ```bash docker-compose up -d @@ -107,7 +116,7 @@ openssl rand -hex 32 > Otherwise Part-DB console might use the wrong configuration to execute commands. -7. Create the initial database with +8. Create the initial database with ```bash docker exec --user=www-data partdb php bin/console doctrine:migrations:migrate @@ -115,7 +124,7 @@ docker exec --user=www-data partdb php bin/console doctrine:migrations:migrate and watch for the password output -8. Part-DB is available under `http://localhost:8080` and you can log in with the username `admin` and the password shown +9. Part-DB is available under `http://localhost:8080` and you can log in with the username `admin` and the password shown before The docker image uses a SQLite database and all data (database, uploads, and other media) is put into folders relative to @@ -171,7 +180,12 @@ services: # Override value if you want to show a given text on homepage. # When this is commented out the webUI can be used to configure the banner #- BANNER=This is a test banner
with a line break - + + # To prevent HTTP Host header attacks, set this to a regex matching all host names Part-DB should be reachable under. + # By default (unset) Part-DB accepts requests for any host name, which is not recommended for production use. + # IMPORTANT: Unlike in .env files, the value here must NOT be wrapped in quotes. + # - TRUSTED_HOSTS=^(part-db\.example\.invalid)$ + # If you need to install additional composer packages (e.g., for specific mailer transports), you can specify them here: # - COMPOSER_EXTRA_PACKAGES=symfony/mailgun-mailer symfony/sendgrid-mailer diff --git a/docs/installation/installation_guide-debian.md b/docs/installation/installation_guide-debian.md index e9f500b8..77ea8020 100644 --- a/docs/installation/installation_guide-debian.md +++ b/docs/installation/installation_guide-debian.md @@ -1,13 +1,13 @@ --- -title: Direct Installation on Debian 12 +title: Direct Installation on Debian 13 layout: default parent: Installation nav_order: 4 --- -# Part-DB installation guide for Debian 12 (Bookworm) +# Part-DB installation guide for Debian 13 (Trixie) -This guide shows you how to install Part-DB directly on Debian 12 using apache2 and SQLite. This guide should work with +This guide shows you how to install Part-DB directly on Debian 13 using apache2 and SQLite. This guide should work with recent Ubuntu and other Debian-based distributions with little to no changes. Depending on what you want to do, using the prebuilt docker images may be a better choice, as you don't need to install this many dependencies. See [here]({% link installation/installation_docker.md %}) for more information on the docker @@ -45,10 +45,10 @@ compatibility. Install PHP with required extensions and apache2: ```bash -sudo apt install apache2 php8.2 libapache2-mod-php8.2 \ - php8.2-opcache php8.2-curl php8.2-gd php8.2-mbstring \ - php8.2-xml php8.2-bcmath php8.2-intl php8.2-zip php8.2-xsl \ - php8.2-sqlite3 php8.2-mysql +sudo apt install apache2 php8.4 libapache2-mod-php8.4 \ + php8.4-opcache php8.4-curl php8.4-gd php8.4-mbstring \ + php8.4-xml php8.4-bcmath php8.4-intl php8.4-zip php8.4-xsl \ + php8.4-sqlite3 php8.4-mysql ``` ### Install composer @@ -145,6 +145,16 @@ A full list of configuration options can be found [here](../configuration.md). > ``` > or edit the file with a text editor and add a new value for `APP_SECRET` (you can generate a random value with `openssl rand -hex 32`). +{: .important } +> **Set `TRUSTED_HOSTS` before going live.** By default Part-DB accepts requests for any host name, which makes it +> vulnerable to `HTTP Host header attacks`. Edit your `.env.local` with a text editor and set `TRUSTED_HOSTS` to a +> regex matching all host names Part-DB should be reachable under, e.g. if Part-DB should only be reachable via +> `part-db.example.invalid`: +> ``` +> TRUSTED_HOSTS='^(part-db\.example\.invalid)$' +> ``` +> See the [configuration options](../configuration.md) page for more information about `TRUSTED_HOSTS`. + Please check that the configured base currency matches your mainly used currency, as this can not be changed after creating price information. @@ -232,7 +242,7 @@ sudo ln -s /etc/apache2/sites-available/partdb.conf /etc/apache2/sites-enabled/p Configure apache to show pretty URL paths for Part-DB (`/label/dialog` instead of `/index.php/label/dialog`): ```bash -sudo a2enmod rewrite +sudo a2enmod rewrite headers ``` If you want to access Part-DB via the IP-Address of the server, instead of the domain name, you have to remove the @@ -291,7 +301,7 @@ sudo -u www-data php bin/console cache:clear ## MySQL/MariaDB database To use a MySQL database, follow the steps from above (except the creation of the database, we will do this later). -Debian 12 does not ship MySQL in its repositories anymore, so we use the compatible MariaDB instead: +Debian does not ship MySQL in its repositories anymore, so we use the compatible MariaDB instead: 1. Install maria-db with: diff --git a/docs/installation/nginx.md b/docs/installation/nginx.md index 981c18d5..1ae1d32c 100644 --- a/docs/installation/nginx.md +++ b/docs/installation/nginx.md @@ -36,6 +36,10 @@ server { root /var/www/partdb/public; location / { + # Headers are set here for static assets. PHP responses are served via the index.php location + # below and inherit neither of these headers, so Nelmio's PHP-side CSP is unaffected. + add_header Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; sandbox;" always; + add_header X-Content-Type-Options "nosniff" always; try_files $uri /index.php$is_args$args; } @@ -57,10 +61,12 @@ server { location ~* ^/media/.*\.(php[3-8]?|phar|phtml|pht|phps)$ { return 403; } - - # Set Content-Security-Policy for svg files, to block embedded javascript in there + + # SVG files get a slightly different CSP because they can embed resources and must not be framed. + # This regex location takes precedence over location /, so headers must be repeated here. location ~* \.svg$ { - add_header Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none';"; + add_header Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'; sandbox;" always; + add_header X-Content-Type-Options "nosniff" always; } error_log /var/log/nginx/parts.error.log; diff --git a/docs/installation/reverse-proxy.md b/docs/installation/reverse-proxy.md index 605b93fa..30c74223 100644 --- a/docs/installation/reverse-proxy.md +++ b/docs/installation/reverse-proxy.md @@ -22,6 +22,22 @@ TRUSTED_PROXIES=192.168.2.10 Set the `DEFAULT_URI` environment variable to the URL of your Part-DB installation, available from the outside (so via the reverse proxy). +You should also set the `TRUSTED_HOSTS` environment variable to a regex matching the host name(s) Part-DB is reachable +under via the reverse proxy, to prevent `HTTP Host header attacks`. For example, if Part-DB is reachable via +`part-db.example.invalid`, set the following in your `.env.local` file (the value must be wrapped in single quotes here): + +``` +TRUSTED_HOSTS='^(part-db\.example\.invalid)$' +``` + +If you set `TRUSTED_HOSTS` in your `docker-compose.yaml` instead, the value must **not** be quoted: + +``` +TRUSTED_HOSTS=^(part-db\.example\.invalid)$ +``` + +See the [configuration options](../configuration.md) page for more information about `TRUSTED_HOSTS`. + ## Part-DB in a subpath via reverse proxy If you put Part-DB into a subpath via the reverse proxy, you have to configure your webserver to include `X-Forwarded-Prefix` in the request headers. diff --git a/docs/usage/ai.md b/docs/usage/ai.md index 3a1fb419..30f6c628 100644 --- a/docs/usage/ai.md +++ b/docs/usage/ai.md @@ -25,3 +25,10 @@ You need to supply an API key for OpenRouter to use it as an AI platform in Part [LMStudio](https://lmstudio.ai/) is a local LLM hosting solution that allows you to run LLMs on your own hardware. You can use LMStudio to host your own LLM and connect it to Part-DB for AI features. Currently only LMStudio without any authentication is supported. Supply your LMStudio instance URL (including the port) to use it as an AI platform in Part-DB. +You have to set a model by hand, as suggestions currently do not work yet. Ensure the context length is suitable for your application. + +### Ollama + +[Ollama](https://ollama.com/) is another local LLM hosting solution that allows you to run LLMs on your own hardware. You can use Ollama to host your own LLM and connect it to Part-DB for AI features. +Supply your Ollama instance URL (including the port) and an optional API key for authentication to use it as an AI platform in Part-DB. The model selector should give you suggestions about available models. +Ensure the context length is suitable for your application. diff --git a/docs/usage/eda_integration.md b/docs/usage/eda_integration.md index 92b1244d..653eb8a3 100644 --- a/docs/usage/eda_integration.md +++ b/docs/usage/eda_integration.md @@ -54,7 +54,7 @@ To connect KiCad with Part-DB do the following steps: ``` 4. Replace the `root_url` with the URL of your Part-DB instance plus `/en/kicad-api/`. You can find the right value for this in the Part-DB user settings page under "API endpoints" in the "API tokens" panel. 5. Replace the `token` field value with the token you have generated in step 1. -6. Open KiCad and add this created file as a library in the KiCad symbol table under (Preferences --> Manage Symbol Libraries) +6. Open KiCad and add this created file as a HTTP library in the KiCad symbol table under (Preferences --> Manage Symbol Libraries) If you then place a new part, the library dialog opens, and you should be able to see the categories and parts from Part-DB. diff --git a/docs/usage/import_export.md b/docs/usage/import_export.md index f4d8d91c..9f1525c2 100644 --- a/docs/usage/import_export.md +++ b/docs/usage/import_export.md @@ -109,6 +109,21 @@ supplier is not specified, the price and supplier product number fields will be * **`supplier_product_number`** or **`supplier_part_number`** or * **`spn`**: The supplier product number of the part. * **`price`**: The price of the part in the base currency of the database (by default euro). +The following fields set the EDA / KiCad metadata of the part (see [EDA / KiCad integration](eda_integration.md)). +These can be given either as the flat column names below, or in the nested `eda_info.*` form used by the +CSV/JSON export and the [example file](#example-data) (e.g. `eda_info.kicad_symbol`), so a file exported from +Part-DB can be re-imported without renaming its headers: + +* **`eda_kicad_symbol`** or **`kicad_symbol`**: The KiCad symbol of the part, e.g. `Device:R`. +* **`eda_kicad_footprint`** or **`kicad_footprint`**: The KiCad footprint, e.g. `Resistor_SMD:R_0805_2012Metric`. +* **`eda_reference_prefix`** or **`kicad_reference`**: The schematic reference prefix, e.g. `R`, `C`, `L`. +* **`eda_value`** or **`kicad_value`**: The value shown in the EDA tool, e.g. `10k`. +* **`eda_exclude_from_bom`** (or **`eda_exclude_bom`**), **`eda_exclude_from_board`** (or **`eda_exclude_board`**), + **`eda_exclude_from_sim`** (or **`eda_exclude_sim`**): Booleans (`1`/`0`) to exclude the part from the BOM, board + or simulation. +* **`eda_visibility`**: Boolean (`1`/`0`) controlling whether the part is exposed to the EDA tool. The inverse + convenience alias **`eda_invisible`** is also accepted (so `eda_invisible=1` hides the part). + #### Example data Here you can find some example data for the import of parts, you can use it as a template for your own import ( diff --git a/package.json b/package.json index f846f1d1..f906d814 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "bootstrap": "^5.1.3", "core-js": "^3.38.0", "intl-messageformat": "^10.5.11", - "jquery": "^3.5.1", + "jquery": "^4.0.0", "popper.js": "^1.14.7", "regenerator-runtime": "^0.14.1", "webpack": "^5.74.0", @@ -38,20 +38,21 @@ "@algolia/autocomplete-theme-classic": "^1.17.0", "@jbtronics/bs-treeview": "^1.0.1", "@part-db/html5-qrcode": "^4.0.0", - "@zxcvbn-ts/core": "^3.0.2", - "@zxcvbn-ts/language-common": "^3.0.3", - "@zxcvbn-ts/language-de": "^3.0.1", - "@zxcvbn-ts/language-en": "^3.0.1", - "@zxcvbn-ts/language-fr": "^3.0.1", - "@zxcvbn-ts/language-ja": "^3.0.1", + "@zxcvbn-ts/core": "^4.1.2", + "@zxcvbn-ts/language-common": "^4.1.2", + "@zxcvbn-ts/language-de": "^4.1.1", + "@zxcvbn-ts/language-en": "^4.1.1", + "@zxcvbn-ts/language-fr": "^4.1.1", + "@zxcvbn-ts/language-it": "^4.1.1", + "@zxcvbn-ts/language-ja": "^4.1.1", + "@zxcvbn-ts/language-pl": "^4.1.1", "attr-accept": "^2.2.5", "barcode-detector": "^3.0.5", - "bootbox": "^6.0.0", "bootswatch": "^5.1.3", "bs-custom-file-input": "^1.3.4", "ckeditor5": "^48.0.0", "clipboard": "^2.0.4", - "compression-webpack-plugin": "^11.1.0", + "compression-webpack-plugin": "^12.0.0", "datatables.net": "^2.0.0", "datatables.net-bs5": "^2.0.0", "datatables.net-buttons-bs5": "^3.0.0", @@ -69,11 +70,12 @@ "marked-mangle": "^1.0.1", "pdfmake": "^0.3.7", "stimulus-use": "^0.52.0", + "sweetalert2": "^11.26.25", "tom-select": "^2.1.0", "ts-loader": "^9.2.6", "typescript": "^6.0.2" }, "resolutions": { - "jquery": "^3.5.1" + "jquery": "^4.0.0" } } diff --git a/public/.htaccess b/public/.htaccess index a13baeee..4aaf1009 100644 --- a/public/.htaccess +++ b/public/.htaccess @@ -119,9 +119,25 @@ DirectoryIndex index.php -# Set Content-Security-Policy for svg files (and compressed variants), to block embedded javascript in there - - Header set Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none';" + # Set a strict CSP for everything Apache serves, except the front controller (index.php), + # whose response CSP is set by NelmioSecurityBundle in PHP. Excluding it via a negative + # FilesMatch (rather than a blanket rule relying on "setifempty" to skip PHP responses) + # avoids depending on mod_headers detecting the PHP-set header at the right time: on a + # real Apache 2.4/mod_php install, "setifempty" did not reliably suppress the static policy, + # so BOTH Content-Security-Policy headers ended up on the same PHP response, and browsers + # enforce the intersection of all CSP headers on a response - so "script-src 'none'; + # sandbox" silently disabled all scripts/styles site-wide. mod_rewrite's internal redirect + # to index.php re-runs directory-scoped matching against the new target, so this FilesMatch + # reliably excludes it regardless of the originally requested URL. + + Header always set Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; sandbox;" + Header always set X-Content-Type-Options "nosniff" - \ No newline at end of file + + # SVG files get a slightly different CSP because they can embed resources and must not be framed. + # (Declared after the general rule above so its more specific "set" wins for SVG files.) + + Header always set Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'; sandbox;" + + diff --git a/public/kicad/footprints.txt b/public/kicad/footprints.txt index 5f691a41..538984c2 100644 --- a/public/kicad/footprints.txt +++ b/public/kicad/footprints.txt @@ -1,4 +1,4 @@ -# Generated on Mon Jun 1 07:07:44 UTC 2026 +# Generated on Mon Jul 27 05:55:56 UTC 2026 # This file contains all footprints available in the offical KiCAD library Audio_Module:Reverb_BTDR-1H Audio_Module:Reverb_BTDR-1V @@ -7732,6 +7732,15 @@ Connector_TE-Connectivity:TE_Micro-MaTch_2-215079-0_2x10_P1.27mm_Vertical Connector_TE-Connectivity:TE_Micro-MaTch_215079-4_2x02_P1.27mm_Vertical Connector_TE-Connectivity:TE_Micro-MaTch_215079-6_2x03_P1.27mm_Vertical Connector_TE-Connectivity:TE_Micro-MaTch_215079-8_2x04_P1.27mm_Vertical +Connector_TE-Connectivity:TE_Micro-MaTch_7-188275-4_2x02_P1.27mm_Vertical +Connector_TE-Connectivity:TE_Micro-MaTch_7-188275-6_2x03_P1.27mm_Vertical +Connector_TE-Connectivity:TE_Micro-MaTch_7-188275-8_2x04_P1.27mm_Vertical +Connector_TE-Connectivity:TE_Micro-MaTch_8-188275-0_2x05_P1.27mm_Vertical +Connector_TE-Connectivity:TE_Micro-MaTch_8-188275-2_2x06_P1.27mm_Vertical +Connector_TE-Connectivity:TE_Micro-MaTch_8-188275-4_2x07_P1.27mm_Vertical +Connector_TE-Connectivity:TE_Micro-MaTch_8-188275-6_2x08_P1.27mm_Vertical +Connector_TE-Connectivity:TE_Micro-MaTch_8-188275-8_2x09_P1.27mm_Vertical +Connector_TE-Connectivity:TE_Micro-MaTch_9-188275-0_2x10_P1.27mm_Vertical Connector_TE-Connectivity:TE_T4041037031-000_M8_03_Socket_Straight Connector_TE-Connectivity:TE_T4041037041-000_M8_04_Socket_Straight Connector_USB:USB3_A_Molex_48393-001 @@ -8293,6 +8302,7 @@ Converter_DCDC:Converter_DCDC_Hamamatsu_C11204-1_THT Converter_DCDC:Converter_DCDC_MeanWell_NID30_THT Converter_DCDC:Converter_DCDC_MeanWell_NID60_THT Converter_DCDC:Converter_DCDC_MeanWell_NSD10_THT +Converter_DCDC:Converter_DCDC_MeanWell_SMU02x-xxN_THT Converter_DCDC:Converter_DCDC_Murata_CRE1xxxxxx3C_THT Converter_DCDC:Converter_DCDC_Murata_CRE1xxxxxxDC_THT Converter_DCDC:Converter_DCDC_Murata_CRE1xxxxxxSC_THT @@ -9149,6 +9159,7 @@ Inductor_SMD:L_APV_APH1770 Inductor_SMD:L_APV_APH2213 Inductor_SMD:L_AVX_LMLP07A7 Inductor_SMD:L_Abracon_ASPI-0425 +Inductor_SMD:L_Abracon_ASPI-0628 Inductor_SMD:L_Abracon_ASPI-0630LR Inductor_SMD:L_Abracon_ASPI-3012S Inductor_SMD:L_Abracon_ASPI-4030S @@ -12012,7 +12023,6 @@ Package_DFN_QFN:WDFN-8-1EP_4x3mm_P0.65mm_EP2.4x1.8mm_ThermalVias Package_DFN_QFN:WDFN-8-1EP_6x5mm_P1.27mm_EP3.4x4mm Package_DFN_QFN:WDFN-8-1EP_8x6mm_P1.27mm_EP6x4.8mm Package_DFN_QFN:WDFN-8-1EP_8x6mm_P1.27mm_EP6x4.8mm_ThermalVias -Package_DFN_QFN:WDFN-8_2x2mm_P0.5mm Package_DFN_QFN:WFDFPN-8-1EP_3x2mm_P0.5mm_EP1.25x1.35mm Package_DFN_QFN:WQFN-14-1EP_2.5x2.5mm_P0.5mm_EP1.45x1.45mm Package_DFN_QFN:WQFN-14-1EP_2.5x2.5mm_P0.5mm_EP1.45x1.45mm_ThermalVias @@ -12358,7 +12368,6 @@ Package_DirectFET:DirectFET_SQ Package_DirectFET:DirectFET_ST Package_LCC:Analog_LCC-8_5x5mm_P1.27mm Package_LCC:MO047AD_PLCC-52_19.1x19.1mm_P1.27mm -Package_LCC:PLCC-20 Package_LCC:PLCC-20_9.0x9.0mm_P1.27mm Package_LCC:PLCC-20_SMD-Socket Package_LCC:PLCC-20_THT-Socket @@ -12566,6 +12575,8 @@ Package_SO:HTSSOP-16-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask2.46x2.31mm Package_SO:HTSSOP-16-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask2.46x2.31mm_ThermalVias Package_SO:HTSSOP-16-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask2.66x2.46mm Package_SO:HTSSOP-16-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask2.66x2.46mm_ThermalVias +Package_SO:HTSSOP-16-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask2.78x3.37mm +Package_SO:HTSSOP-16-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask2.78x3.37mm_ThermalVias Package_SO:HTSSOP-16-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask3x3mm_ThermalVias Package_SO:HTSSOP-16-1EP_4.4x5mm_P0.65mm_EP3x3mm Package_SO:HTSSOP-20-1EP_4.4x6.5mm_P0.65mm_EP2.74x3.86mm @@ -12920,6 +12931,8 @@ Package_SO:Texas_DYY0016A_TSOT-23-16_2x4.2mm_P0.5mm Package_SO:Texas_HSOP-8-1EP_3.9x4.9mm_P1.27mm Package_SO:Texas_HSOP-8-1EP_3.9x4.9mm_P1.27mm_ThermalVias Package_SO:Texas_HTSOP-8-1EP_3.9x4.9mm_P1.27mm_EP2.95x4.9mm_Mask2.4x3.1mm_ThermalVias +Package_SO:Texas_HTSSOP-14-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask2.94x3.34mm +Package_SO:Texas_HTSSOP-14-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask2.94x3.34mm_ThermalVias Package_SO:Texas_HTSSOP-14-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask3.155x3.255mm Package_SO:Texas_HTSSOP-14-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask3.155x3.255mm_ThermalVias Package_SO:Texas_PW0020A_TSSOP-20_4.4x6.5mm_P0.65mm @@ -13032,8 +13045,6 @@ Package_SON:WSON-6-1EP_2x2mm_P0.65mm_EP1x1.6mm Package_SON:WSON-6-1EP_2x2mm_P0.65mm_EP1x1.6mm_ThermalVias Package_SON:WSON-6-1EP_3x3mm_P0.95mm Package_SON:WSON-6_1.5x1.5mm_P0.5mm -Package_SON:WSON-8-1EP_2x2mm_P0.5mm_EP0.9x1.6mm -Package_SON:WSON-8-1EP_2x2mm_P0.5mm_EP0.9x1.6mm_ThermalVias Package_SON:WSON-8-1EP_3x2.5mm_P0.5mm_EP1.2x1.5mm_PullBack Package_SON:WSON-8-1EP_3x2.5mm_P0.5mm_EP1.2x1.5mm_PullBack_ThermalVias Package_SON:WSON-8-1EP_3x3mm_P0.5mm_EP1.2x2mm diff --git a/public/kicad/symbols.txt b/public/kicad/symbols.txt index 7b4fde9c..2c0e2558 100644 --- a/public/kicad/symbols.txt +++ b/public/kicad/symbols.txt @@ -1,4 +1,4 @@ -# Generated on Mon Jun 1 07:08:24 UTC 2026 +# Generated on Mon Jul 27 05:56:38 UTC 2026 # This file contains all symbols available in the offical KiCAD library 4xxx:14528 4xxx:14529 @@ -951,6 +951,8 @@ Amplifier_Current:INA201D Amplifier_Current:INA201DGK Amplifier_Current:INA202D Amplifier_Current:INA202DGK +Amplifier_Current:INA2180AxxDGK +Amplifier_Current:INA2180AxxDSG Amplifier_Current:INA225 Amplifier_Current:INA240A1D Amplifier_Current:INA240A1PW @@ -1461,6 +1463,8 @@ Amplifier_Operational:OPA858xDSG Amplifier_Operational:OPA859xDSG Amplifier_Operational:OPA890xD Amplifier_Operational:OPA890xDBV +Amplifier_Operational:Opamp_Dual +Amplifier_Operational:Opamp_Quad Amplifier_Operational:RC4558 Amplifier_Operational:RC4560 Amplifier_Operational:RC4580 @@ -4398,401 +4402,7 @@ Converter_ACDC:VTX-214-015-115 Converter_ACDC:VTX-214-015-118 Converter_ACDC:VTX-214-015-124 Converter_ACDC:VTX-214-015-148 -Converter_DCDC:ATA00A18S-L -Converter_DCDC:ATA00A36S-L -Converter_DCDC:ATA00AA18S-L -Converter_DCDC:ATA00AA36S-L -Converter_DCDC:ATA00B18S-L -Converter_DCDC:ATA00B36S-L -Converter_DCDC:ATA00BB18S-L -Converter_DCDC:ATA00BB36S-L -Converter_DCDC:ATA00C18S-L -Converter_DCDC:ATA00C36S-L -Converter_DCDC:ATA00CC18S-L -Converter_DCDC:ATA00CC36S-L -Converter_DCDC:ATA00F18S-L -Converter_DCDC:ATA00F36S-L -Converter_DCDC:ATA00H18S-L -Converter_DCDC:ATA00H36S-L -Converter_DCDC:Ag9905LP Converter_DCDC:C11204-01 -Converter_DCDC:IA0305D -Converter_DCDC:IA0305S -Converter_DCDC:IA0503D -Converter_DCDC:IA0503S -Converter_DCDC:IA0505D -Converter_DCDC:IA0505S -Converter_DCDC:IA0509D -Converter_DCDC:IA0509S -Converter_DCDC:IA0512D -Converter_DCDC:IA0512S -Converter_DCDC:IA0515D -Converter_DCDC:IA0515S -Converter_DCDC:IA0524D -Converter_DCDC:IA0524S -Converter_DCDC:IA1203D -Converter_DCDC:IA1203S -Converter_DCDC:IA1205D -Converter_DCDC:IA1205S -Converter_DCDC:IA1209D -Converter_DCDC:IA1209S -Converter_DCDC:IA1212D -Converter_DCDC:IA1212S -Converter_DCDC:IA1215D -Converter_DCDC:IA1215S -Converter_DCDC:IA1224D -Converter_DCDC:IA1224S -Converter_DCDC:IA2403D -Converter_DCDC:IA2403S -Converter_DCDC:IA2405D -Converter_DCDC:IA2405S -Converter_DCDC:IA2409D -Converter_DCDC:IA2409S -Converter_DCDC:IA2412D -Converter_DCDC:IA2412S -Converter_DCDC:IA2415D -Converter_DCDC:IA2415S -Converter_DCDC:IA2424D -Converter_DCDC:IA2424S -Converter_DCDC:IA4803D -Converter_DCDC:IA4803S -Converter_DCDC:IA4805D -Converter_DCDC:IA4805S -Converter_DCDC:IA4809D -Converter_DCDC:IA4809S -Converter_DCDC:IA4812D -Converter_DCDC:IA4812S -Converter_DCDC:IA4815D -Converter_DCDC:IA4815S -Converter_DCDC:IA4824D -Converter_DCDC:IA4824S -Converter_DCDC:IH0503D -Converter_DCDC:IH0503DH -Converter_DCDC:IH0503S -Converter_DCDC:IH0503SH -Converter_DCDC:IH0505D -Converter_DCDC:IH0505DH -Converter_DCDC:IH0505S -Converter_DCDC:IH0505SH -Converter_DCDC:IH0509D -Converter_DCDC:IH0509DH -Converter_DCDC:IH0509S -Converter_DCDC:IH0509SH -Converter_DCDC:IH0512D -Converter_DCDC:IH0512DH -Converter_DCDC:IH0512S -Converter_DCDC:IH0512SH -Converter_DCDC:IH0515D -Converter_DCDC:IH0515DH -Converter_DCDC:IH0515S -Converter_DCDC:IH0515SH -Converter_DCDC:IH0524D -Converter_DCDC:IH0524DH -Converter_DCDC:IH0524S -Converter_DCDC:IH0524SH -Converter_DCDC:IH1203D -Converter_DCDC:IH1203DH -Converter_DCDC:IH1203S -Converter_DCDC:IH1203SH -Converter_DCDC:IH1205D -Converter_DCDC:IH1205DH -Converter_DCDC:IH1205S -Converter_DCDC:IH1205SH -Converter_DCDC:IH1209D -Converter_DCDC:IH1209DH -Converter_DCDC:IH1209S -Converter_DCDC:IH1209SH -Converter_DCDC:IH1212D -Converter_DCDC:IH1212DH -Converter_DCDC:IH1212S -Converter_DCDC:IH1212SH -Converter_DCDC:IH1215D -Converter_DCDC:IH1215DH -Converter_DCDC:IH1215S -Converter_DCDC:IH1215SH -Converter_DCDC:IH1224D -Converter_DCDC:IH1224DH -Converter_DCDC:IH1224S -Converter_DCDC:IH1224SH -Converter_DCDC:IH2403D -Converter_DCDC:IH2403DH -Converter_DCDC:IH2403S -Converter_DCDC:IH2403SH -Converter_DCDC:IH2405D -Converter_DCDC:IH2405DH -Converter_DCDC:IH2405S -Converter_DCDC:IH2405SH -Converter_DCDC:IH2409D -Converter_DCDC:IH2409DH -Converter_DCDC:IH2409S -Converter_DCDC:IH2409SH -Converter_DCDC:IH2412D -Converter_DCDC:IH2412DH -Converter_DCDC:IH2412S -Converter_DCDC:IH2412SH -Converter_DCDC:IH2415D -Converter_DCDC:IH2415DH -Converter_DCDC:IH2415S -Converter_DCDC:IH2415SH -Converter_DCDC:IH2424D -Converter_DCDC:IH2424DH -Converter_DCDC:IH2424S -Converter_DCDC:IH2424SH -Converter_DCDC:IH4803D -Converter_DCDC:IH4803DH -Converter_DCDC:IH4803S -Converter_DCDC:IH4803SH -Converter_DCDC:IH4805D -Converter_DCDC:IH4805DH -Converter_DCDC:IH4805S -Converter_DCDC:IH4805SH -Converter_DCDC:IH4809D -Converter_DCDC:IH4809DH -Converter_DCDC:IH4809S -Converter_DCDC:IH4809SH -Converter_DCDC:IH4812D -Converter_DCDC:IH4812DH -Converter_DCDC:IH4812S -Converter_DCDC:IH4812SH -Converter_DCDC:IH4815D -Converter_DCDC:IH4815DH -Converter_DCDC:IH4815S -Converter_DCDC:IH4815SH -Converter_DCDC:IH4824D -Converter_DCDC:IH4824DH -Converter_DCDC:IH4824S -Converter_DCDC:IH4824SH -Converter_DCDC:ISU0205D12 -Converter_DCDC:ISU0205D15 -Converter_DCDC:ISU0205S05 -Converter_DCDC:ISU0205S12 -Converter_DCDC:ISU0205S15 -Converter_DCDC:ISU0205S24 -Converter_DCDC:ISU0224D12 -Converter_DCDC:ISU0224D15 -Converter_DCDC:ISU0224S05 -Converter_DCDC:ISU0224S12 -Converter_DCDC:ISU0224S15 -Converter_DCDC:ISU0224S24 -Converter_DCDC:ISU0248D12 -Converter_DCDC:ISU0248D15 -Converter_DCDC:ISU0248S05 -Converter_DCDC:ISU0248S12 -Converter_DCDC:ISU0248S15 -Converter_DCDC:ISU0248S24 -Converter_DCDC:ITQ2403SA -Converter_DCDC:ITQ2403SA-H -Converter_DCDC:ITQ2405S -Converter_DCDC:ITQ2405S-H -Converter_DCDC:ITQ2405SA -Converter_DCDC:ITQ2405SA-H -Converter_DCDC:ITQ2409SA -Converter_DCDC:ITQ2409SA-H -Converter_DCDC:ITQ2412S -Converter_DCDC:ITQ2412S-H -Converter_DCDC:ITQ2412SA -Converter_DCDC:ITQ2412SA-H -Converter_DCDC:ITQ2415S -Converter_DCDC:ITQ2415S-H -Converter_DCDC:ITQ2415SA -Converter_DCDC:ITQ2415SA-H -Converter_DCDC:ITQ2424SA -Converter_DCDC:ITQ2424SA-H -Converter_DCDC:ITQ4803SA -Converter_DCDC:ITQ4803SA-H -Converter_DCDC:ITQ4805S -Converter_DCDC:ITQ4805S-H -Converter_DCDC:ITQ4805SA -Converter_DCDC:ITQ4805SA-H -Converter_DCDC:ITQ4809SA -Converter_DCDC:ITQ4809SA-H -Converter_DCDC:ITQ4812S -Converter_DCDC:ITQ4812S-H -Converter_DCDC:ITQ4812SA -Converter_DCDC:ITQ4812SA-H -Converter_DCDC:ITQ4815S -Converter_DCDC:ITQ4815S-H -Converter_DCDC:ITQ4815SA -Converter_DCDC:ITQ4815SA-H -Converter_DCDC:ITQ4824SA -Converter_DCDC:ITQ4824SA-H -Converter_DCDC:ITX0503SA -Converter_DCDC:ITX0503SA-H -Converter_DCDC:ITX0503SA-HR -Converter_DCDC:ITX0503SA-R -Converter_DCDC:ITX0505S -Converter_DCDC:ITX0505S-H -Converter_DCDC:ITX0505S-HR -Converter_DCDC:ITX0505S-R -Converter_DCDC:ITX0505SA -Converter_DCDC:ITX0505SA-H -Converter_DCDC:ITX0505SA-HR -Converter_DCDC:ITX0505SA-R -Converter_DCDC:ITX0509SA -Converter_DCDC:ITX0509SA-H -Converter_DCDC:ITX0509SA-HR -Converter_DCDC:ITX0509SA-R -Converter_DCDC:ITX0512S -Converter_DCDC:ITX0512S-H -Converter_DCDC:ITX0512S-HR -Converter_DCDC:ITX0512S-R -Converter_DCDC:ITX0512SA -Converter_DCDC:ITX0512SA-H -Converter_DCDC:ITX0512SA-HR -Converter_DCDC:ITX0512SA-R -Converter_DCDC:ITX0515S -Converter_DCDC:ITX0515S-H -Converter_DCDC:ITX0515S-HR -Converter_DCDC:ITX0515S-R -Converter_DCDC:ITX0515SA -Converter_DCDC:ITX0515SA-H -Converter_DCDC:ITX0515SA-HR -Converter_DCDC:ITX0515SA-R -Converter_DCDC:ITX0524SA -Converter_DCDC:ITX0524SA-H -Converter_DCDC:ITX0524SA-HR -Converter_DCDC:ITX0524SA-R -Converter_DCDC:ITX1203SA -Converter_DCDC:ITX1203SA-H -Converter_DCDC:ITX1203SA-HR -Converter_DCDC:ITX1203SA-R -Converter_DCDC:ITX1205S -Converter_DCDC:ITX1205S-H -Converter_DCDC:ITX1205S-HR -Converter_DCDC:ITX1205S-R -Converter_DCDC:ITX1205SA -Converter_DCDC:ITX1205SA-H -Converter_DCDC:ITX1205SA-HR -Converter_DCDC:ITX1205SA-R -Converter_DCDC:ITX1209SA -Converter_DCDC:ITX1209SA-H -Converter_DCDC:ITX1209SA-HR -Converter_DCDC:ITX1209SA-R -Converter_DCDC:ITX1212S -Converter_DCDC:ITX1212S-H -Converter_DCDC:ITX1212S-HR -Converter_DCDC:ITX1212S-R -Converter_DCDC:ITX1212SA -Converter_DCDC:ITX1212SA-H -Converter_DCDC:ITX1212SA-HR -Converter_DCDC:ITX1212SA-R -Converter_DCDC:ITX1215S -Converter_DCDC:ITX1215S-H -Converter_DCDC:ITX1215S-HR -Converter_DCDC:ITX1215S-R -Converter_DCDC:ITX1215SA -Converter_DCDC:ITX1215SA-H -Converter_DCDC:ITX1215SA-HR -Converter_DCDC:ITX1215SA-R -Converter_DCDC:ITX1224SA -Converter_DCDC:ITX1224SA-H -Converter_DCDC:ITX1224SA-HR -Converter_DCDC:ITX1224SA-R -Converter_DCDC:ITX2403SA -Converter_DCDC:ITX2403SA-H -Converter_DCDC:ITX2403SA-HR -Converter_DCDC:ITX2403SA-R -Converter_DCDC:ITX2405S -Converter_DCDC:ITX2405S-H -Converter_DCDC:ITX2405S-HR -Converter_DCDC:ITX2405S-R -Converter_DCDC:ITX2405SA -Converter_DCDC:ITX2405SA-H -Converter_DCDC:ITX2405SA-HR -Converter_DCDC:ITX2405SA-R -Converter_DCDC:ITX2409SA -Converter_DCDC:ITX2409SA-H -Converter_DCDC:ITX2409SA-HR -Converter_DCDC:ITX2409SA-R -Converter_DCDC:ITX2412S -Converter_DCDC:ITX2412S-H -Converter_DCDC:ITX2412S-HR -Converter_DCDC:ITX2412S-R -Converter_DCDC:ITX2412SA -Converter_DCDC:ITX2412SA-H -Converter_DCDC:ITX2412SA-HR -Converter_DCDC:ITX2412SA-R -Converter_DCDC:ITX2415S -Converter_DCDC:ITX2415S-H -Converter_DCDC:ITX2415S-HR -Converter_DCDC:ITX2415S-R -Converter_DCDC:ITX2415SA -Converter_DCDC:ITX2415SA-H -Converter_DCDC:ITX2415SA-HR -Converter_DCDC:ITX2415SA-R -Converter_DCDC:ITX2424SA -Converter_DCDC:ITX2424SA-H -Converter_DCDC:ITX2424SA-HR -Converter_DCDC:ITX2424SA-R -Converter_DCDC:ITX4803SA -Converter_DCDC:ITX4803SA-H -Converter_DCDC:ITX4803SA-HR -Converter_DCDC:ITX4803SA-R -Converter_DCDC:ITX4805S -Converter_DCDC:ITX4805S-H -Converter_DCDC:ITX4805S-HR -Converter_DCDC:ITX4805S-R -Converter_DCDC:ITX4805SA -Converter_DCDC:ITX4805SA-H -Converter_DCDC:ITX4805SA-HR -Converter_DCDC:ITX4805SA-R -Converter_DCDC:ITX4809SA -Converter_DCDC:ITX4809SA-H -Converter_DCDC:ITX4809SA-HR -Converter_DCDC:ITX4809SA-R -Converter_DCDC:ITX4812S -Converter_DCDC:ITX4812S-H -Converter_DCDC:ITX4812S-HR -Converter_DCDC:ITX4812S-R -Converter_DCDC:ITX4812SA -Converter_DCDC:ITX4812SA-H -Converter_DCDC:ITX4812SA-HR -Converter_DCDC:ITX4812SA-R -Converter_DCDC:ITX4815S -Converter_DCDC:ITX4815S-H -Converter_DCDC:ITX4815S-HR -Converter_DCDC:ITX4815S-R -Converter_DCDC:ITX4815SA -Converter_DCDC:ITX4815SA-H -Converter_DCDC:ITX4815SA-HR -Converter_DCDC:ITX4815SA-R -Converter_DCDC:ITX4824SA -Converter_DCDC:ITX4824SA-H -Converter_DCDC:ITX4824SA-HR -Converter_DCDC:ITX4824SA-R -Converter_DCDC:JTD1524D05 -Converter_DCDC:JTD1524D12 -Converter_DCDC:JTD1524D15 -Converter_DCDC:JTD1524S05 -Converter_DCDC:JTD1524S12 -Converter_DCDC:JTD1524S15 -Converter_DCDC:JTD1524S3V3 -Converter_DCDC:JTD1548D05 -Converter_DCDC:JTD1548D12 -Converter_DCDC:JTD1548D15 -Converter_DCDC:JTD1548S05 -Converter_DCDC:JTD1548S12 -Converter_DCDC:JTD1548S15 -Converter_DCDC:JTD1548S3V3 -Converter_DCDC:JTD2024D05 -Converter_DCDC:JTD2024D12 -Converter_DCDC:JTD2024D15 -Converter_DCDC:JTD2024S05 -Converter_DCDC:JTD2024S12 -Converter_DCDC:JTD2024S15 -Converter_DCDC:JTD2024S3V3 -Converter_DCDC:JTD2048D05 -Converter_DCDC:JTD2048D12 -Converter_DCDC:JTD2048D15 -Converter_DCDC:JTD2048S05 -Converter_DCDC:JTD2048S12 -Converter_DCDC:JTD2048S15 -Converter_DCDC:JTD2048S3V3 -Converter_DCDC:JTE0624D03 -Converter_DCDC:JTE0624D05 -Converter_DCDC:JTE0624D12 -Converter_DCDC:JTE0624D15 -Converter_DCDC:JTE0624D24 Converter_DCDC:LMZ13608 Converter_DCDC:LMZ22003TZ Converter_DCDC:LMZ22005TZ @@ -4815,74 +4425,10 @@ Converter_DCDC:LTM4668A Converter_DCDC:LTM4671 Converter_DCDC:LTM8049 Converter_DCDC:LTM8063 -Converter_DCDC:MEE1S0303SC -Converter_DCDC:MEE1S0305SC -Converter_DCDC:MEE1S0309SC -Converter_DCDC:MEE1S0312SC -Converter_DCDC:MEE1S0315SC -Converter_DCDC:MEE1S0503SC -Converter_DCDC:MEE1S0505SC -Converter_DCDC:MEE1S0509SC -Converter_DCDC:MEE1S0512SC -Converter_DCDC:MEE1S0515SC -Converter_DCDC:MEE1S1205SC -Converter_DCDC:MEE1S1209SC -Converter_DCDC:MEE1S1212SC -Converter_DCDC:MEE1S1215SC -Converter_DCDC:MEE1S1505SC -Converter_DCDC:MEE1S1509SC -Converter_DCDC:MEE1S1512SC -Converter_DCDC:MEE1S1515SC -Converter_DCDC:MEE1S2405SC -Converter_DCDC:MEE1S2409SC -Converter_DCDC:MEE1S2412SC -Converter_DCDC:MEE1S2415SC -Converter_DCDC:MEE3S0505SC -Converter_DCDC:MEE3S0509SC -Converter_DCDC:MEE3S0512SC -Converter_DCDC:MEE3S0515SC -Converter_DCDC:MEE3S1205SC -Converter_DCDC:MEE3S1209SC -Converter_DCDC:MEE3S1212SC -Converter_DCDC:MEE3S1215SC -Converter_DCDC:MGJ2D051505SC -Converter_DCDC:MGJ2D051509SC -Converter_DCDC:MGJ2D051515SC -Converter_DCDC:MGJ2D051802SC -Converter_DCDC:MGJ2D052003SC -Converter_DCDC:MGJ2D052005SC -Converter_DCDC:MGJ2D121505SC -Converter_DCDC:MGJ2D121509SC -Converter_DCDC:MGJ2D121802SC -Converter_DCDC:MGJ2D122003SC -Converter_DCDC:MGJ2D122005SC -Converter_DCDC:MGJ2D151505SC -Converter_DCDC:MGJ2D151509SC -Converter_DCDC:MGJ2D151515SC -Converter_DCDC:MGJ2D151802SC -Converter_DCDC:MGJ2D152003SC -Converter_DCDC:MGJ2D152005SC -Converter_DCDC:MGJ2D241505SC -Converter_DCDC:MGJ2D241509SC -Converter_DCDC:MGJ2D241709SC -Converter_DCDC:MGJ2D241802SC -Converter_DCDC:MGJ2D242003SC -Converter_DCDC:MGJ2D242005SC -Converter_DCDC:MGJ3T05150505MC -Converter_DCDC:MGJ3T12150505MC -Converter_DCDC:MGJ3T24150505MC Converter_DCDC:MUN12AD01-SH Converter_DCDC:MUN12AD03-SH Converter_DCDC:MYRGPxx0060x21RC Converter_DCDC:MYRGPxx0060x21RF -Converter_DCDC:NCS1S1203SC -Converter_DCDC:NCS1S1205SC -Converter_DCDC:NCS1S1212SC -Converter_DCDC:NCS1S2403SC -Converter_DCDC:NCS1S2405SC -Converter_DCDC:NCS1S2412SC -Converter_DCDC:NSD10-xxDyy -Converter_DCDC:NSD10-xxSyy Converter_DCDC:OKI-78SR-12_1.0-W36-C Converter_DCDC:OKI-78SR-12_1.0-W36H-C Converter_DCDC:OKI-78SR-3.3_1.5-W36-C @@ -4937,10 +4483,6 @@ Converter_DCDC:R-78HB5.0-0.5 Converter_DCDC:R-78HB6.5-0.5 Converter_DCDC:R-78HB9.0-0.5 Converter_DCDC:R-78S3.3-0.1 -Converter_DCDC:RPA60-2405SFW -Converter_DCDC:RPA60-2412SFW -Converter_DCDC:RPA60-2415SFW -Converter_DCDC:RPA60-2424SFW Converter_DCDC:RPM3.3-1.0 Converter_DCDC:RPM3.3-2.0 Converter_DCDC:RPM3.3-3.0 @@ -4954,430 +4496,9 @@ Converter_DCDC:RPMH15-1.5 Converter_DCDC:RPMH24-1.5 Converter_DCDC:RPMH3.3-1.5 Converter_DCDC:RPMH5.0-1.5 -Converter_DCDC:TBA1-0310 -Converter_DCDC:TBA1-0311 -Converter_DCDC:TBA1-0510 -Converter_DCDC:TBA1-0511 -Converter_DCDC:TBA1-0511E -Converter_DCDC:TBA1-0512 -Converter_DCDC:TBA1-0512E -Converter_DCDC:TBA1-0513 -Converter_DCDC:TBA1-0513E -Converter_DCDC:TBA1-0519 -Converter_DCDC:TBA1-0521E -Converter_DCDC:TBA1-0522E -Converter_DCDC:TBA1-0523E -Converter_DCDC:TBA1-1211 -Converter_DCDC:TBA1-1211E -Converter_DCDC:TBA1-1212 -Converter_DCDC:TBA1-1212E -Converter_DCDC:TBA1-1213 -Converter_DCDC:TBA1-1213E -Converter_DCDC:TBA1-1219 -Converter_DCDC:TBA1-1221E -Converter_DCDC:TBA1-1222E -Converter_DCDC:TBA1-1223E -Converter_DCDC:TBA1-2411 -Converter_DCDC:TBA1-2411E -Converter_DCDC:TBA1-2412 -Converter_DCDC:TBA1-2412E -Converter_DCDC:TBA1-2413 -Converter_DCDC:TBA1-2413E -Converter_DCDC:TBA1-2419 -Converter_DCDC:TBA1-2421E -Converter_DCDC:TBA1-2422E -Converter_DCDC:TBA1-2423E -Converter_DCDC:TBA2-0511 -Converter_DCDC:TBA2-0512 -Converter_DCDC:TBA2-0513 -Converter_DCDC:TBA2-0521 -Converter_DCDC:TBA2-0522 -Converter_DCDC:TBA2-0523 -Converter_DCDC:TBA2-1211 -Converter_DCDC:TBA2-1212 -Converter_DCDC:TBA2-1213 -Converter_DCDC:TBA2-1221 -Converter_DCDC:TBA2-1222 -Converter_DCDC:TBA2-1223 -Converter_DCDC:TBA2-2411 -Converter_DCDC:TBA2-2412 -Converter_DCDC:TBA2-2413 -Converter_DCDC:TBA2-2421 -Converter_DCDC:TBA2-2422 -Converter_DCDC:TBA2-2423 Converter_DCDC:TC7662AxPA Converter_DCDC:TC7662Bx0A Converter_DCDC:TC7662BxPA -Converter_DCDC:TDU1-0511 -Converter_DCDC:TDU1-0512 -Converter_DCDC:TDU1-0513 -Converter_DCDC:TDU1-1211 -Converter_DCDC:TDU1-1212 -Converter_DCDC:TDU1-1213 -Converter_DCDC:TDU1-2411 -Converter_DCDC:TDU1-2412 -Converter_DCDC:TDU1-2413 -Converter_DCDC:TEA1-0505 -Converter_DCDC:TEA1-0505E -Converter_DCDC:TEA1-0505HI -Converter_DCDC:TEC2-1210WI -Converter_DCDC:TEC2-1211WI -Converter_DCDC:TEC2-1212WI -Converter_DCDC:TEC2-1213WI -Converter_DCDC:TEC2-1215WI -Converter_DCDC:TEC2-1219WI -Converter_DCDC:TEC2-1221WI -Converter_DCDC:TEC2-1222WI -Converter_DCDC:TEC2-1223WI -Converter_DCDC:TEC2-2410WI -Converter_DCDC:TEC2-2411WI -Converter_DCDC:TEC2-2412WI -Converter_DCDC:TEC2-2413WI -Converter_DCDC:TEC2-2415WI -Converter_DCDC:TEC2-2419WI -Converter_DCDC:TEC2-2421WI -Converter_DCDC:TEC2-2422WI -Converter_DCDC:TEC2-2423WI -Converter_DCDC:TEC2-4810WI -Converter_DCDC:TEC2-4811WI -Converter_DCDC:TEC2-4812WI -Converter_DCDC:TEC2-4813WI -Converter_DCDC:TEC2-4815WI -Converter_DCDC:TEC2-4819WI -Converter_DCDC:TEC2-4821WI -Converter_DCDC:TEC2-4822WI -Converter_DCDC:TEC2-4823WI -Converter_DCDC:TEC3-2410UI -Converter_DCDC:TEC3-2411UI -Converter_DCDC:TEC3-2412UI -Converter_DCDC:TEC3-2413UI -Converter_DCDC:TEC3-2421UI -Converter_DCDC:TEC3-2422UI -Converter_DCDC:TEC3-2423UI -Converter_DCDC:TEC6-2410UI -Converter_DCDC:TEC6-2411UI -Converter_DCDC:TEC6-2412UI -Converter_DCDC:TEC6-2413UI -Converter_DCDC:TEC6-2415UI -Converter_DCDC:TEC6-2419UI -Converter_DCDC:TEC6-2421UI -Converter_DCDC:TEC6-2422UI -Converter_DCDC:TEC6-2423UI -Converter_DCDC:TEL12-1211 -Converter_DCDC:TEL12-1212 -Converter_DCDC:TEL12-1213 -Converter_DCDC:TEL12-1215 -Converter_DCDC:TEL12-1222 -Converter_DCDC:TEL12-1223 -Converter_DCDC:TEL12-2411 -Converter_DCDC:TEL12-2411WI -Converter_DCDC:TEL12-2412 -Converter_DCDC:TEL12-2412WI -Converter_DCDC:TEL12-2413 -Converter_DCDC:TEL12-2413WI -Converter_DCDC:TEL12-2415 -Converter_DCDC:TEL12-2415WI -Converter_DCDC:TEL12-2422 -Converter_DCDC:TEL12-2422WI -Converter_DCDC:TEL12-2423 -Converter_DCDC:TEL12-2423WI -Converter_DCDC:TEL12-4811 -Converter_DCDC:TEL12-4811WI -Converter_DCDC:TEL12-4812 -Converter_DCDC:TEL12-4812WI -Converter_DCDC:TEL12-4813 -Converter_DCDC:TEL12-4813WI -Converter_DCDC:TEL12-4815 -Converter_DCDC:TEL12-4815WI -Converter_DCDC:TEL12-4822 -Converter_DCDC:TEL12-4822WI -Converter_DCDC:TEL12-4823 -Converter_DCDC:TEL12-4823WI -Converter_DCDC:TEN10-11010WIRH -Converter_DCDC:TEN10-11011WIRH -Converter_DCDC:TEN10-11012WIRH -Converter_DCDC:TEN10-11013WIRH -Converter_DCDC:TEN10-11015WIRH -Converter_DCDC:TEN10-11021WIRH -Converter_DCDC:TEN10-11022WIRH -Converter_DCDC:TEN10-11023WIRH -Converter_DCDC:TEN20-11011WIRH -Converter_DCDC:TEN20-11012WIRH -Converter_DCDC:TEN20-11013WIRH -Converter_DCDC:TEN20-11015WIRH -Converter_DCDC:TEN20-11021WIRH -Converter_DCDC:TEN20-11022WIRH -Converter_DCDC:TEN20-11023WIRH -Converter_DCDC:TEN20-2410WIN -Converter_DCDC:TEN20-2411WIN -Converter_DCDC:TEN20-2412WIN -Converter_DCDC:TEN20-2413WIN -Converter_DCDC:TEN20-2421WIN -Converter_DCDC:TEN20-2422WIN -Converter_DCDC:TEN20-2423WIN -Converter_DCDC:TEN20-4810WIN -Converter_DCDC:TEN20-4811WIN -Converter_DCDC:TEN20-4812WIN -Converter_DCDC:TEN20-4813WIN -Converter_DCDC:TEN20-4821WIN -Converter_DCDC:TEN20-4822WIN -Converter_DCDC:TEN20-4823WIN -Converter_DCDC:TEN3-11010WIRH -Converter_DCDC:TEN3-11011WIRH -Converter_DCDC:TEN3-11012WIRH -Converter_DCDC:TEN3-11013WIRH -Converter_DCDC:TEN3-11015WIRH -Converter_DCDC:TEN3-11021WIRH -Converter_DCDC:TEN3-11022WIRH -Converter_DCDC:TEN3-11023WIRH -Converter_DCDC:TEN40-11011WIRH -Converter_DCDC:TEN40-11012WIRH -Converter_DCDC:TEN40-11013WIRH -Converter_DCDC:TEN40-11015WIRH -Converter_DCDC:TEN40-11022WIRH -Converter_DCDC:TEN40-11023WIRH -Converter_DCDC:TEN6-11010WIRH -Converter_DCDC:TEN6-11011WIRH -Converter_DCDC:TEN6-11012WIRH -Converter_DCDC:TEN6-11013WIRH -Converter_DCDC:TEN6-11015WIRH -Converter_DCDC:TEN6-11021WIRH -Converter_DCDC:TEN6-11022WIRH -Converter_DCDC:TEN6-11023WIRH -Converter_DCDC:TEP150-7211UIR -Converter_DCDC:TEP150-7212UIR -Converter_DCDC:TEP150-7213UIR -Converter_DCDC:TEP150-7215UIR -Converter_DCDC:TEP150-7218UIR -Converter_DCDC:TEP200-7211UIR -Converter_DCDC:TEP200-7212UIR -Converter_DCDC:TEP200-7213UIR -Converter_DCDC:TEP200-7215UIR -Converter_DCDC:TEP200-7218UIR -Converter_DCDC:TES1-0510 -Converter_DCDC:TES1-0511 -Converter_DCDC:TES1-0512 -Converter_DCDC:TES1-0513 -Converter_DCDC:TES1-0519 -Converter_DCDC:TES1-0521 -Converter_DCDC:TES1-0522 -Converter_DCDC:TES1-0523 -Converter_DCDC:TES1-1211 -Converter_DCDC:TES1-1212 -Converter_DCDC:TES1-1213 -Converter_DCDC:TES1-1219 -Converter_DCDC:TES1-1221 -Converter_DCDC:TES1-1222 -Converter_DCDC:TES1-1223 -Converter_DCDC:TES1-2411 -Converter_DCDC:TES1-2412 -Converter_DCDC:TES1-2413 -Converter_DCDC:TES1-2419 -Converter_DCDC:TES1-2421 -Converter_DCDC:TES1-2422 -Converter_DCDC:TES1-2423 -Converter_DCDC:THB10-1211 -Converter_DCDC:THB10-1212 -Converter_DCDC:THB10-1222 -Converter_DCDC:THB10-1223 -Converter_DCDC:THB10-2411 -Converter_DCDC:THB10-2412 -Converter_DCDC:THB10-2422 -Converter_DCDC:THB10-2423 -Converter_DCDC:THB10-4811 -Converter_DCDC:THB10-4812 -Converter_DCDC:THB10-4822 -Converter_DCDC:THB10-4823 -Converter_DCDC:THL30-2410WI -Converter_DCDC:THL30-2411WI -Converter_DCDC:THL30-2412WI -Converter_DCDC:THL30-2413WI -Converter_DCDC:THL30-2415WI -Converter_DCDC:THL30-2422WI -Converter_DCDC:THL30-2423WI -Converter_DCDC:THL30-4810WI -Converter_DCDC:THL30-4811WI -Converter_DCDC:THL30-4812WI -Converter_DCDC:THL30-4813WI -Converter_DCDC:THL30-4815WI -Converter_DCDC:THL30-4822WI -Converter_DCDC:THL30-4823WI -Converter_DCDC:THL40-2411WI -Converter_DCDC:THL40-2412WI -Converter_DCDC:THL40-2413WI -Converter_DCDC:THL40-2415WI -Converter_DCDC:THL40-2422WI -Converter_DCDC:THL40-2423WI -Converter_DCDC:THL40-4811WI -Converter_DCDC:THL40-4812WI -Converter_DCDC:THL40-4813WI -Converter_DCDC:THL40-4815WI -Converter_DCDC:THL40-4822WI -Converter_DCDC:THL40-4823WI -Converter_DCDC:THN10-3610UIR -Converter_DCDC:THN10-3611UIR -Converter_DCDC:THN10-3612UIR -Converter_DCDC:THN10-3613UIR -Converter_DCDC:THN10-3615UIR -Converter_DCDC:THN10-3621UIR -Converter_DCDC:THN10-3622UIR -Converter_DCDC:THN10-3623UIR -Converter_DCDC:THN10-7210UIR -Converter_DCDC:THN10-7211UIR -Converter_DCDC:THN10-7212UIR -Converter_DCDC:THN10-7213UIR -Converter_DCDC:THN10-7215UIR -Converter_DCDC:THN10-7221UIR -Converter_DCDC:THN10-7222UIR -Converter_DCDC:THN10-7223UIR -Converter_DCDC:THR40-7211WI -Converter_DCDC:THR40-7212WI -Converter_DCDC:THR40-7213WI -Converter_DCDC:THR40-72154WI -Converter_DCDC:THR40-7215WI -Converter_DCDC:THR40-7222WI -Converter_DCDC:THR40-7223WI -Converter_DCDC:TMA-0505D -Converter_DCDC:TMA-0505S -Converter_DCDC:TMA-0512D -Converter_DCDC:TMA-0512S -Converter_DCDC:TMA-0515D -Converter_DCDC:TMA-0515S -Converter_DCDC:TMA-1205D -Converter_DCDC:TMA-1205S -Converter_DCDC:TMA-1212D -Converter_DCDC:TMA-1212S -Converter_DCDC:TMA-1215D -Converter_DCDC:TMA-1215S -Converter_DCDC:TMA-1505D -Converter_DCDC:TMA-1505S -Converter_DCDC:TMA-1512D -Converter_DCDC:TMA-1512S -Converter_DCDC:TMA-1515D -Converter_DCDC:TMA-1515S -Converter_DCDC:TMA-2405D -Converter_DCDC:TMA-2405S -Converter_DCDC:TMA-2412D -Converter_DCDC:TMA-2412S -Converter_DCDC:TMA-2415D -Converter_DCDC:TMA-2415S -Converter_DCDC:TME-0303S -Converter_DCDC:TME-0305S -Converter_DCDC:TME-0503S -Converter_DCDC:TME-0505S -Converter_DCDC:TME-0509S -Converter_DCDC:TME-0512S -Converter_DCDC:TME-0515S -Converter_DCDC:TME-1205S -Converter_DCDC:TME-1209S -Converter_DCDC:TME-1212S -Converter_DCDC:TME-1215S -Converter_DCDC:TME-2405S -Converter_DCDC:TME-2409S -Converter_DCDC:TME-2412S -Converter_DCDC:TME-2415S -Converter_DCDC:TMR-0510 -Converter_DCDC:TMR-0511 -Converter_DCDC:TMR-0512 -Converter_DCDC:TMR-0521 -Converter_DCDC:TMR-0522 -Converter_DCDC:TMR-0523 -Converter_DCDC:TMR-1210 -Converter_DCDC:TMR-1211 -Converter_DCDC:TMR-1212 -Converter_DCDC:TMR-1221 -Converter_DCDC:TMR-1222 -Converter_DCDC:TMR-1223 -Converter_DCDC:TMR-2410 -Converter_DCDC:TMR-2411 -Converter_DCDC:TMR-2412 -Converter_DCDC:TMR-2421 -Converter_DCDC:TMR-2422 -Converter_DCDC:TMR-2423 -Converter_DCDC:TMR-4810 -Converter_DCDC:TMR-4811 -Converter_DCDC:TMR-4812 -Converter_DCDC:TMR-4821 -Converter_DCDC:TMR-4822 -Converter_DCDC:TMR-4823 -Converter_DCDC:TMR10-2410WIR -Converter_DCDC:TMR10-2411WIR -Converter_DCDC:TMR10-2412WIR -Converter_DCDC:TMR10-2413WIR -Converter_DCDC:TMR10-2415WIR -Converter_DCDC:TMR10-2421WIR -Converter_DCDC:TMR10-2422WIR -Converter_DCDC:TMR10-2423WIR -Converter_DCDC:TMR10-4810WIR -Converter_DCDC:TMR10-4811WIR -Converter_DCDC:TMR10-4812WIR -Converter_DCDC:TMR10-4813WIR -Converter_DCDC:TMR10-4815WIR -Converter_DCDC:TMR10-4821WIR -Converter_DCDC:TMR10-4822WIR -Converter_DCDC:TMR10-4823WIR -Converter_DCDC:TMR10-7210WIR -Converter_DCDC:TMR10-7211WIR -Converter_DCDC:TMR10-7212WIR -Converter_DCDC:TMR10-7213WIR -Converter_DCDC:TMR10-7215WIR -Converter_DCDC:TMR10-7221WIR -Converter_DCDC:TMR10-7222WIR -Converter_DCDC:TMR10-7223WIR -Converter_DCDC:TMR2-2410WI -Converter_DCDC:TMR2-2411WI -Converter_DCDC:TMR2-2412WI -Converter_DCDC:TMR2-2413WI -Converter_DCDC:TMR2-2421WI -Converter_DCDC:TMR2-2422WI -Converter_DCDC:TMR2-2423WI -Converter_DCDC:TMR2-4810WI -Converter_DCDC:TMR2-4811WI -Converter_DCDC:TMR2-4812WI -Converter_DCDC:TMR2-4813WI -Converter_DCDC:TMR2-4821WI -Converter_DCDC:TMR2-4822WI -Converter_DCDC:TMR2-4823WI -Converter_DCDC:TMR4-2411WI -Converter_DCDC:TMR4-2412WI -Converter_DCDC:TMR4-2413WI -Converter_DCDC:TMR4-2415WI -Converter_DCDC:TMR4-2422WI -Converter_DCDC:TMR4-2423WI -Converter_DCDC:TMR4-4811WI -Converter_DCDC:TMR4-4812WI -Converter_DCDC:TMR4-4813WI -Converter_DCDC:TMR4-4815WI -Converter_DCDC:TMR4-4822WI -Converter_DCDC:TMR4-4823WI -Converter_DCDC:TMU3-0511 -Converter_DCDC:TMU3-0512 -Converter_DCDC:TMU3-0513 -Converter_DCDC:TMU3-1211 -Converter_DCDC:TMU3-1212 -Converter_DCDC:TMU3-1213 -Converter_DCDC:TMU3-2411 -Converter_DCDC:TMU3-2412 -Converter_DCDC:TMU3-2413 -Converter_DCDC:TMV0505D -Converter_DCDC:TMV0505S -Converter_DCDC:TMV0509S -Converter_DCDC:TMV0512D -Converter_DCDC:TMV0512S -Converter_DCDC:TMV0515D -Converter_DCDC:TMV0515S -Converter_DCDC:TMV1205D -Converter_DCDC:TMV1205S -Converter_DCDC:TMV1212D -Converter_DCDC:TMV1212S -Converter_DCDC:TMV1215D -Converter_DCDC:TMV1215S -Converter_DCDC:TMV2405D -Converter_DCDC:TMV2405S -Converter_DCDC:TMV2412D -Converter_DCDC:TMV2412S -Converter_DCDC:TMV2415D -Converter_DCDC:TMV2415S Converter_DCDC:TPS43060RTE Converter_DCDC:TPS54240DGQ Converter_DCDC:TPS54240DRC @@ -5388,131 +4509,6 @@ Converter_DCDC:TPS82150 Converter_DCDC:TPSM53602RDA Converter_DCDC:TPSM53603RDA Converter_DCDC:TPSM53604RDA -Converter_DCDC:TRA3-0511 -Converter_DCDC:TRA3-0512 -Converter_DCDC:TRA3-0513 -Converter_DCDC:TRA3-0519 -Converter_DCDC:TRA3-1211 -Converter_DCDC:TRA3-1212 -Converter_DCDC:TRA3-1213 -Converter_DCDC:TRA3-1219 -Converter_DCDC:TRA3-2411 -Converter_DCDC:TRA3-2412 -Converter_DCDC:TRA3-2413 -Converter_DCDC:TRA3-2419 -Converter_DCDC:TRI1-0511 -Converter_DCDC:TRI1-0511SM -Converter_DCDC:TRI1-0512 -Converter_DCDC:TRI1-0512SM -Converter_DCDC:TRI1-0513 -Converter_DCDC:TRI1-0513SM -Converter_DCDC:TRI1-0522SM -Converter_DCDC:TRI1-0523SM -Converter_DCDC:TRI1-1211 -Converter_DCDC:TRI1-1211SM -Converter_DCDC:TRI1-1212 -Converter_DCDC:TRI1-1212SM -Converter_DCDC:TRI1-1213 -Converter_DCDC:TRI1-1213SM -Converter_DCDC:TRI1-1222SM -Converter_DCDC:TRI1-1223SM -Converter_DCDC:TRI1-2411 -Converter_DCDC:TRI1-2411SM -Converter_DCDC:TRI1-2412 -Converter_DCDC:TRI1-2412SM -Converter_DCDC:TRI1-2413 -Converter_DCDC:TRI1-2413SM -Converter_DCDC:TRI1-2422SM -Converter_DCDC:TRI1-2423SM -Converter_DCDC:TRI10-1210 -Converter_DCDC:TRI10-1211 -Converter_DCDC:TRI10-1212 -Converter_DCDC:TRI10-1213 -Converter_DCDC:TRI10-1215 -Converter_DCDC:TRI10-1222 -Converter_DCDC:TRI10-1223 -Converter_DCDC:TRI10-2410 -Converter_DCDC:TRI10-2411 -Converter_DCDC:TRI10-2412 -Converter_DCDC:TRI10-2413 -Converter_DCDC:TRI10-2415 -Converter_DCDC:TRI10-2422 -Converter_DCDC:TRI10-2423 -Converter_DCDC:TRI10-4810 -Converter_DCDC:TRI10-4811 -Converter_DCDC:TRI10-4812 -Converter_DCDC:TRI10-4813 -Converter_DCDC:TRI10-4815 -Converter_DCDC:TRI10-4822 -Converter_DCDC:TRI10-4823 -Converter_DCDC:TRI15-1211 -Converter_DCDC:TRI15-1212 -Converter_DCDC:TRI15-1213 -Converter_DCDC:TRI15-1215 -Converter_DCDC:TRI15-1222 -Converter_DCDC:TRI15-1223 -Converter_DCDC:TRI15-2411 -Converter_DCDC:TRI15-2412 -Converter_DCDC:TRI15-2413 -Converter_DCDC:TRI15-2415 -Converter_DCDC:TRI15-2422 -Converter_DCDC:TRI15-2423 -Converter_DCDC:TRI15-4811 -Converter_DCDC:TRI15-4812 -Converter_DCDC:TRI15-4813 -Converter_DCDC:TRI15-4815 -Converter_DCDC:TRI15-4822 -Converter_DCDC:TRI15-4823 -Converter_DCDC:TRI20-1211 -Converter_DCDC:TRI20-1212 -Converter_DCDC:TRI20-1213 -Converter_DCDC:TRI20-1215 -Converter_DCDC:TRI20-1222 -Converter_DCDC:TRI20-1223 -Converter_DCDC:TRI20-2411 -Converter_DCDC:TRI20-2412 -Converter_DCDC:TRI20-2413 -Converter_DCDC:TRI20-2415 -Converter_DCDC:TRI20-2422 -Converter_DCDC:TRI20-2423 -Converter_DCDC:TRI20-4811 -Converter_DCDC:TRI20-4812 -Converter_DCDC:TRI20-4813 -Converter_DCDC:TRI20-4815 -Converter_DCDC:TRI20-4822 -Converter_DCDC:TRI20-4823 -Converter_DCDC:TRN3-0510 -Converter_DCDC:TRN3-0511 -Converter_DCDC:TRN3-0512 -Converter_DCDC:TRN3-0513 -Converter_DCDC:TRN3-0515 -Converter_DCDC:TRN3-0521 -Converter_DCDC:TRN3-0522 -Converter_DCDC:TRN3-0523 -Converter_DCDC:TRN3-1210 -Converter_DCDC:TRN3-1211 -Converter_DCDC:TRN3-1212 -Converter_DCDC:TRN3-1213 -Converter_DCDC:TRN3-1215 -Converter_DCDC:TRN3-1221 -Converter_DCDC:TRN3-1222 -Converter_DCDC:TRN3-1223 -Converter_DCDC:TRN3-2410 -Converter_DCDC:TRN3-2411 -Converter_DCDC:TRN3-2412 -Converter_DCDC:TRN3-2413 -Converter_DCDC:TRN3-2415 -Converter_DCDC:TRN3-2421 -Converter_DCDC:TRN3-2422 -Converter_DCDC:TRN3-2423 -Converter_DCDC:TRN3-4810 -Converter_DCDC:TRN3-4811 -Converter_DCDC:TRN3-4812 -Converter_DCDC:TRN3-4813 -Converter_DCDC:TRN3-4815 -Converter_DCDC:TRN3-4821 -Converter_DCDC:TRN3-4822 -Converter_DCDC:TRN3-4823 Converter_DCDC:TSR0.5-24120 Converter_DCDC:TSR0.5-24120SM Converter_DCDC:TSR0.5-2415 @@ -5587,6 +4583,1146 @@ Converter_DCDC:TSR_1-2433 Converter_DCDC:TSR_1-2450 Converter_DCDC:TSR_1-2465 Converter_DCDC:TSR_1-2490 +Converter_DCDC_Isolated:ADuM6000 +Converter_DCDC_Isolated:ATA00A18S-L +Converter_DCDC_Isolated:ATA00A36S-L +Converter_DCDC_Isolated:ATA00AA18S-L +Converter_DCDC_Isolated:ATA00AA36S-L +Converter_DCDC_Isolated:ATA00B18S-L +Converter_DCDC_Isolated:ATA00B36S-L +Converter_DCDC_Isolated:ATA00BB18S-L +Converter_DCDC_Isolated:ATA00BB36S-L +Converter_DCDC_Isolated:ATA00C18S-L +Converter_DCDC_Isolated:ATA00C36S-L +Converter_DCDC_Isolated:ATA00CC18S-L +Converter_DCDC_Isolated:ATA00CC36S-L +Converter_DCDC_Isolated:ATA00F18S-L +Converter_DCDC_Isolated:ATA00F36S-L +Converter_DCDC_Isolated:ATA00H18S-L +Converter_DCDC_Isolated:ATA00H36S-L +Converter_DCDC_Isolated:Ag9905LP +Converter_DCDC_Isolated:CRE1S0305S3C +Converter_DCDC_Isolated:CRE1S0505DC +Converter_DCDC_Isolated:CRE1S0505S3C +Converter_DCDC_Isolated:CRE1S0505SC +Converter_DCDC_Isolated:CRE1S0515SC +Converter_DCDC_Isolated:CRE1S1205SC +Converter_DCDC_Isolated:CRE1S1212SC +Converter_DCDC_Isolated:CRE1S2405SC +Converter_DCDC_Isolated:CRE1S2412SC +Converter_DCDC_Isolated:IA0305D +Converter_DCDC_Isolated:IA0305S +Converter_DCDC_Isolated:IA0503D +Converter_DCDC_Isolated:IA0503S +Converter_DCDC_Isolated:IA0505D +Converter_DCDC_Isolated:IA0505S +Converter_DCDC_Isolated:IA0509D +Converter_DCDC_Isolated:IA0509S +Converter_DCDC_Isolated:IA0512D +Converter_DCDC_Isolated:IA0512S +Converter_DCDC_Isolated:IA0515D +Converter_DCDC_Isolated:IA0515S +Converter_DCDC_Isolated:IA0524D +Converter_DCDC_Isolated:IA0524S +Converter_DCDC_Isolated:IA1203D +Converter_DCDC_Isolated:IA1203S +Converter_DCDC_Isolated:IA1205D +Converter_DCDC_Isolated:IA1205S +Converter_DCDC_Isolated:IA1209D +Converter_DCDC_Isolated:IA1209S +Converter_DCDC_Isolated:IA1212D +Converter_DCDC_Isolated:IA1212S +Converter_DCDC_Isolated:IA1215D +Converter_DCDC_Isolated:IA1215S +Converter_DCDC_Isolated:IA1224D +Converter_DCDC_Isolated:IA1224S +Converter_DCDC_Isolated:IA2403D +Converter_DCDC_Isolated:IA2403S +Converter_DCDC_Isolated:IA2405D +Converter_DCDC_Isolated:IA2405S +Converter_DCDC_Isolated:IA2409D +Converter_DCDC_Isolated:IA2409S +Converter_DCDC_Isolated:IA2412D +Converter_DCDC_Isolated:IA2412S +Converter_DCDC_Isolated:IA2415D +Converter_DCDC_Isolated:IA2415S +Converter_DCDC_Isolated:IA2424D +Converter_DCDC_Isolated:IA2424S +Converter_DCDC_Isolated:IA4803D +Converter_DCDC_Isolated:IA4803S +Converter_DCDC_Isolated:IA4805D +Converter_DCDC_Isolated:IA4805S +Converter_DCDC_Isolated:IA4809D +Converter_DCDC_Isolated:IA4809S +Converter_DCDC_Isolated:IA4812D +Converter_DCDC_Isolated:IA4812S +Converter_DCDC_Isolated:IA4815D +Converter_DCDC_Isolated:IA4815S +Converter_DCDC_Isolated:IA4824D +Converter_DCDC_Isolated:IA4824S +Converter_DCDC_Isolated:IH0503D +Converter_DCDC_Isolated:IH0503DH +Converter_DCDC_Isolated:IH0503S +Converter_DCDC_Isolated:IH0503SH +Converter_DCDC_Isolated:IH0505D +Converter_DCDC_Isolated:IH0505DH +Converter_DCDC_Isolated:IH0505S +Converter_DCDC_Isolated:IH0505SH +Converter_DCDC_Isolated:IH0509D +Converter_DCDC_Isolated:IH0509DH +Converter_DCDC_Isolated:IH0509S +Converter_DCDC_Isolated:IH0509SH +Converter_DCDC_Isolated:IH0512D +Converter_DCDC_Isolated:IH0512DH +Converter_DCDC_Isolated:IH0512S +Converter_DCDC_Isolated:IH0512SH +Converter_DCDC_Isolated:IH0515D +Converter_DCDC_Isolated:IH0515DH +Converter_DCDC_Isolated:IH0515S +Converter_DCDC_Isolated:IH0515SH +Converter_DCDC_Isolated:IH0524D +Converter_DCDC_Isolated:IH0524DH +Converter_DCDC_Isolated:IH0524S +Converter_DCDC_Isolated:IH0524SH +Converter_DCDC_Isolated:IH1203D +Converter_DCDC_Isolated:IH1203DH +Converter_DCDC_Isolated:IH1203S +Converter_DCDC_Isolated:IH1203SH +Converter_DCDC_Isolated:IH1205D +Converter_DCDC_Isolated:IH1205DH +Converter_DCDC_Isolated:IH1205S +Converter_DCDC_Isolated:IH1205SH +Converter_DCDC_Isolated:IH1209D +Converter_DCDC_Isolated:IH1209DH +Converter_DCDC_Isolated:IH1209S +Converter_DCDC_Isolated:IH1209SH +Converter_DCDC_Isolated:IH1212D +Converter_DCDC_Isolated:IH1212DH +Converter_DCDC_Isolated:IH1212S +Converter_DCDC_Isolated:IH1212SH +Converter_DCDC_Isolated:IH1215D +Converter_DCDC_Isolated:IH1215DH +Converter_DCDC_Isolated:IH1215S +Converter_DCDC_Isolated:IH1215SH +Converter_DCDC_Isolated:IH1224D +Converter_DCDC_Isolated:IH1224DH +Converter_DCDC_Isolated:IH1224S +Converter_DCDC_Isolated:IH1224SH +Converter_DCDC_Isolated:IH2403D +Converter_DCDC_Isolated:IH2403DH +Converter_DCDC_Isolated:IH2403S +Converter_DCDC_Isolated:IH2403SH +Converter_DCDC_Isolated:IH2405D +Converter_DCDC_Isolated:IH2405DH +Converter_DCDC_Isolated:IH2405S +Converter_DCDC_Isolated:IH2405SH +Converter_DCDC_Isolated:IH2409D +Converter_DCDC_Isolated:IH2409DH +Converter_DCDC_Isolated:IH2409S +Converter_DCDC_Isolated:IH2409SH +Converter_DCDC_Isolated:IH2412D +Converter_DCDC_Isolated:IH2412DH +Converter_DCDC_Isolated:IH2412S +Converter_DCDC_Isolated:IH2412SH +Converter_DCDC_Isolated:IH2415D +Converter_DCDC_Isolated:IH2415DH +Converter_DCDC_Isolated:IH2415S +Converter_DCDC_Isolated:IH2415SH +Converter_DCDC_Isolated:IH2424D +Converter_DCDC_Isolated:IH2424DH +Converter_DCDC_Isolated:IH2424S +Converter_DCDC_Isolated:IH2424SH +Converter_DCDC_Isolated:IH4803D +Converter_DCDC_Isolated:IH4803DH +Converter_DCDC_Isolated:IH4803S +Converter_DCDC_Isolated:IH4803SH +Converter_DCDC_Isolated:IH4805D +Converter_DCDC_Isolated:IH4805DH +Converter_DCDC_Isolated:IH4805S +Converter_DCDC_Isolated:IH4805SH +Converter_DCDC_Isolated:IH4809D +Converter_DCDC_Isolated:IH4809DH +Converter_DCDC_Isolated:IH4809S +Converter_DCDC_Isolated:IH4809SH +Converter_DCDC_Isolated:IH4812D +Converter_DCDC_Isolated:IH4812DH +Converter_DCDC_Isolated:IH4812S +Converter_DCDC_Isolated:IH4812SH +Converter_DCDC_Isolated:IH4815D +Converter_DCDC_Isolated:IH4815DH +Converter_DCDC_Isolated:IH4815S +Converter_DCDC_Isolated:IH4815SH +Converter_DCDC_Isolated:IH4824D +Converter_DCDC_Isolated:IH4824DH +Converter_DCDC_Isolated:IH4824S +Converter_DCDC_Isolated:IH4824SH +Converter_DCDC_Isolated:ISU0205D12 +Converter_DCDC_Isolated:ISU0205D15 +Converter_DCDC_Isolated:ISU0205S05 +Converter_DCDC_Isolated:ISU0205S12 +Converter_DCDC_Isolated:ISU0205S15 +Converter_DCDC_Isolated:ISU0205S24 +Converter_DCDC_Isolated:ISU0224D12 +Converter_DCDC_Isolated:ISU0224D15 +Converter_DCDC_Isolated:ISU0224S05 +Converter_DCDC_Isolated:ISU0224S12 +Converter_DCDC_Isolated:ISU0224S15 +Converter_DCDC_Isolated:ISU0224S24 +Converter_DCDC_Isolated:ISU0248D12 +Converter_DCDC_Isolated:ISU0248D15 +Converter_DCDC_Isolated:ISU0248S05 +Converter_DCDC_Isolated:ISU0248S12 +Converter_DCDC_Isolated:ISU0248S15 +Converter_DCDC_Isolated:ISU0248S24 +Converter_DCDC_Isolated:ITQ2403SA +Converter_DCDC_Isolated:ITQ2403SA-H +Converter_DCDC_Isolated:ITQ2405S +Converter_DCDC_Isolated:ITQ2405S-H +Converter_DCDC_Isolated:ITQ2405SA +Converter_DCDC_Isolated:ITQ2405SA-H +Converter_DCDC_Isolated:ITQ2409SA +Converter_DCDC_Isolated:ITQ2409SA-H +Converter_DCDC_Isolated:ITQ2412S +Converter_DCDC_Isolated:ITQ2412S-H +Converter_DCDC_Isolated:ITQ2412SA +Converter_DCDC_Isolated:ITQ2412SA-H +Converter_DCDC_Isolated:ITQ2415S +Converter_DCDC_Isolated:ITQ2415S-H +Converter_DCDC_Isolated:ITQ2415SA +Converter_DCDC_Isolated:ITQ2415SA-H +Converter_DCDC_Isolated:ITQ2424SA +Converter_DCDC_Isolated:ITQ2424SA-H +Converter_DCDC_Isolated:ITQ4803SA +Converter_DCDC_Isolated:ITQ4803SA-H +Converter_DCDC_Isolated:ITQ4805S +Converter_DCDC_Isolated:ITQ4805S-H +Converter_DCDC_Isolated:ITQ4805SA +Converter_DCDC_Isolated:ITQ4805SA-H +Converter_DCDC_Isolated:ITQ4809SA +Converter_DCDC_Isolated:ITQ4809SA-H +Converter_DCDC_Isolated:ITQ4812S +Converter_DCDC_Isolated:ITQ4812S-H +Converter_DCDC_Isolated:ITQ4812SA +Converter_DCDC_Isolated:ITQ4812SA-H +Converter_DCDC_Isolated:ITQ4815S +Converter_DCDC_Isolated:ITQ4815S-H +Converter_DCDC_Isolated:ITQ4815SA +Converter_DCDC_Isolated:ITQ4815SA-H +Converter_DCDC_Isolated:ITQ4824SA +Converter_DCDC_Isolated:ITQ4824SA-H +Converter_DCDC_Isolated:ITX0503SA +Converter_DCDC_Isolated:ITX0503SA-H +Converter_DCDC_Isolated:ITX0503SA-HR +Converter_DCDC_Isolated:ITX0503SA-R +Converter_DCDC_Isolated:ITX0505S +Converter_DCDC_Isolated:ITX0505S-H +Converter_DCDC_Isolated:ITX0505S-HR +Converter_DCDC_Isolated:ITX0505S-R +Converter_DCDC_Isolated:ITX0505SA +Converter_DCDC_Isolated:ITX0505SA-H +Converter_DCDC_Isolated:ITX0505SA-HR +Converter_DCDC_Isolated:ITX0505SA-R +Converter_DCDC_Isolated:ITX0509SA +Converter_DCDC_Isolated:ITX0509SA-H +Converter_DCDC_Isolated:ITX0509SA-HR +Converter_DCDC_Isolated:ITX0509SA-R +Converter_DCDC_Isolated:ITX0512S +Converter_DCDC_Isolated:ITX0512S-H +Converter_DCDC_Isolated:ITX0512S-HR +Converter_DCDC_Isolated:ITX0512S-R +Converter_DCDC_Isolated:ITX0512SA +Converter_DCDC_Isolated:ITX0512SA-H +Converter_DCDC_Isolated:ITX0512SA-HR +Converter_DCDC_Isolated:ITX0512SA-R +Converter_DCDC_Isolated:ITX0515S +Converter_DCDC_Isolated:ITX0515S-H +Converter_DCDC_Isolated:ITX0515S-HR +Converter_DCDC_Isolated:ITX0515S-R +Converter_DCDC_Isolated:ITX0515SA +Converter_DCDC_Isolated:ITX0515SA-H +Converter_DCDC_Isolated:ITX0515SA-HR +Converter_DCDC_Isolated:ITX0515SA-R +Converter_DCDC_Isolated:ITX0524SA +Converter_DCDC_Isolated:ITX0524SA-H +Converter_DCDC_Isolated:ITX0524SA-HR +Converter_DCDC_Isolated:ITX0524SA-R +Converter_DCDC_Isolated:ITX1203SA +Converter_DCDC_Isolated:ITX1203SA-H +Converter_DCDC_Isolated:ITX1203SA-HR +Converter_DCDC_Isolated:ITX1203SA-R +Converter_DCDC_Isolated:ITX1205S +Converter_DCDC_Isolated:ITX1205S-H +Converter_DCDC_Isolated:ITX1205S-HR +Converter_DCDC_Isolated:ITX1205S-R +Converter_DCDC_Isolated:ITX1205SA +Converter_DCDC_Isolated:ITX1205SA-H +Converter_DCDC_Isolated:ITX1205SA-HR +Converter_DCDC_Isolated:ITX1205SA-R +Converter_DCDC_Isolated:ITX1209SA +Converter_DCDC_Isolated:ITX1209SA-H +Converter_DCDC_Isolated:ITX1209SA-HR +Converter_DCDC_Isolated:ITX1209SA-R +Converter_DCDC_Isolated:ITX1212S +Converter_DCDC_Isolated:ITX1212S-H +Converter_DCDC_Isolated:ITX1212S-HR +Converter_DCDC_Isolated:ITX1212S-R +Converter_DCDC_Isolated:ITX1212SA +Converter_DCDC_Isolated:ITX1212SA-H +Converter_DCDC_Isolated:ITX1212SA-HR +Converter_DCDC_Isolated:ITX1212SA-R +Converter_DCDC_Isolated:ITX1215S +Converter_DCDC_Isolated:ITX1215S-H +Converter_DCDC_Isolated:ITX1215S-HR +Converter_DCDC_Isolated:ITX1215S-R +Converter_DCDC_Isolated:ITX1215SA +Converter_DCDC_Isolated:ITX1215SA-H +Converter_DCDC_Isolated:ITX1215SA-HR +Converter_DCDC_Isolated:ITX1215SA-R +Converter_DCDC_Isolated:ITX1224SA +Converter_DCDC_Isolated:ITX1224SA-H +Converter_DCDC_Isolated:ITX1224SA-HR +Converter_DCDC_Isolated:ITX1224SA-R +Converter_DCDC_Isolated:ITX2403SA +Converter_DCDC_Isolated:ITX2403SA-H +Converter_DCDC_Isolated:ITX2403SA-HR +Converter_DCDC_Isolated:ITX2403SA-R +Converter_DCDC_Isolated:ITX2405S +Converter_DCDC_Isolated:ITX2405S-H +Converter_DCDC_Isolated:ITX2405S-HR +Converter_DCDC_Isolated:ITX2405S-R +Converter_DCDC_Isolated:ITX2405SA +Converter_DCDC_Isolated:ITX2405SA-H +Converter_DCDC_Isolated:ITX2405SA-HR +Converter_DCDC_Isolated:ITX2405SA-R +Converter_DCDC_Isolated:ITX2409SA +Converter_DCDC_Isolated:ITX2409SA-H +Converter_DCDC_Isolated:ITX2409SA-HR +Converter_DCDC_Isolated:ITX2409SA-R +Converter_DCDC_Isolated:ITX2412S +Converter_DCDC_Isolated:ITX2412S-H +Converter_DCDC_Isolated:ITX2412S-HR +Converter_DCDC_Isolated:ITX2412S-R +Converter_DCDC_Isolated:ITX2412SA +Converter_DCDC_Isolated:ITX2412SA-H +Converter_DCDC_Isolated:ITX2412SA-HR +Converter_DCDC_Isolated:ITX2412SA-R +Converter_DCDC_Isolated:ITX2415S +Converter_DCDC_Isolated:ITX2415S-H +Converter_DCDC_Isolated:ITX2415S-HR +Converter_DCDC_Isolated:ITX2415S-R +Converter_DCDC_Isolated:ITX2415SA +Converter_DCDC_Isolated:ITX2415SA-H +Converter_DCDC_Isolated:ITX2415SA-HR +Converter_DCDC_Isolated:ITX2415SA-R +Converter_DCDC_Isolated:ITX2424SA +Converter_DCDC_Isolated:ITX2424SA-H +Converter_DCDC_Isolated:ITX2424SA-HR +Converter_DCDC_Isolated:ITX2424SA-R +Converter_DCDC_Isolated:ITX4803SA +Converter_DCDC_Isolated:ITX4803SA-H +Converter_DCDC_Isolated:ITX4803SA-HR +Converter_DCDC_Isolated:ITX4803SA-R +Converter_DCDC_Isolated:ITX4805S +Converter_DCDC_Isolated:ITX4805S-H +Converter_DCDC_Isolated:ITX4805S-HR +Converter_DCDC_Isolated:ITX4805S-R +Converter_DCDC_Isolated:ITX4805SA +Converter_DCDC_Isolated:ITX4805SA-H +Converter_DCDC_Isolated:ITX4805SA-HR +Converter_DCDC_Isolated:ITX4805SA-R +Converter_DCDC_Isolated:ITX4809SA +Converter_DCDC_Isolated:ITX4809SA-H +Converter_DCDC_Isolated:ITX4809SA-HR +Converter_DCDC_Isolated:ITX4809SA-R +Converter_DCDC_Isolated:ITX4812S +Converter_DCDC_Isolated:ITX4812S-H +Converter_DCDC_Isolated:ITX4812S-HR +Converter_DCDC_Isolated:ITX4812S-R +Converter_DCDC_Isolated:ITX4812SA +Converter_DCDC_Isolated:ITX4812SA-H +Converter_DCDC_Isolated:ITX4812SA-HR +Converter_DCDC_Isolated:ITX4812SA-R +Converter_DCDC_Isolated:ITX4815S +Converter_DCDC_Isolated:ITX4815S-H +Converter_DCDC_Isolated:ITX4815S-HR +Converter_DCDC_Isolated:ITX4815S-R +Converter_DCDC_Isolated:ITX4815SA +Converter_DCDC_Isolated:ITX4815SA-H +Converter_DCDC_Isolated:ITX4815SA-HR +Converter_DCDC_Isolated:ITX4815SA-R +Converter_DCDC_Isolated:ITX4824SA +Converter_DCDC_Isolated:ITX4824SA-H +Converter_DCDC_Isolated:ITX4824SA-HR +Converter_DCDC_Isolated:ITX4824SA-R +Converter_DCDC_Isolated:JTD1524D05 +Converter_DCDC_Isolated:JTD1524D12 +Converter_DCDC_Isolated:JTD1524D15 +Converter_DCDC_Isolated:JTD1524S05 +Converter_DCDC_Isolated:JTD1524S12 +Converter_DCDC_Isolated:JTD1524S15 +Converter_DCDC_Isolated:JTD1524S3V3 +Converter_DCDC_Isolated:JTD1548D05 +Converter_DCDC_Isolated:JTD1548D12 +Converter_DCDC_Isolated:JTD1548D15 +Converter_DCDC_Isolated:JTD1548S05 +Converter_DCDC_Isolated:JTD1548S12 +Converter_DCDC_Isolated:JTD1548S15 +Converter_DCDC_Isolated:JTD1548S3V3 +Converter_DCDC_Isolated:JTD2024D05 +Converter_DCDC_Isolated:JTD2024D12 +Converter_DCDC_Isolated:JTD2024D15 +Converter_DCDC_Isolated:JTD2024S05 +Converter_DCDC_Isolated:JTD2024S12 +Converter_DCDC_Isolated:JTD2024S15 +Converter_DCDC_Isolated:JTD2024S3V3 +Converter_DCDC_Isolated:JTD2048D05 +Converter_DCDC_Isolated:JTD2048D12 +Converter_DCDC_Isolated:JTD2048D15 +Converter_DCDC_Isolated:JTD2048S05 +Converter_DCDC_Isolated:JTD2048S12 +Converter_DCDC_Isolated:JTD2048S15 +Converter_DCDC_Isolated:JTD2048S3V3 +Converter_DCDC_Isolated:JTE0624D03 +Converter_DCDC_Isolated:JTE0624D05 +Converter_DCDC_Isolated:JTE0624D12 +Converter_DCDC_Isolated:JTE0624D15 +Converter_DCDC_Isolated:JTE0624D24 +Converter_DCDC_Isolated:MEE1S0303SC +Converter_DCDC_Isolated:MEE1S0305SC +Converter_DCDC_Isolated:MEE1S0309SC +Converter_DCDC_Isolated:MEE1S0312SC +Converter_DCDC_Isolated:MEE1S0315SC +Converter_DCDC_Isolated:MEE1S0503SC +Converter_DCDC_Isolated:MEE1S0505SC +Converter_DCDC_Isolated:MEE1S0509SC +Converter_DCDC_Isolated:MEE1S0512SC +Converter_DCDC_Isolated:MEE1S0515SC +Converter_DCDC_Isolated:MEE1S1205SC +Converter_DCDC_Isolated:MEE1S1209SC +Converter_DCDC_Isolated:MEE1S1212SC +Converter_DCDC_Isolated:MEE1S1215SC +Converter_DCDC_Isolated:MEE1S1505SC +Converter_DCDC_Isolated:MEE1S1509SC +Converter_DCDC_Isolated:MEE1S1512SC +Converter_DCDC_Isolated:MEE1S1515SC +Converter_DCDC_Isolated:MEE1S2405SC +Converter_DCDC_Isolated:MEE1S2409SC +Converter_DCDC_Isolated:MEE1S2412SC +Converter_DCDC_Isolated:MEE1S2415SC +Converter_DCDC_Isolated:MEE3S0505SC +Converter_DCDC_Isolated:MEE3S0509SC +Converter_DCDC_Isolated:MEE3S0512SC +Converter_DCDC_Isolated:MEE3S0515SC +Converter_DCDC_Isolated:MEE3S1205SC +Converter_DCDC_Isolated:MEE3S1209SC +Converter_DCDC_Isolated:MEE3S1212SC +Converter_DCDC_Isolated:MEE3S1215SC +Converter_DCDC_Isolated:MGJ2D051505SC +Converter_DCDC_Isolated:MGJ2D051509SC +Converter_DCDC_Isolated:MGJ2D051515SC +Converter_DCDC_Isolated:MGJ2D051802SC +Converter_DCDC_Isolated:MGJ2D052003SC +Converter_DCDC_Isolated:MGJ2D052005SC +Converter_DCDC_Isolated:MGJ2D121505SC +Converter_DCDC_Isolated:MGJ2D121509SC +Converter_DCDC_Isolated:MGJ2D121802SC +Converter_DCDC_Isolated:MGJ2D122003SC +Converter_DCDC_Isolated:MGJ2D122005SC +Converter_DCDC_Isolated:MGJ2D151505SC +Converter_DCDC_Isolated:MGJ2D151509SC +Converter_DCDC_Isolated:MGJ2D151515SC +Converter_DCDC_Isolated:MGJ2D151802SC +Converter_DCDC_Isolated:MGJ2D152003SC +Converter_DCDC_Isolated:MGJ2D152005SC +Converter_DCDC_Isolated:MGJ2D241505SC +Converter_DCDC_Isolated:MGJ2D241509SC +Converter_DCDC_Isolated:MGJ2D241709SC +Converter_DCDC_Isolated:MGJ2D241802SC +Converter_DCDC_Isolated:MGJ2D242003SC +Converter_DCDC_Isolated:MGJ2D242005SC +Converter_DCDC_Isolated:MGJ3T05150505MC +Converter_DCDC_Isolated:MGJ3T12150505MC +Converter_DCDC_Isolated:MGJ3T24150505MC +Converter_DCDC_Isolated:NCS1S1203SC +Converter_DCDC_Isolated:NCS1S1205SC +Converter_DCDC_Isolated:NCS1S1212SC +Converter_DCDC_Isolated:NCS1S2403SC +Converter_DCDC_Isolated:NCS1S2405SC +Converter_DCDC_Isolated:NCS1S2412SC +Converter_DCDC_Isolated:NMA0505DC +Converter_DCDC_Isolated:NMA0505SC +Converter_DCDC_Isolated:NMA0509DC +Converter_DCDC_Isolated:NMA0509SC +Converter_DCDC_Isolated:NMA0512DC +Converter_DCDC_Isolated:NMA0512SC +Converter_DCDC_Isolated:NMA0515DC +Converter_DCDC_Isolated:NMA0515SC +Converter_DCDC_Isolated:NMA1205DC +Converter_DCDC_Isolated:NMA1205SC +Converter_DCDC_Isolated:NMA1209DC +Converter_DCDC_Isolated:NMA1209SC +Converter_DCDC_Isolated:NMA1212DC +Converter_DCDC_Isolated:NMA1212SC +Converter_DCDC_Isolated:NMA1215DC +Converter_DCDC_Isolated:NMA1215SC +Converter_DCDC_Isolated:NMA1505DC +Converter_DCDC_Isolated:NMA1505SC +Converter_DCDC_Isolated:NMA1512DC +Converter_DCDC_Isolated:NMA1512SC +Converter_DCDC_Isolated:NMA1515DC +Converter_DCDC_Isolated:NMA1515SC +Converter_DCDC_Isolated:NSD10-xxDyy +Converter_DCDC_Isolated:NSD10-xxSyy +Converter_DCDC_Isolated:NXE1S0303MC +Converter_DCDC_Isolated:NXE1S0305MC +Converter_DCDC_Isolated:NXE1S0505MC +Converter_DCDC_Isolated:NXE2S0505MC +Converter_DCDC_Isolated:NXE2S1205MC +Converter_DCDC_Isolated:NXE2S1212MC +Converter_DCDC_Isolated:NXE2S1215MC +Converter_DCDC_Isolated:RPA60-2405SFW +Converter_DCDC_Isolated:RPA60-2412SFW +Converter_DCDC_Isolated:RPA60-2415SFW +Converter_DCDC_Isolated:RPA60-2424SFW +Converter_DCDC_Isolated:SMU02L-24N +Converter_DCDC_Isolated:TBA1-0310 +Converter_DCDC_Isolated:TBA1-0311 +Converter_DCDC_Isolated:TBA1-0510 +Converter_DCDC_Isolated:TBA1-0511 +Converter_DCDC_Isolated:TBA1-0511E +Converter_DCDC_Isolated:TBA1-0512 +Converter_DCDC_Isolated:TBA1-0512E +Converter_DCDC_Isolated:TBA1-0513 +Converter_DCDC_Isolated:TBA1-0513E +Converter_DCDC_Isolated:TBA1-0519 +Converter_DCDC_Isolated:TBA1-0521E +Converter_DCDC_Isolated:TBA1-0522E +Converter_DCDC_Isolated:TBA1-0523E +Converter_DCDC_Isolated:TBA1-1211 +Converter_DCDC_Isolated:TBA1-1211E +Converter_DCDC_Isolated:TBA1-1212 +Converter_DCDC_Isolated:TBA1-1212E +Converter_DCDC_Isolated:TBA1-1213 +Converter_DCDC_Isolated:TBA1-1213E +Converter_DCDC_Isolated:TBA1-1219 +Converter_DCDC_Isolated:TBA1-1221E +Converter_DCDC_Isolated:TBA1-1222E +Converter_DCDC_Isolated:TBA1-1223E +Converter_DCDC_Isolated:TBA1-2411 +Converter_DCDC_Isolated:TBA1-2411E +Converter_DCDC_Isolated:TBA1-2412 +Converter_DCDC_Isolated:TBA1-2412E +Converter_DCDC_Isolated:TBA1-2413 +Converter_DCDC_Isolated:TBA1-2413E +Converter_DCDC_Isolated:TBA1-2419 +Converter_DCDC_Isolated:TBA1-2421E +Converter_DCDC_Isolated:TBA1-2422E +Converter_DCDC_Isolated:TBA1-2423E +Converter_DCDC_Isolated:TBA2-0511 +Converter_DCDC_Isolated:TBA2-0512 +Converter_DCDC_Isolated:TBA2-0513 +Converter_DCDC_Isolated:TBA2-0521 +Converter_DCDC_Isolated:TBA2-0522 +Converter_DCDC_Isolated:TBA2-0523 +Converter_DCDC_Isolated:TBA2-1211 +Converter_DCDC_Isolated:TBA2-1212 +Converter_DCDC_Isolated:TBA2-1213 +Converter_DCDC_Isolated:TBA2-1221 +Converter_DCDC_Isolated:TBA2-1222 +Converter_DCDC_Isolated:TBA2-1223 +Converter_DCDC_Isolated:TBA2-2411 +Converter_DCDC_Isolated:TBA2-2412 +Converter_DCDC_Isolated:TBA2-2413 +Converter_DCDC_Isolated:TBA2-2421 +Converter_DCDC_Isolated:TBA2-2422 +Converter_DCDC_Isolated:TBA2-2423 +Converter_DCDC_Isolated:TDU1-0511 +Converter_DCDC_Isolated:TDU1-0512 +Converter_DCDC_Isolated:TDU1-0513 +Converter_DCDC_Isolated:TDU1-1211 +Converter_DCDC_Isolated:TDU1-1212 +Converter_DCDC_Isolated:TDU1-1213 +Converter_DCDC_Isolated:TDU1-2411 +Converter_DCDC_Isolated:TDU1-2412 +Converter_DCDC_Isolated:TDU1-2413 +Converter_DCDC_Isolated:TEA1-0505 +Converter_DCDC_Isolated:TEA1-0505E +Converter_DCDC_Isolated:TEA1-0505HI +Converter_DCDC_Isolated:TEC2-1210WI +Converter_DCDC_Isolated:TEC2-1211WI +Converter_DCDC_Isolated:TEC2-1212WI +Converter_DCDC_Isolated:TEC2-1213WI +Converter_DCDC_Isolated:TEC2-1215WI +Converter_DCDC_Isolated:TEC2-1219WI +Converter_DCDC_Isolated:TEC2-1221WI +Converter_DCDC_Isolated:TEC2-1222WI +Converter_DCDC_Isolated:TEC2-1223WI +Converter_DCDC_Isolated:TEC2-2410WI +Converter_DCDC_Isolated:TEC2-2411WI +Converter_DCDC_Isolated:TEC2-2412WI +Converter_DCDC_Isolated:TEC2-2413WI +Converter_DCDC_Isolated:TEC2-2415WI +Converter_DCDC_Isolated:TEC2-2419WI +Converter_DCDC_Isolated:TEC2-2421WI +Converter_DCDC_Isolated:TEC2-2422WI +Converter_DCDC_Isolated:TEC2-2423WI +Converter_DCDC_Isolated:TEC2-4810WI +Converter_DCDC_Isolated:TEC2-4811WI +Converter_DCDC_Isolated:TEC2-4812WI +Converter_DCDC_Isolated:TEC2-4813WI +Converter_DCDC_Isolated:TEC2-4815WI +Converter_DCDC_Isolated:TEC2-4819WI +Converter_DCDC_Isolated:TEC2-4821WI +Converter_DCDC_Isolated:TEC2-4822WI +Converter_DCDC_Isolated:TEC2-4823WI +Converter_DCDC_Isolated:TEC3-2410UI +Converter_DCDC_Isolated:TEC3-2411UI +Converter_DCDC_Isolated:TEC3-2412UI +Converter_DCDC_Isolated:TEC3-2413UI +Converter_DCDC_Isolated:TEC3-2421UI +Converter_DCDC_Isolated:TEC3-2422UI +Converter_DCDC_Isolated:TEC3-2423UI +Converter_DCDC_Isolated:TEC6-2410UI +Converter_DCDC_Isolated:TEC6-2411UI +Converter_DCDC_Isolated:TEC6-2412UI +Converter_DCDC_Isolated:TEC6-2413UI +Converter_DCDC_Isolated:TEC6-2415UI +Converter_DCDC_Isolated:TEC6-2419UI +Converter_DCDC_Isolated:TEC6-2421UI +Converter_DCDC_Isolated:TEC6-2422UI +Converter_DCDC_Isolated:TEC6-2423UI +Converter_DCDC_Isolated:TEL12-1211 +Converter_DCDC_Isolated:TEL12-1212 +Converter_DCDC_Isolated:TEL12-1213 +Converter_DCDC_Isolated:TEL12-1215 +Converter_DCDC_Isolated:TEL12-1222 +Converter_DCDC_Isolated:TEL12-1223 +Converter_DCDC_Isolated:TEL12-2411 +Converter_DCDC_Isolated:TEL12-2411WI +Converter_DCDC_Isolated:TEL12-2412 +Converter_DCDC_Isolated:TEL12-2412WI +Converter_DCDC_Isolated:TEL12-2413 +Converter_DCDC_Isolated:TEL12-2413WI +Converter_DCDC_Isolated:TEL12-2415 +Converter_DCDC_Isolated:TEL12-2415WI +Converter_DCDC_Isolated:TEL12-2422 +Converter_DCDC_Isolated:TEL12-2422WI +Converter_DCDC_Isolated:TEL12-2423 +Converter_DCDC_Isolated:TEL12-2423WI +Converter_DCDC_Isolated:TEL12-4811 +Converter_DCDC_Isolated:TEL12-4811WI +Converter_DCDC_Isolated:TEL12-4812 +Converter_DCDC_Isolated:TEL12-4812WI +Converter_DCDC_Isolated:TEL12-4813 +Converter_DCDC_Isolated:TEL12-4813WI +Converter_DCDC_Isolated:TEL12-4815 +Converter_DCDC_Isolated:TEL12-4815WI +Converter_DCDC_Isolated:TEL12-4822 +Converter_DCDC_Isolated:TEL12-4822WI +Converter_DCDC_Isolated:TEL12-4823 +Converter_DCDC_Isolated:TEL12-4823WI +Converter_DCDC_Isolated:TEN10-11010WIRH +Converter_DCDC_Isolated:TEN10-11011WIRH +Converter_DCDC_Isolated:TEN10-11012WIRH +Converter_DCDC_Isolated:TEN10-11013WIRH +Converter_DCDC_Isolated:TEN10-11015WIRH +Converter_DCDC_Isolated:TEN10-11021WIRH +Converter_DCDC_Isolated:TEN10-11022WIRH +Converter_DCDC_Isolated:TEN10-11023WIRH +Converter_DCDC_Isolated:TEN20-11011WIRH +Converter_DCDC_Isolated:TEN20-11012WIRH +Converter_DCDC_Isolated:TEN20-11013WIRH +Converter_DCDC_Isolated:TEN20-11015WIRH +Converter_DCDC_Isolated:TEN20-11021WIRH +Converter_DCDC_Isolated:TEN20-11022WIRH +Converter_DCDC_Isolated:TEN20-11023WIRH +Converter_DCDC_Isolated:TEN20-2410WIN +Converter_DCDC_Isolated:TEN20-2411WIN +Converter_DCDC_Isolated:TEN20-2412WIN +Converter_DCDC_Isolated:TEN20-2413WIN +Converter_DCDC_Isolated:TEN20-2421WIN +Converter_DCDC_Isolated:TEN20-2422WIN +Converter_DCDC_Isolated:TEN20-2423WIN +Converter_DCDC_Isolated:TEN20-4810WIN +Converter_DCDC_Isolated:TEN20-4811WIN +Converter_DCDC_Isolated:TEN20-4812WIN +Converter_DCDC_Isolated:TEN20-4813WIN +Converter_DCDC_Isolated:TEN20-4821WIN +Converter_DCDC_Isolated:TEN20-4822WIN +Converter_DCDC_Isolated:TEN20-4823WIN +Converter_DCDC_Isolated:TEN3-11010WIRH +Converter_DCDC_Isolated:TEN3-11011WIRH +Converter_DCDC_Isolated:TEN3-11012WIRH +Converter_DCDC_Isolated:TEN3-11013WIRH +Converter_DCDC_Isolated:TEN3-11015WIRH +Converter_DCDC_Isolated:TEN3-11021WIRH +Converter_DCDC_Isolated:TEN3-11022WIRH +Converter_DCDC_Isolated:TEN3-11023WIRH +Converter_DCDC_Isolated:TEN40-11011WIRH +Converter_DCDC_Isolated:TEN40-11012WIRH +Converter_DCDC_Isolated:TEN40-11013WIRH +Converter_DCDC_Isolated:TEN40-11015WIRH +Converter_DCDC_Isolated:TEN40-11022WIRH +Converter_DCDC_Isolated:TEN40-11023WIRH +Converter_DCDC_Isolated:TEN6-11010WIRH +Converter_DCDC_Isolated:TEN6-11011WIRH +Converter_DCDC_Isolated:TEN6-11012WIRH +Converter_DCDC_Isolated:TEN6-11013WIRH +Converter_DCDC_Isolated:TEN6-11015WIRH +Converter_DCDC_Isolated:TEN6-11021WIRH +Converter_DCDC_Isolated:TEN6-11022WIRH +Converter_DCDC_Isolated:TEN6-11023WIRH +Converter_DCDC_Isolated:TEP150-7211UIR +Converter_DCDC_Isolated:TEP150-7212UIR +Converter_DCDC_Isolated:TEP150-7213UIR +Converter_DCDC_Isolated:TEP150-7215UIR +Converter_DCDC_Isolated:TEP150-7218UIR +Converter_DCDC_Isolated:TEP200-7211UIR +Converter_DCDC_Isolated:TEP200-7212UIR +Converter_DCDC_Isolated:TEP200-7213UIR +Converter_DCDC_Isolated:TEP200-7215UIR +Converter_DCDC_Isolated:TEP200-7218UIR +Converter_DCDC_Isolated:TES1-0510 +Converter_DCDC_Isolated:TES1-0511 +Converter_DCDC_Isolated:TES1-0512 +Converter_DCDC_Isolated:TES1-0513 +Converter_DCDC_Isolated:TES1-0519 +Converter_DCDC_Isolated:TES1-0521 +Converter_DCDC_Isolated:TES1-0522 +Converter_DCDC_Isolated:TES1-0523 +Converter_DCDC_Isolated:TES1-1211 +Converter_DCDC_Isolated:TES1-1212 +Converter_DCDC_Isolated:TES1-1213 +Converter_DCDC_Isolated:TES1-1219 +Converter_DCDC_Isolated:TES1-1221 +Converter_DCDC_Isolated:TES1-1222 +Converter_DCDC_Isolated:TES1-1223 +Converter_DCDC_Isolated:TES1-2411 +Converter_DCDC_Isolated:TES1-2412 +Converter_DCDC_Isolated:TES1-2413 +Converter_DCDC_Isolated:TES1-2419 +Converter_DCDC_Isolated:TES1-2421 +Converter_DCDC_Isolated:TES1-2422 +Converter_DCDC_Isolated:TES1-2423 +Converter_DCDC_Isolated:THB10-1211 +Converter_DCDC_Isolated:THB10-1212 +Converter_DCDC_Isolated:THB10-1222 +Converter_DCDC_Isolated:THB10-1223 +Converter_DCDC_Isolated:THB10-2411 +Converter_DCDC_Isolated:THB10-2412 +Converter_DCDC_Isolated:THB10-2422 +Converter_DCDC_Isolated:THB10-2423 +Converter_DCDC_Isolated:THB10-4811 +Converter_DCDC_Isolated:THB10-4812 +Converter_DCDC_Isolated:THB10-4822 +Converter_DCDC_Isolated:THB10-4823 +Converter_DCDC_Isolated:THL30-2410WI +Converter_DCDC_Isolated:THL30-2411WI +Converter_DCDC_Isolated:THL30-2412WI +Converter_DCDC_Isolated:THL30-2413WI +Converter_DCDC_Isolated:THL30-2415WI +Converter_DCDC_Isolated:THL30-2422WI +Converter_DCDC_Isolated:THL30-2423WI +Converter_DCDC_Isolated:THL30-4810WI +Converter_DCDC_Isolated:THL30-4811WI +Converter_DCDC_Isolated:THL30-4812WI +Converter_DCDC_Isolated:THL30-4813WI +Converter_DCDC_Isolated:THL30-4815WI +Converter_DCDC_Isolated:THL30-4822WI +Converter_DCDC_Isolated:THL30-4823WI +Converter_DCDC_Isolated:THL40-2411WI +Converter_DCDC_Isolated:THL40-2412WI +Converter_DCDC_Isolated:THL40-2413WI +Converter_DCDC_Isolated:THL40-2415WI +Converter_DCDC_Isolated:THL40-2422WI +Converter_DCDC_Isolated:THL40-2423WI +Converter_DCDC_Isolated:THL40-4811WI +Converter_DCDC_Isolated:THL40-4812WI +Converter_DCDC_Isolated:THL40-4813WI +Converter_DCDC_Isolated:THL40-4815WI +Converter_DCDC_Isolated:THL40-4822WI +Converter_DCDC_Isolated:THL40-4823WI +Converter_DCDC_Isolated:THN10-3610UIR +Converter_DCDC_Isolated:THN10-3611UIR +Converter_DCDC_Isolated:THN10-3612UIR +Converter_DCDC_Isolated:THN10-3613UIR +Converter_DCDC_Isolated:THN10-3615UIR +Converter_DCDC_Isolated:THN10-3621UIR +Converter_DCDC_Isolated:THN10-3622UIR +Converter_DCDC_Isolated:THN10-3623UIR +Converter_DCDC_Isolated:THN10-7210UIR +Converter_DCDC_Isolated:THN10-7211UIR +Converter_DCDC_Isolated:THN10-7212UIR +Converter_DCDC_Isolated:THN10-7213UIR +Converter_DCDC_Isolated:THN10-7215UIR +Converter_DCDC_Isolated:THN10-7221UIR +Converter_DCDC_Isolated:THN10-7222UIR +Converter_DCDC_Isolated:THN10-7223UIR +Converter_DCDC_Isolated:THN15-3611UIR +Converter_DCDC_Isolated:THN15-3612UIR +Converter_DCDC_Isolated:THN15-3613UIR +Converter_DCDC_Isolated:THN15-3615UIR +Converter_DCDC_Isolated:THN15-3622UIR +Converter_DCDC_Isolated:THN15-3623UIR +Converter_DCDC_Isolated:THN15-7211UIR +Converter_DCDC_Isolated:THN15-7212UIR +Converter_DCDC_Isolated:THN15-7213UIR +Converter_DCDC_Isolated:THN15-7215UIR +Converter_DCDC_Isolated:THN15-7222UIR +Converter_DCDC_Isolated:THN15-7223UIR +Converter_DCDC_Isolated:THR40-7211WI +Converter_DCDC_Isolated:THR40-7212WI +Converter_DCDC_Isolated:THR40-7213WI +Converter_DCDC_Isolated:THR40-72154WI +Converter_DCDC_Isolated:THR40-7215WI +Converter_DCDC_Isolated:THR40-7222WI +Converter_DCDC_Isolated:THR40-7223WI +Converter_DCDC_Isolated:TMA-0505D +Converter_DCDC_Isolated:TMA-0505S +Converter_DCDC_Isolated:TMA-0512D +Converter_DCDC_Isolated:TMA-0512S +Converter_DCDC_Isolated:TMA-0515D +Converter_DCDC_Isolated:TMA-0515S +Converter_DCDC_Isolated:TMA-1205D +Converter_DCDC_Isolated:TMA-1205S +Converter_DCDC_Isolated:TMA-1212D +Converter_DCDC_Isolated:TMA-1212S +Converter_DCDC_Isolated:TMA-1215D +Converter_DCDC_Isolated:TMA-1215S +Converter_DCDC_Isolated:TMA-1505D +Converter_DCDC_Isolated:TMA-1505S +Converter_DCDC_Isolated:TMA-1512D +Converter_DCDC_Isolated:TMA-1512S +Converter_DCDC_Isolated:TMA-1515D +Converter_DCDC_Isolated:TMA-1515S +Converter_DCDC_Isolated:TMA-2405D +Converter_DCDC_Isolated:TMA-2405S +Converter_DCDC_Isolated:TMA-2412D +Converter_DCDC_Isolated:TMA-2412S +Converter_DCDC_Isolated:TMA-2415D +Converter_DCDC_Isolated:TMA-2415S +Converter_DCDC_Isolated:TME-0303S +Converter_DCDC_Isolated:TME-0305S +Converter_DCDC_Isolated:TME-0503S +Converter_DCDC_Isolated:TME-0505S +Converter_DCDC_Isolated:TME-0509S +Converter_DCDC_Isolated:TME-0512S +Converter_DCDC_Isolated:TME-0515S +Converter_DCDC_Isolated:TME-1205S +Converter_DCDC_Isolated:TME-1209S +Converter_DCDC_Isolated:TME-1212S +Converter_DCDC_Isolated:TME-1215S +Converter_DCDC_Isolated:TME-2405S +Converter_DCDC_Isolated:TME-2409S +Converter_DCDC_Isolated:TME-2412S +Converter_DCDC_Isolated:TME-2415S +Converter_DCDC_Isolated:TMR-0510 +Converter_DCDC_Isolated:TMR-0511 +Converter_DCDC_Isolated:TMR-0512 +Converter_DCDC_Isolated:TMR-0521 +Converter_DCDC_Isolated:TMR-0522 +Converter_DCDC_Isolated:TMR-0523 +Converter_DCDC_Isolated:TMR-1210 +Converter_DCDC_Isolated:TMR-1211 +Converter_DCDC_Isolated:TMR-1212 +Converter_DCDC_Isolated:TMR-1221 +Converter_DCDC_Isolated:TMR-1222 +Converter_DCDC_Isolated:TMR-1223 +Converter_DCDC_Isolated:TMR-2410 +Converter_DCDC_Isolated:TMR-2411 +Converter_DCDC_Isolated:TMR-2412 +Converter_DCDC_Isolated:TMR-2421 +Converter_DCDC_Isolated:TMR-2422 +Converter_DCDC_Isolated:TMR-2423 +Converter_DCDC_Isolated:TMR-4810 +Converter_DCDC_Isolated:TMR-4811 +Converter_DCDC_Isolated:TMR-4812 +Converter_DCDC_Isolated:TMR-4821 +Converter_DCDC_Isolated:TMR-4822 +Converter_DCDC_Isolated:TMR-4823 +Converter_DCDC_Isolated:TMR10-1211WI +Converter_DCDC_Isolated:TMR10-1212WI +Converter_DCDC_Isolated:TMR10-1213WI +Converter_DCDC_Isolated:TMR10-1215WI +Converter_DCDC_Isolated:TMR10-1222WI +Converter_DCDC_Isolated:TMR10-1223WI +Converter_DCDC_Isolated:TMR10-2410WIR +Converter_DCDC_Isolated:TMR10-2411WI +Converter_DCDC_Isolated:TMR10-2411WIR +Converter_DCDC_Isolated:TMR10-2412WI +Converter_DCDC_Isolated:TMR10-2412WIR +Converter_DCDC_Isolated:TMR10-2413WI +Converter_DCDC_Isolated:TMR10-2413WIR +Converter_DCDC_Isolated:TMR10-2415WI +Converter_DCDC_Isolated:TMR10-2415WIR +Converter_DCDC_Isolated:TMR10-2421WIR +Converter_DCDC_Isolated:TMR10-2422WI +Converter_DCDC_Isolated:TMR10-2422WIR +Converter_DCDC_Isolated:TMR10-2423WI +Converter_DCDC_Isolated:TMR10-2423WIR +Converter_DCDC_Isolated:TMR10-4810WIR +Converter_DCDC_Isolated:TMR10-4811WI +Converter_DCDC_Isolated:TMR10-4811WIR +Converter_DCDC_Isolated:TMR10-4812WI +Converter_DCDC_Isolated:TMR10-4812WIR +Converter_DCDC_Isolated:TMR10-4813WI +Converter_DCDC_Isolated:TMR10-4813WIR +Converter_DCDC_Isolated:TMR10-4815WI +Converter_DCDC_Isolated:TMR10-4815WIR +Converter_DCDC_Isolated:TMR10-4821WIR +Converter_DCDC_Isolated:TMR10-4822WI +Converter_DCDC_Isolated:TMR10-4822WIR +Converter_DCDC_Isolated:TMR10-4823WI +Converter_DCDC_Isolated:TMR10-4823WIR +Converter_DCDC_Isolated:TMR10-7210WIR +Converter_DCDC_Isolated:TMR10-7211WIR +Converter_DCDC_Isolated:TMR10-7212WIR +Converter_DCDC_Isolated:TMR10-7213WIR +Converter_DCDC_Isolated:TMR10-7215WIR +Converter_DCDC_Isolated:TMR10-7221WIR +Converter_DCDC_Isolated:TMR10-7222WIR +Converter_DCDC_Isolated:TMR10-7223WIR +Converter_DCDC_Isolated:TMR2-2410WI +Converter_DCDC_Isolated:TMR2-2411WI +Converter_DCDC_Isolated:TMR2-2412WI +Converter_DCDC_Isolated:TMR2-2413WI +Converter_DCDC_Isolated:TMR2-2421WI +Converter_DCDC_Isolated:TMR2-2422WI +Converter_DCDC_Isolated:TMR2-2423WI +Converter_DCDC_Isolated:TMR2-4810WI +Converter_DCDC_Isolated:TMR2-4811WI +Converter_DCDC_Isolated:TMR2-4812WI +Converter_DCDC_Isolated:TMR2-4813WI +Converter_DCDC_Isolated:TMR2-4821WI +Converter_DCDC_Isolated:TMR2-4822WI +Converter_DCDC_Isolated:TMR2-4823WI +Converter_DCDC_Isolated:TMR4-2411WI +Converter_DCDC_Isolated:TMR4-2412WI +Converter_DCDC_Isolated:TMR4-2413WI +Converter_DCDC_Isolated:TMR4-2415WI +Converter_DCDC_Isolated:TMR4-2422WI +Converter_DCDC_Isolated:TMR4-2423WI +Converter_DCDC_Isolated:TMR4-4811WI +Converter_DCDC_Isolated:TMR4-4812WI +Converter_DCDC_Isolated:TMR4-4813WI +Converter_DCDC_Isolated:TMR4-4815WI +Converter_DCDC_Isolated:TMR4-4822WI +Converter_DCDC_Isolated:TMR4-4823WI +Converter_DCDC_Isolated:TMR8-1211WI +Converter_DCDC_Isolated:TMR8-1212WI +Converter_DCDC_Isolated:TMR8-1213WI +Converter_DCDC_Isolated:TMR8-1215WI +Converter_DCDC_Isolated:TMR8-1222WI +Converter_DCDC_Isolated:TMR8-1223WI +Converter_DCDC_Isolated:TMR8-2411WI +Converter_DCDC_Isolated:TMR8-2412WI +Converter_DCDC_Isolated:TMR8-2413WI +Converter_DCDC_Isolated:TMR8-2415WI +Converter_DCDC_Isolated:TMR8-2422WI +Converter_DCDC_Isolated:TMR8-2423WI +Converter_DCDC_Isolated:TMR8-4811WI +Converter_DCDC_Isolated:TMR8-4812WI +Converter_DCDC_Isolated:TMR8-4813WI +Converter_DCDC_Isolated:TMR8-4815WI +Converter_DCDC_Isolated:TMR8-4822WI +Converter_DCDC_Isolated:TMR8-4823WI +Converter_DCDC_Isolated:TMR_1-0511 +Converter_DCDC_Isolated:TMR_1-0511SM +Converter_DCDC_Isolated:TMR_1-0512 +Converter_DCDC_Isolated:TMR_1-0512SM +Converter_DCDC_Isolated:TMR_1-0513 +Converter_DCDC_Isolated:TMR_1-0513SM +Converter_DCDC_Isolated:TMR_1-0515 +Converter_DCDC_Isolated:TMR_1-0522 +Converter_DCDC_Isolated:TMR_1-0522SM +Converter_DCDC_Isolated:TMR_1-0523 +Converter_DCDC_Isolated:TMR_1-0523SM +Converter_DCDC_Isolated:TMR_1-1211 +Converter_DCDC_Isolated:TMR_1-1211SM +Converter_DCDC_Isolated:TMR_1-1212 +Converter_DCDC_Isolated:TMR_1-1212SM +Converter_DCDC_Isolated:TMR_1-1213 +Converter_DCDC_Isolated:TMR_1-1213SM +Converter_DCDC_Isolated:TMR_1-1215 +Converter_DCDC_Isolated:TMR_1-1222 +Converter_DCDC_Isolated:TMR_1-1222SM +Converter_DCDC_Isolated:TMR_1-1223 +Converter_DCDC_Isolated:TMR_1-1223SM +Converter_DCDC_Isolated:TMR_1-2411 +Converter_DCDC_Isolated:TMR_1-2411SM +Converter_DCDC_Isolated:TMR_1-2412 +Converter_DCDC_Isolated:TMR_1-2412SM +Converter_DCDC_Isolated:TMR_1-2413 +Converter_DCDC_Isolated:TMR_1-2413SM +Converter_DCDC_Isolated:TMR_1-2415 +Converter_DCDC_Isolated:TMR_1-2422 +Converter_DCDC_Isolated:TMR_1-2422SM +Converter_DCDC_Isolated:TMR_1-2423 +Converter_DCDC_Isolated:TMR_1-2423SM +Converter_DCDC_Isolated:TMR_1-4811 +Converter_DCDC_Isolated:TMR_1-4811SM +Converter_DCDC_Isolated:TMR_1-4812 +Converter_DCDC_Isolated:TMR_1-4812SM +Converter_DCDC_Isolated:TMR_1-4813 +Converter_DCDC_Isolated:TMR_1-4813SM +Converter_DCDC_Isolated:TMR_1-4815 +Converter_DCDC_Isolated:TMR_1-4822 +Converter_DCDC_Isolated:TMR_1-4822SM +Converter_DCDC_Isolated:TMR_1-4823 +Converter_DCDC_Isolated:TMR_1-4823SM +Converter_DCDC_Isolated:TMU3-0511 +Converter_DCDC_Isolated:TMU3-0512 +Converter_DCDC_Isolated:TMU3-0513 +Converter_DCDC_Isolated:TMU3-1211 +Converter_DCDC_Isolated:TMU3-1212 +Converter_DCDC_Isolated:TMU3-1213 +Converter_DCDC_Isolated:TMU3-2411 +Converter_DCDC_Isolated:TMU3-2412 +Converter_DCDC_Isolated:TMU3-2413 +Converter_DCDC_Isolated:TMV0505D +Converter_DCDC_Isolated:TMV0505S +Converter_DCDC_Isolated:TMV0509S +Converter_DCDC_Isolated:TMV0512D +Converter_DCDC_Isolated:TMV0512S +Converter_DCDC_Isolated:TMV0515D +Converter_DCDC_Isolated:TMV0515S +Converter_DCDC_Isolated:TMV1205D +Converter_DCDC_Isolated:TMV1205S +Converter_DCDC_Isolated:TMV1212D +Converter_DCDC_Isolated:TMV1212S +Converter_DCDC_Isolated:TMV1215D +Converter_DCDC_Isolated:TMV1215S +Converter_DCDC_Isolated:TMV2405D +Converter_DCDC_Isolated:TMV2405S +Converter_DCDC_Isolated:TMV2412D +Converter_DCDC_Isolated:TMV2412S +Converter_DCDC_Isolated:TMV2415D +Converter_DCDC_Isolated:TMV2415S +Converter_DCDC_Isolated:TRA3-0511 +Converter_DCDC_Isolated:TRA3-0512 +Converter_DCDC_Isolated:TRA3-0513 +Converter_DCDC_Isolated:TRA3-0519 +Converter_DCDC_Isolated:TRA3-1211 +Converter_DCDC_Isolated:TRA3-1212 +Converter_DCDC_Isolated:TRA3-1213 +Converter_DCDC_Isolated:TRA3-1219 +Converter_DCDC_Isolated:TRA3-2411 +Converter_DCDC_Isolated:TRA3-2412 +Converter_DCDC_Isolated:TRA3-2413 +Converter_DCDC_Isolated:TRA3-2419 +Converter_DCDC_Isolated:TRI1-0511 +Converter_DCDC_Isolated:TRI1-0511SM +Converter_DCDC_Isolated:TRI1-0512 +Converter_DCDC_Isolated:TRI1-0512SM +Converter_DCDC_Isolated:TRI1-0513 +Converter_DCDC_Isolated:TRI1-0513SM +Converter_DCDC_Isolated:TRI1-0522SM +Converter_DCDC_Isolated:TRI1-0523SM +Converter_DCDC_Isolated:TRI1-1211 +Converter_DCDC_Isolated:TRI1-1211SM +Converter_DCDC_Isolated:TRI1-1212 +Converter_DCDC_Isolated:TRI1-1212SM +Converter_DCDC_Isolated:TRI1-1213 +Converter_DCDC_Isolated:TRI1-1213SM +Converter_DCDC_Isolated:TRI1-1222SM +Converter_DCDC_Isolated:TRI1-1223SM +Converter_DCDC_Isolated:TRI1-2411 +Converter_DCDC_Isolated:TRI1-2411SM +Converter_DCDC_Isolated:TRI1-2412 +Converter_DCDC_Isolated:TRI1-2412SM +Converter_DCDC_Isolated:TRI1-2413 +Converter_DCDC_Isolated:TRI1-2413SM +Converter_DCDC_Isolated:TRI1-2422SM +Converter_DCDC_Isolated:TRI1-2423SM +Converter_DCDC_Isolated:TRI10-1210 +Converter_DCDC_Isolated:TRI10-1211 +Converter_DCDC_Isolated:TRI10-1212 +Converter_DCDC_Isolated:TRI10-1213 +Converter_DCDC_Isolated:TRI10-1215 +Converter_DCDC_Isolated:TRI10-1222 +Converter_DCDC_Isolated:TRI10-1223 +Converter_DCDC_Isolated:TRI10-2410 +Converter_DCDC_Isolated:TRI10-2411 +Converter_DCDC_Isolated:TRI10-2412 +Converter_DCDC_Isolated:TRI10-2413 +Converter_DCDC_Isolated:TRI10-2415 +Converter_DCDC_Isolated:TRI10-2422 +Converter_DCDC_Isolated:TRI10-2423 +Converter_DCDC_Isolated:TRI10-4810 +Converter_DCDC_Isolated:TRI10-4811 +Converter_DCDC_Isolated:TRI10-4812 +Converter_DCDC_Isolated:TRI10-4813 +Converter_DCDC_Isolated:TRI10-4815 +Converter_DCDC_Isolated:TRI10-4822 +Converter_DCDC_Isolated:TRI10-4823 +Converter_DCDC_Isolated:TRI15-1211 +Converter_DCDC_Isolated:TRI15-1212 +Converter_DCDC_Isolated:TRI15-1213 +Converter_DCDC_Isolated:TRI15-1215 +Converter_DCDC_Isolated:TRI15-1222 +Converter_DCDC_Isolated:TRI15-1223 +Converter_DCDC_Isolated:TRI15-2411 +Converter_DCDC_Isolated:TRI15-2412 +Converter_DCDC_Isolated:TRI15-2413 +Converter_DCDC_Isolated:TRI15-2415 +Converter_DCDC_Isolated:TRI15-2422 +Converter_DCDC_Isolated:TRI15-2423 +Converter_DCDC_Isolated:TRI15-4811 +Converter_DCDC_Isolated:TRI15-4812 +Converter_DCDC_Isolated:TRI15-4813 +Converter_DCDC_Isolated:TRI15-4815 +Converter_DCDC_Isolated:TRI15-4822 +Converter_DCDC_Isolated:TRI15-4823 +Converter_DCDC_Isolated:TRI20-1211 +Converter_DCDC_Isolated:TRI20-1212 +Converter_DCDC_Isolated:TRI20-1213 +Converter_DCDC_Isolated:TRI20-1215 +Converter_DCDC_Isolated:TRI20-1222 +Converter_DCDC_Isolated:TRI20-1223 +Converter_DCDC_Isolated:TRI20-2411 +Converter_DCDC_Isolated:TRI20-2412 +Converter_DCDC_Isolated:TRI20-2413 +Converter_DCDC_Isolated:TRI20-2415 +Converter_DCDC_Isolated:TRI20-2422 +Converter_DCDC_Isolated:TRI20-2423 +Converter_DCDC_Isolated:TRI20-4811 +Converter_DCDC_Isolated:TRI20-4812 +Converter_DCDC_Isolated:TRI20-4813 +Converter_DCDC_Isolated:TRI20-4815 +Converter_DCDC_Isolated:TRI20-4822 +Converter_DCDC_Isolated:TRI20-4823 +Converter_DCDC_Isolated:TRN3-0510 +Converter_DCDC_Isolated:TRN3-0511 +Converter_DCDC_Isolated:TRN3-0512 +Converter_DCDC_Isolated:TRN3-0513 +Converter_DCDC_Isolated:TRN3-0515 +Converter_DCDC_Isolated:TRN3-0521 +Converter_DCDC_Isolated:TRN3-0522 +Converter_DCDC_Isolated:TRN3-0523 +Converter_DCDC_Isolated:TRN3-1210 +Converter_DCDC_Isolated:TRN3-1211 +Converter_DCDC_Isolated:TRN3-1212 +Converter_DCDC_Isolated:TRN3-1213 +Converter_DCDC_Isolated:TRN3-1215 +Converter_DCDC_Isolated:TRN3-1221 +Converter_DCDC_Isolated:TRN3-1222 +Converter_DCDC_Isolated:TRN3-1223 +Converter_DCDC_Isolated:TRN3-2410 +Converter_DCDC_Isolated:TRN3-2411 +Converter_DCDC_Isolated:TRN3-2412 +Converter_DCDC_Isolated:TRN3-2413 +Converter_DCDC_Isolated:TRN3-2415 +Converter_DCDC_Isolated:TRN3-2421 +Converter_DCDC_Isolated:TRN3-2422 +Converter_DCDC_Isolated:TRN3-2423 +Converter_DCDC_Isolated:TRN3-4810 +Converter_DCDC_Isolated:TRN3-4811 +Converter_DCDC_Isolated:TRN3-4812 +Converter_DCDC_Isolated:TRN3-4813 +Converter_DCDC_Isolated:TRN3-4815 +Converter_DCDC_Isolated:TRN3-4821 +Converter_DCDC_Isolated:TRN3-4822 +Converter_DCDC_Isolated:TRN3-4823 DSP_AnalogDevices:ADAU1450 DSP_AnalogDevices:ADAU1451 DSP_AnalogDevices:ADAU1452 @@ -5962,8 +6098,6 @@ Device:NetTie_3_Tee Device:NetTie_4 Device:NetTie_4_Cross Device:Ohmmeter -Device:Opamp_Dual -Device:Opamp_Quad Device:Oscilloscope Device:PeltierElement Device:Polyfuse @@ -6124,6 +6258,7 @@ Device:SparkGap Device:Speaker Device:Speaker_Crystal Device:Speaker_Ultrasound +Device:Thermal_Jumper Device:Thermistor Device:Thermistor_NTC Device:Thermistor_NTC_3Wire @@ -7577,6 +7712,7 @@ Driver_LED:IS31FL3218-GR Driver_LED:IS31FL3218-QF Driver_LED:IS31FL3236-TQ Driver_LED:IS31FL3236A-TQ +Driver_LED:IS31FL3242 Driver_LED:IS31FL3731-QF Driver_LED:IS31FL3731-SA Driver_LED:IS31FL3733-QF @@ -8168,6 +8304,10 @@ Filter:MAX7422xUA Filter:MAX7423xUA Filter:MAX7424xUA Filter:MAX7425xUA +Filter:MAX7426xPA +Filter:MAX7426xUA +Filter:MAX7427xPA +Filter:MAX7427xUA Filter:MAX7480xPA Filter:MAX7480xSA Filter:P300PL104M275xC222 @@ -8949,6 +9089,7 @@ Interface_USB:TUSB320I Interface_USB:TUSB321 Interface_USB:TUSB322I Interface_USB:TUSB4041I +Interface_USB:TUSB564 Interface_USB:TUSB7340 Interface_USB:TUSB8041 Interface_USB:TUSB8043A @@ -9555,8 +9696,9 @@ Logic_Programmable:PAL20L10xxNS Logic_Programmable:PAL20L8 Logic_Programmable:PAL20R8xx-P Logic_Programmable:PAL20RS10 -Logic_Programmable:PAL24 +Logic_Programmable:PAL22V10-xxP Logic_Programmable:PALCE16V8 +Logic_Programmable:PALCE22V10x-xxP Logic_Programmable:PEEL22CV10AP Logic_Programmable:PEEL22CV10AS MCU_AnalogDevices:ADUC816BSZ @@ -14593,6 +14735,8 @@ MCU_Texas:LM4F111C4QR MCU_Texas:LM4F111E5QR MCU_Texas:LM4F111H5QR MCU_Texas:MSP432E401Y +MCU_Texas:MSPM0C110xSDDF +MCU_Texas:MSPM0C110xSDSG MCU_Texas:TM4C1230C3PM MCU_Texas:TM4C1230D5PM MCU_Texas:TM4C1230E6PM @@ -15115,6 +15259,7 @@ Memory_Flash:AM29F400Bx-xxEx Memory_Flash:AM29F400Bx-xxSx Memory_Flash:AM29PDL128G Memory_Flash:AT25DF041x-UxN-x +Memory_Flash:AT25SF041B-SSHD-X Memory_Flash:AT25SF081-SSHD-X Memory_Flash:AT25SF081-SSHF-X Memory_Flash:AT25SF081-XMHD-X @@ -15670,13 +15815,14 @@ Power_Management:LM5069MM-1 Power_Management:LM5069MM-2 Power_Management:LM66100DCK Power_Management:LM74700 +Power_Management:LM74701-Q1 Power_Management:LMG3410 Power_Management:LMG5200 Power_Management:LT1641-1 Power_Management:LT1641-2 Power_Management:LT4230xDD -Power_Management:LT4231xUF Power_Management:LT4320xDD-1 +Power_Management:LT4321xUF Power_Management:LTC4242xUHF Power_Management:LTC4357DCB Power_Management:LTC4357MS8 @@ -15771,6 +15917,7 @@ Power_Management:TPS22810DBV Power_Management:TPS22810DRV Power_Management:TPS22917DBV Power_Management:TPS22917LDBV +Power_Management:TPS22919DCK Power_Management:TPS22929D Power_Management:TPS22993 Power_Management:TPS2412D @@ -16480,6 +16627,7 @@ RF_Module:DWM3000 RF_Module:E18-MS1-PCB RF_Module:E73-2G4M04S-52810 RF_Module:E73-2G4M04S-52832 +RF_Module:ESP-01 RF_Module:ESP-07 RF_Module:ESP-12E RF_Module:ESP-12F @@ -16681,6 +16829,19 @@ Reference_Voltage:LM4040LP-4.1 Reference_Voltage:LM4040LP-5 Reference_Voltage:LM4040LP-8.2 Reference_Voltage:LM4041LP-ADJ +Reference_Voltage:LM4050xEM3-2.1 +Reference_Voltage:LM4050xEM3-2.5 +Reference_Voltage:LM4050xEM3-3.0 +Reference_Voltage:LM4050xEM3-3.3 +Reference_Voltage:LM4050xEM3-4.1 +Reference_Voltage:LM4050xEM3-5.0 +Reference_Voltage:LM4050xEX3-2.1 +Reference_Voltage:LM4050xEX3-2.5 +Reference_Voltage:LM4050xEX3-3.3 +Reference_Voltage:LM4050xEX3-4.1 +Reference_Voltage:LM4050xEX3-5.0 +Reference_Voltage:LM4051xEM3-1.2 +Reference_Voltage:LM4051xEX3-1.2 Reference_Voltage:LM4125AIM5-2.5 Reference_Voltage:LM4125IM5-2.0 Reference_Voltage:LM4125IM5-2.5 @@ -16828,7 +16989,6 @@ Reference_Voltage:MCP1501-25xCH Reference_Voltage:MCP1501-25xRW Reference_Voltage:MCP1501-25xSN Reference_Voltage:MCP1501-30xCH -Reference_Voltage:MCP1501-30xRW Reference_Voltage:MCP1501-30xSN Reference_Voltage:MCP1501-33xCH Reference_Voltage:MCP1501-33xRW @@ -18813,7 +18973,6 @@ Regulator_Switching:ADP2360xCP-5.0 Regulator_Switching:ADP5054 Regulator_Switching:ADP5070AREZ Regulator_Switching:ADP5071AREZ -Regulator_Switching:ADuM6000 Regulator_Switching:AOZ1280CI Regulator_Switching:AOZ1282CI Regulator_Switching:AOZ1282CI-1 @@ -18863,15 +19022,6 @@ Regulator_Switching:BD9778F Regulator_Switching:BD9778HFP Regulator_Switching:BD9781HFP Regulator_Switching:BD9G341EFJ -Regulator_Switching:CRE1S0305S3C -Regulator_Switching:CRE1S0505DC -Regulator_Switching:CRE1S0505S3C -Regulator_Switching:CRE1S0505SC -Regulator_Switching:CRE1S0515SC -Regulator_Switching:CRE1S1205SC -Regulator_Switching:CRE1S1212SC -Regulator_Switching:CRE1S2405SC -Regulator_Switching:CRE1S2412SC Regulator_Switching:DIO6970 Regulator_Switching:EA3036CQB Regulator_Switching:EA3058QD @@ -19261,6 +19411,7 @@ Regulator_Switching:LT1373HVCN8 Regulator_Switching:LT1373HVCS8 Regulator_Switching:LT1377CN8 Regulator_Switching:LT1377CS8 +Regulator_Switching:LT1931 Regulator_Switching:LT1945 Regulator_Switching:LT3430 Regulator_Switching:LT3430-1 @@ -19451,35 +19602,6 @@ Regulator_Switching:NID60S24-05 Regulator_Switching:NID60S24-12 Regulator_Switching:NID60S24-15 Regulator_Switching:NID60S48-24 -Regulator_Switching:NMA0505DC -Regulator_Switching:NMA0505SC -Regulator_Switching:NMA0509DC -Regulator_Switching:NMA0509SC -Regulator_Switching:NMA0512DC -Regulator_Switching:NMA0512SC -Regulator_Switching:NMA0515DC -Regulator_Switching:NMA0515SC -Regulator_Switching:NMA1205DC -Regulator_Switching:NMA1205SC -Regulator_Switching:NMA1209DC -Regulator_Switching:NMA1209SC -Regulator_Switching:NMA1212DC -Regulator_Switching:NMA1212SC -Regulator_Switching:NMA1215DC -Regulator_Switching:NMA1215SC -Regulator_Switching:NMA1505DC -Regulator_Switching:NMA1505SC -Regulator_Switching:NMA1512DC -Regulator_Switching:NMA1512SC -Regulator_Switching:NMA1515DC -Regulator_Switching:NMA1515SC -Regulator_Switching:NXE1S0303MC -Regulator_Switching:NXE1S0305MC -Regulator_Switching:NXE1S0505MC -Regulator_Switching:NXE2S0505MC -Regulator_Switching:NXE2S1205MC -Regulator_Switching:NXE2S1212MC -Regulator_Switching:NXE2S1215MC Regulator_Switching:PAM2301CAAB120 Regulator_Switching:PAM2301CAAB330 Regulator_Switching:PAM2301CAABADJ @@ -19571,50 +19693,6 @@ Regulator_Switching:TLV62569ADRL Regulator_Switching:TLV62569DBV Regulator_Switching:TLV62569DDC Regulator_Switching:TLV62569DRL -Regulator_Switching:TMR_1-0511 -Regulator_Switching:TMR_1-0511SM -Regulator_Switching:TMR_1-0512 -Regulator_Switching:TMR_1-0512SM -Regulator_Switching:TMR_1-0513 -Regulator_Switching:TMR_1-0513SM -Regulator_Switching:TMR_1-0515 -Regulator_Switching:TMR_1-0522 -Regulator_Switching:TMR_1-0522SM -Regulator_Switching:TMR_1-0523 -Regulator_Switching:TMR_1-0523SM -Regulator_Switching:TMR_1-1211 -Regulator_Switching:TMR_1-1211SM -Regulator_Switching:TMR_1-1212 -Regulator_Switching:TMR_1-1212SM -Regulator_Switching:TMR_1-1213 -Regulator_Switching:TMR_1-1213SM -Regulator_Switching:TMR_1-1215 -Regulator_Switching:TMR_1-1222 -Regulator_Switching:TMR_1-1222SM -Regulator_Switching:TMR_1-1223 -Regulator_Switching:TMR_1-1223SM -Regulator_Switching:TMR_1-2411 -Regulator_Switching:TMR_1-2411SM -Regulator_Switching:TMR_1-2412 -Regulator_Switching:TMR_1-2412SM -Regulator_Switching:TMR_1-2413 -Regulator_Switching:TMR_1-2413SM -Regulator_Switching:TMR_1-2415 -Regulator_Switching:TMR_1-2422 -Regulator_Switching:TMR_1-2422SM -Regulator_Switching:TMR_1-2423 -Regulator_Switching:TMR_1-2423SM -Regulator_Switching:TMR_1-4811 -Regulator_Switching:TMR_1-4811SM -Regulator_Switching:TMR_1-4812 -Regulator_Switching:TMR_1-4812SM -Regulator_Switching:TMR_1-4813 -Regulator_Switching:TMR_1-4813SM -Regulator_Switching:TMR_1-4815 -Regulator_Switching:TMR_1-4822 -Regulator_Switching:TMR_1-4822SM -Regulator_Switching:TMR_1-4823 -Regulator_Switching:TMR_1-4823SM Regulator_Switching:TNY263G Regulator_Switching:TNY263P Regulator_Switching:TNY264G @@ -20989,6 +21067,7 @@ Sensor_Temperature:MCP9501 Sensor_Temperature:MCP9502 Sensor_Temperature:MCP9503 Sensor_Temperature:MCP9504 +Sensor_Temperature:MCP96xx01x-x-MX Sensor_Temperature:MCP9700Ax-ELT Sensor_Temperature:MCP9700Ax-ETT Sensor_Temperature:MCP9700Ax-HLT @@ -22479,6 +22558,7 @@ Transistor_FET_Other:Q_NMOS_Depletion_GDS Transistor_FET_Other:Q_NMOS_Depletion_GSD Transistor_FET_Other:Q_NMOS_Depletion_SDG Transistor_FET_Other:Q_NMOS_Depletion_SGD +Transistor_FET_Other:SP010N70T8 Transistor_FET_Other:VNB35N07xx-E Transistor_FET_Other:VNP10N07 Transistor_FET_Other:VNP35N07xx-E diff --git a/src/Controller/AttachmentFileController.php b/src/Controller/AttachmentFileController.php index 01aeab11..7f48e661 100644 --- a/src/Controller/AttachmentFileController.php +++ b/src/Controller/AttachmentFileController.php @@ -93,6 +93,8 @@ class AttachmentFileController extends AbstractController //Set header content disposition, so that the file will be downloaded $response->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $attachment->getFilename()); + $this->setAttachmentCSPHeaders($response); + return $response; } @@ -112,6 +114,16 @@ class AttachmentFileController extends AbstractController //Set header content disposition, so that the file will be downloaded $response->setContentDisposition(ResponseHeaderBag::DISPOSITION_INLINE, $attachment->getFilename()); + $this->setAttachmentCSPHeaders($response); + + return $response; + } + + private function setAttachmentCSPHeaders(Response $response): Response + { + //Set an CSP that disallow to run any scripts, styles or images from the attachment render page, as it is not used anywhere else for now and can be a security risk if used without proper precautions, so it should be opt-in + $response->headers->set('Content-Security-Policy', "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; sandbox;"); + return $response; } diff --git a/src/Controller/BrowserPluginController.php b/src/Controller/BrowserPluginController.php index 1bb95787..403b0dc1 100644 --- a/src/Controller/BrowserPluginController.php +++ b/src/Controller/BrowserPluginController.php @@ -73,7 +73,7 @@ class BrowserPluginController extends AbstractController if (isset($activeProviders[$key])) { $urlProviders[] = [ 'id' => $key, - 'label' => $activeProviders[$key]->getProviderInfo()['name'], + 'label' => $activeProviders[$key]->getProviderInfo()->name, ]; } } diff --git a/src/Controller/HomepageController.php b/src/Controller/HomepageController.php index f64f281f..bd7f7593 100644 --- a/src/Controller/HomepageController.php +++ b/src/Controller/HomepageController.php @@ -27,6 +27,7 @@ use App\Entity\Parts\Part; use App\Services\System\AppSecretChecker; use App\Services\System\BannerHelper; use App\Services\System\GitVersionInfoProvider; +use App\Services\System\TrustedHostsChecker; use App\Services\System\UpdateAvailableFacade; use Doctrine\ORM\EntityManagerInterface; use Omines\DataTablesBundle\DataTableFactory; @@ -41,6 +42,7 @@ class HomepageController extends AbstractController private readonly DataTableFactory $dataTable, private readonly BannerHelper $bannerHelper, private readonly AppSecretChecker $appSecretChecker, + private readonly TrustedHostsChecker $trustedHostsChecker, ) { } @@ -90,6 +92,7 @@ class HomepageController extends AbstractController 'new_version_url' => $updateAvailableManager->getLatestVersionUrl(), 'insecure_app_secret' => $this->appSecretChecker->isInsecureSecret(), 'suggested_app_secret' => $this->appSecretChecker->isInsecureSecret() ? $this->appSecretChecker->generateSecret() : null, + 'trusted_hosts_unconfigured' => $this->trustedHostsChecker->isTrustedHostsUnconfigured(), ]); } } diff --git a/src/Controller/InfoProviderController.php b/src/Controller/InfoProviderController.php index 28c281d0..09251043 100644 --- a/src/Controller/InfoProviderController.php +++ b/src/Controller/InfoProviderController.php @@ -88,7 +88,7 @@ class InfoProviderController extends AbstractController $this->denyAccessUnlessGranted('@info_providers.create_parts'); $providerInstance = $this->providerRegistry->getProviderByKey($provider); - $settingsClass = $providerInstance->getProviderInfo()['settings_class'] ?? throw new \LogicException('Provider ' . $provider . ' does not have a settings class defined'); + $settingsClass = $providerInstance->getProviderInfo()->settingsClass ?? throw new \LogicException('Provider ' . $provider . ' does not have a settings class defined'); //Create a clone of the settings object $settings = $this->settingsManager->createTemporaryCopy($settingsClass); diff --git a/src/Controller/ToolsController.php b/src/Controller/ToolsController.php index 76dffb4d..a92a863f 100644 --- a/src/Controller/ToolsController.php +++ b/src/Controller/ToolsController.php @@ -74,7 +74,7 @@ class ToolsController extends AbstractController 'detailed_error_pages' => $this->getParameter('partdb.error_pages.show_help'), 'error_page_admin_email' => $this->getParameter('partdb.error_pages.admin_email'), 'configured_max_file_size' => $settings->system->attachments->maxFileSize, - 'effective_max_file_size' => $attachmentSubmitHandler->getMaximumAllowedUploadSize(), + 'effective_max_file_size' => $attachmentSubmitHandler->getMaximumEffectiveUploadSize(), 'saml_enabled' => $this->getParameter('partdb.saml.enabled'), //PHP section diff --git a/src/Controller/TreeController.php b/src/Controller/TreeController.php index 71f8ba5c..b8c50d9b 100644 --- a/src/Controller/TreeController.php +++ b/src/Controller/TreeController.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace App\Controller; +use Symfony\Bridge\Doctrine\Attribute\MapEntity; use Symfony\Component\HttpFoundation\Response; use App\Entity\ProjectSystem\Project; use App\Entity\Parts\Category; @@ -55,7 +56,7 @@ class TreeController extends AbstractController #[Route(path: '/category/{id}', name: 'tree_category')] #[Route(path: '/categories', name: 'tree_category_root')] - public function categoryTree(?Category $category = null): JsonResponse + public function categoryTree(#[MapEntity(id: 'id')] ?Category $category = null): JsonResponse { if ($this->isGranted('@parts.read') && $this->isGranted('@categories.read')) { $tree = $this->treeGenerator->getTreeView(Category::class, $category, 'list_parts_root'); @@ -68,7 +69,7 @@ class TreeController extends AbstractController #[Route(path: '/footprint/{id}', name: 'tree_footprint')] #[Route(path: '/footprints', name: 'tree_footprint_root')] - public function footprintTree(?Footprint $footprint = null): JsonResponse + public function footprintTree(#[MapEntity(id: 'id')] ?Footprint $footprint = null): JsonResponse { if ($this->isGranted('@parts.read') && $this->isGranted('@footprints.read')) { $tree = $this->treeGenerator->getTreeView(Footprint::class, $footprint, 'list_parts_root'); @@ -80,7 +81,7 @@ class TreeController extends AbstractController #[Route(path: '/location/{id}', name: 'tree_location')] #[Route(path: '/locations', name: 'tree_location_root')] - public function locationTree(?StorageLocation $location = null): JsonResponse + public function locationTree(#[MapEntity(id: 'id')] ?StorageLocation $location = null): JsonResponse { if ($this->isGranted('@parts.read') && $this->isGranted('@storelocations.read')) { $tree = $this->treeGenerator->getTreeView(StorageLocation::class, $location, 'list_parts_root'); @@ -93,7 +94,7 @@ class TreeController extends AbstractController #[Route(path: '/manufacturer/{id}', name: 'tree_manufacturer')] #[Route(path: '/manufacturers', name: 'tree_manufacturer_root')] - public function manufacturerTree(?Manufacturer $manufacturer = null): JsonResponse + public function manufacturerTree(#[MapEntity(id: 'id')] ?Manufacturer $manufacturer = null): JsonResponse { if ($this->isGranted('@parts.read') && $this->isGranted('@manufacturers.read')) { $tree = $this->treeGenerator->getTreeView(Manufacturer::class, $manufacturer, 'list_parts_root'); @@ -106,7 +107,7 @@ class TreeController extends AbstractController #[Route(path: '/supplier/{id}', name: 'tree_supplier')] #[Route(path: '/suppliers', name: 'tree_supplier_root')] - public function supplierTree(?Supplier $supplier = null): JsonResponse + public function supplierTree(#[MapEntity(id: 'id')] ?Supplier $supplier = null): JsonResponse { if ($this->isGranted('@parts.read') && $this->isGranted('@suppliers.read')) { $tree = $this->treeGenerator->getTreeView(Supplier::class, $supplier, 'list_parts_root'); @@ -119,7 +120,7 @@ class TreeController extends AbstractController #[Route(path: '/device/{id}', name: 'tree_device')] #[Route(path: '/devices', name: 'tree_device_root')] - public function deviceTree(?Project $device = null): JsonResponse + public function deviceTree(#[MapEntity(id: 'id')] ?Project $device = null): JsonResponse { if ($this->isGranted('@projects.read')) { $tree = $this->treeGenerator->getTreeView(Project::class, $device, 'devices'); diff --git a/src/DataTables/AttachmentDataTable.php b/src/DataTables/AttachmentDataTable.php index 16e6a7a7..6c4c905a 100644 --- a/src/DataTables/AttachmentDataTable.php +++ b/src/DataTables/AttachmentDataTable.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace App\DataTables; +use App\DataTables\Column\HTMLColumn; use App\DataTables\Column\LocaleDateTimeColumn; use App\DataTables\Column\PrettyBoolColumn; use App\DataTables\Column\RowClassColumn; @@ -40,14 +41,19 @@ use Omines\DataTablesBundle\DataTable; use Omines\DataTablesBundle\DataTableTypeInterface; use Symfony\Contracts\Translation\TranslatorInterface; -final class AttachmentDataTable implements DataTableTypeInterface +final readonly class AttachmentDataTable implements DataTableTypeInterface { - public function __construct(private readonly TranslatorInterface $translator, private readonly EntityURLGenerator $entityURLGenerator, private readonly AttachmentManager $attachmentHelper, private readonly AttachmentURLGenerator $attachmentURLGenerator, private readonly ElementTypeNameGenerator $elementTypeNameGenerator) + public function __construct(private TranslatorInterface $translator, private EntityURLGenerator $entityURLGenerator, private AttachmentManager $attachmentHelper, private AttachmentURLGenerator $attachmentURLGenerator, private ElementTypeNameGenerator $elementTypeNameGenerator) { } public function configure(DataTable $dataTable, array $options): void { + /************************************************************************************************************* + * Avoid using render, as it has no escaping, and is a potential security risk. Use data on TextColumn or the + * HTMLColumn, if necessary + ************************************************************************************************************/ + $dataTable->add('dont_matter', RowClassColumn::class, [ 'render' => function ($value, Attachment $context): string { //Mark attachments yellow which have an internal file linked that doesn't exist @@ -59,10 +65,10 @@ final class AttachmentDataTable implements DataTableTypeInterface }, ]); - $dataTable->add('picture', TextColumn::class, [ + $dataTable->add('picture', HTMLColumn::class, [ 'label' => '', 'className' => 'no-colvis', - 'render' => function ($value, Attachment $context): string { + 'data' => function (Attachment $context): string { if ($context->isPicture() && $this->attachmentHelper->isInternalFileExisting($context)) { @@ -95,65 +101,65 @@ final class AttachmentDataTable implements DataTableTypeInterface 'orderField' => 'NATSORT(attachment.name)', ]); - $dataTable->add('attachment_type', TextColumn::class, [ + $dataTable->add('attachment_type', HTMLColumn::class, [ 'label' => 'attachment.table.type', 'field' => 'attachment_type.name', 'orderField' => 'NATSORT(attachment_type.name)', - 'render' => fn($value, Attachment $context): string => sprintf( + 'data' => fn(Attachment $context, $value): string => sprintf( '%s', $this->entityURLGenerator->editURL($context->getAttachmentType()), htmlspecialchars((string) $value) ), ]); - $dataTable->add('element', TextColumn::class, [ + $dataTable->add('element', HTMLColumn::class, [ 'label' => 'attachment.table.element', //'propertyPath' => 'element.name', - 'render' => fn($value, Attachment $context): string => sprintf( + 'data' => fn(Attachment $context): string => sprintf( '%s', $this->entityURLGenerator->infoURL($context->getElement()), $this->elementTypeNameGenerator->getTypeNameCombination($context->getElement(), true) ), ]); - $dataTable->add('internal_link', TextColumn::class, [ + $dataTable->add('internal_link', HTMLColumn::class, [ 'label' => 'attachment.table.internal_file', 'propertyPath' => 'filename', 'orderField' => 'NATSORT(attachment.original_filename)', - 'render' => function ($value, Attachment $context) { + 'data' => function (Attachment $context, $value) { if ($this->attachmentHelper->isInternalFileExisting($context)) { return sprintf( '%s', $this->entityURLGenerator->viewURL($context), - htmlspecialchars($value) + htmlspecialchars((string) $value) ); } - return $value; - } + return htmlspecialchars((string) $value); + }, ]); - $dataTable->add('external_link', TextColumn::class, [ + $dataTable->add('external_link', HTMLColumn::class, [ 'label' => 'attachment.table.external_link', 'propertyPath' => 'host', 'orderField' => 'attachment.external_path', - 'render' => function ($value, Attachment $context) { + 'data' => function (Attachment $context, $value) { if ($context->hasExternal()) { return sprintf( '%s', htmlspecialchars((string) $context->getExternalPath()), htmlspecialchars((string) $context->getExternalPath()), - htmlspecialchars($value), + htmlspecialchars((string) $value), ); } - return $value; - } + return htmlspecialchars((string) $value); + }, ]); - $dataTable->add('filesize', TextColumn::class, [ + $dataTable->add('filesize', HTMLColumn::class, [ 'label' => $this->translator->trans('attachment.table.filesize'), - 'render' => function ($value, Attachment $context) { + 'data' => function (Attachment $context) { if (!$context->hasInternal()) { return sprintf( ' @@ -168,7 +174,7 @@ final class AttachmentDataTable implements DataTableTypeInterface ' %s ', - $this->attachmentHelper->getHumanFileSize($context) + htmlspecialchars($this->attachmentHelper->getHumanFileSize($context)) ); } diff --git a/src/DataTables/Column/EntityColumn.php b/src/DataTables/Column/EntityColumn.php index 54ae3fb3..b5d71a08 100644 --- a/src/DataTables/Column/EntityColumn.php +++ b/src/DataTables/Column/EntityColumn.php @@ -78,7 +78,7 @@ class EntityColumn extends AbstractColumn ); } - return sprintf('%s', $value); + return sprintf('%s', htmlspecialchars($value)); } return ''; diff --git a/assets/css/components/bootbox_extensions.css b/src/DataTables/Column/HTMLColumn.php similarity index 58% rename from assets/css/components/bootbox_extensions.css rename to src/DataTables/Column/HTMLColumn.php index 42bbd78d..a1220dd3 100644 --- a/assets/css/components/bootbox_extensions.css +++ b/src/DataTables/Column/HTMLColumn.php @@ -1,7 +1,11 @@ +. */ +namespace App\DataTables\Column; -.modal-body > .bootbox-close-button { - position: absolute; - top: 0; - right: 0; - padding: 0.5rem 0.75rem; - z-index: 1; -} -.modal .bootbox-close-button { - font-weight: 100; -} +use Omines\DataTablesBundle\Column\TextColumn; -button.bootbox-close-button { - padding: 0; - background-color: transparent; - border: 0; - -webkit-appearance: none; +/** + * A TextColumn whose value is always treated as raw HTML and therefore never passed through htmlspecialchars(). + * The value returned by the 'data' option must already contain properly escaped/sanitized HTML, as it is output as-is. + */ +class HTMLColumn extends TextColumn +{ + public function isRaw(): bool + { + return true; + } } - -.bootbox-close-button { - /* float: right; */ - font-size: 1.40625rem; - font-weight: 600; - line-height: 1; - color: #000; - text-shadow: none; - opacity: .5; -} \ No newline at end of file diff --git a/src/DataTables/Column/IconLinkColumn.php b/src/DataTables/Column/IconLinkColumn.php index 6704cb4a..47b35d82 100644 --- a/src/DataTables/Column/IconLinkColumn.php +++ b/src/DataTables/Column/IconLinkColumn.php @@ -87,9 +87,9 @@ class IconLinkColumn extends AbstractColumn return sprintf( '', $disabled ? 'disabled' : '', - $href, - $title, - $icon + htmlspecialchars($href), + htmlspecialchars($title ?? ''), + htmlspecialchars($icon ?? '') ); } diff --git a/src/DataTables/ErrorDataTable.php b/src/DataTables/ErrorDataTable.php index 833ea934..a16b453e 100644 --- a/src/DataTables/ErrorDataTable.php +++ b/src/DataTables/ErrorDataTable.php @@ -22,9 +22,9 @@ declare(strict_types=1); */ namespace App\DataTables; +use App\DataTables\Column\HTMLColumn; use App\DataTables\Column\RowClassColumn; use Omines\DataTablesBundle\Adapter\ArrayAdapter; -use Omines\DataTablesBundle\Column\TextColumn; use Omines\DataTablesBundle\DataTable; use Omines\DataTablesBundle\DataTableFactory; use Omines\DataTablesBundle\DataTableTypeInterface; @@ -32,7 +32,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\OptionsResolver\OptionsResolver; -class ErrorDataTable implements DataTableTypeInterface +final readonly class ErrorDataTable implements DataTableTypeInterface { public function configureOptions(OptionsResolver $optionsResolver): void { @@ -49,6 +49,11 @@ class ErrorDataTable implements DataTableTypeInterface public function configure(DataTable $dataTable, array $options): void { + /************************************************************************************************************* + * Avoid using render, as it has no escaping, and is a potential security risk. Use data on TextColumn or the + * HTMLColumn, if necessary + ************************************************************************************************************/ + $optionsResolver = new OptionsResolver(); $this->configureOptions($optionsResolver); $options = $optionsResolver->resolve($options); @@ -58,9 +63,9 @@ class ErrorDataTable implements DataTableTypeInterface 'render' => fn($value, $context): string => 'table-warning', ]) - ->add('error', TextColumn::class, [ + ->add('error', HTMLColumn::class, [ 'label' => 'error_table.error', - 'render' => fn($value, $context): string => ' ' . $value, + 'data' => fn($context, $value): string => ' ' . htmlspecialchars((string) $value), ]) ; diff --git a/src/DataTables/Helpers/PartDataTableHelper.php b/src/DataTables/Helpers/PartDataTableHelper.php index 54094ff1..2f40dbd2 100644 --- a/src/DataTables/Helpers/PartDataTableHelper.php +++ b/src/DataTables/Helpers/PartDataTableHelper.php @@ -62,7 +62,7 @@ class PartDataTableHelper } if ($context->getBuiltProject() instanceof Project) { $icon = sprintf('', - $this->translator->trans('part.info.projectBuildPart.hint').': '.$context->getBuiltProject()->getName()); + $this->translator->trans('part.info.projectBuildPart.hint').': '.htmlspecialchars($context->getBuiltProject()->getName())); } diff --git a/src/DataTables/LogDataTable.php b/src/DataTables/LogDataTable.php index 2c37767b..5c4ca88b 100644 --- a/src/DataTables/LogDataTable.php +++ b/src/DataTables/LogDataTable.php @@ -25,6 +25,7 @@ namespace App\DataTables; use App\DataTables\Column\EnumColumn; use App\Entity\LogSystem\LogTargetType; use Symfony\Bundle\SecurityBundle\Security; +use App\DataTables\Column\HTMLColumn; use App\DataTables\Column\IconLinkColumn; use App\DataTables\Column\LocaleDateTimeColumn; use App\DataTables\Column\LogEntryExtraColumn; @@ -59,7 +60,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Contracts\Translation\TranslatorInterface; -class LogDataTable implements DataTableTypeInterface +final readonly class LogDataTable implements DataTableTypeInterface { protected LogEntryRepository $logRepo; @@ -95,6 +96,11 @@ class LogDataTable implements DataTableTypeInterface public function configure(DataTable $dataTable, array $options): void { + /************************************************************************************************************* + * Avoid using render, as it has no escaping, and is a potential security risk. Use data on TextColumn or the + * HTMLColumn, if necessary + ************************************************************************************************************/ + $resolver = new OptionsResolver(); $this->configureOptions($resolver); $options = $resolver->resolve($options); @@ -104,10 +110,10 @@ class LogDataTable implements DataTableTypeInterface 'render' => fn($value, AbstractLogEntry $context) => $this->logLevelHelper->logLevelToTableColorClass($context->getLevelString()), ]); - $dataTable->add('symbol', TextColumn::class, [ + $dataTable->add('symbol', HTMLColumn::class, [ 'label' => '', 'className' => 'no-colvis', - 'render' => fn($value, AbstractLogEntry $context): string => sprintf( + 'data' => fn(AbstractLogEntry $context): string => sprintf( '', $this->logLevelHelper->logLevelToIconClass($context->getLevelString()), $context->getLevelString() @@ -128,10 +134,10 @@ class LogDataTable implements DataTableTypeInterface ) ]); - $dataTable->add('type', TextColumn::class, [ + $dataTable->add('type', HTMLColumn::class, [ 'label' => 'log.type', 'propertyPath' => 'type', - 'render' => function (string $value, AbstractLogEntry $context) { + 'data' => function (AbstractLogEntry $context, string $value) { $text = $this->translator->trans('log.type.'.$value); if ($context instanceof PartStockChangedLogEntry) { @@ -149,20 +155,20 @@ class LogDataTable implements DataTableTypeInterface 'label' => 'log.level', 'visible' => 'system_log' === $options['mode'], 'propertyPath' => 'levelString', - 'render' => fn(string $value, AbstractLogEntry $context) => $this->translator->trans('log.level.'.$value), + 'data' => fn(AbstractLogEntry $context, string $value) => $this->translator->trans('log.level.'.$value), ]); - $dataTable->add('user', TextColumn::class, [ + $dataTable->add('user', HTMLColumn::class, [ 'label' => 'log.user', 'orderField' => 'NATSORT(user.name)', - 'render' => function ($value, AbstractLogEntry $context): string { + 'data' => function (AbstractLogEntry $context): string { $user = $context->getUser(); //If user was deleted, show the info from the username field if (!$user instanceof User) { if ($context->isCLIEntry()) { return sprintf('%s [%s]', - htmlentities((string) $context->getCLIUsername()), + htmlspecialchars((string) $context->getCLIUsername()), $this->translator->trans('log.cli_user') ); } @@ -170,7 +176,7 @@ class LogDataTable implements DataTableTypeInterface //Else we just deal with a deleted user return sprintf( '@%s [%s]', - htmlentities($context->getUsername()), + htmlspecialchars($context->getUsername()), $this->translator->trans('log.target_deleted'), ); } @@ -182,7 +188,7 @@ class LogDataTable implements DataTableTypeInterface $img_url, $this->userAvatarHelper->getAvatarMdURL($user), $this->urlGenerator->generate('user_info', ['id' => $user->getID()]), - htmlentities($user->getFullName(true)) + htmlspecialchars($user->getFullName(true)) ); }, ]); @@ -194,7 +200,7 @@ class LogDataTable implements DataTableTypeInterface 'render' => function (LogTargetType $value, AbstractLogEntry $context) { $class = $value->toClass(); if (null !== $class) { - return $this->elementTypeNameGenerator->getLocalizedTypeLabel($class); + return $this->elementTypeNameGenerator->typeLabel($class); } return ''; @@ -216,9 +222,9 @@ class LogDataTable implements DataTableTypeInterface 'icon' => 'fas fa-fw fa-eye', 'href' => function ($value, AbstractLogEntry $context) { if ( + $context instanceof CollectionElementDeleted || ($context instanceof TimeTravelInterface && $context->hasOldDataInformation()) - || $context instanceof CollectionElementDeleted ) { try { $target = $this->logRepo->getTargetElement($context); diff --git a/src/DataTables/PartsDataTable.php b/src/DataTables/PartsDataTable.php index bcf64056..5912a20e 100644 --- a/src/DataTables/PartsDataTable.php +++ b/src/DataTables/PartsDataTable.php @@ -25,6 +25,7 @@ namespace App\DataTables; use App\DataTables\Adapters\TwoStepORMAdapter; use App\DataTables\Column\EntityColumn; use App\DataTables\Column\EnumColumn; +use App\DataTables\Column\HTMLColumn; use App\DataTables\Column\IconLinkColumn; use App\DataTables\Column\LocaleDateTimeColumn; use App\DataTables\Column\MarkdownColumn; @@ -58,7 +59,7 @@ use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Contracts\Translation\TranslatorInterface; -final class PartsDataTable implements DataTableTypeInterface +final readonly class PartsDataTable implements DataTableTypeInterface { public const LENGTH_MENU = [[10, 25, 50, 100, 250, 500, -1], [10, 25, 50, 100, 250, 500, "All"]]; @@ -94,6 +95,11 @@ final class PartsDataTable implements DataTableTypeInterface * When adding columns here, add them also to PartTableColumns enum, to make them configurable in the settings! *************************************************************************************************************/ + /************************************************************************************************************* + * Avoid using render, as it has no escaping, and is a potential security risk. Use data on TextColumn or the + * HTMLColumn, if necessary + ************************************************************************************************************/ + $this->csh //Color the table rows depending on the review and favorite status ->add('row_color', RowClassColumn::class, [ @@ -109,23 +115,23 @@ final class PartsDataTable implements DataTableTypeInterface }, ], visibility_configurable: false) ->add('select', SelectColumn::class, visibility_configurable: false) - ->add('picture', TextColumn::class, [ + ->add('picture', HTMLColumn::class, [ 'label' => '', 'className' => 'no-colvis', - 'render' => fn($value, Part $context) => $this->partDataTableHelper->renderPicture($context), + 'data' => fn(Part $context) => $this->partDataTableHelper->renderPicture($context), ], visibility_configurable: false) - ->add('name', TextColumn::class, [ + ->add('name', HTMLColumn::class, [ 'label' => $this->translator->trans('part.table.name'), - 'render' => fn($value, Part $context) => $this->partDataTableHelper->renderName($context), + 'data' => fn(Part $context) => $this->partDataTableHelper->renderName($context), 'orderField' => 'NATSORT(part.name)' ]) ->add('si_value', TextColumn::class, [ 'label' => $this->translator->trans('part.table.si_value'), - 'render' => function ($value, Part $context): string { + 'data' => function (Part $context): string { $siValue = SiValueSort::sqliteSiValue($context->getName()); if ($siValue !== null) { //Output it as scientific number with a big E - return htmlspecialchars(sprintf('%G', $siValue)); + return sprintf('%G', $siValue); } return ''; }, @@ -156,38 +162,38 @@ final class PartsDataTable implements DataTableTypeInterface 'label' => $this->translator->trans('part.table.manufacturer'), 'orderField' => 'NATSORT(_manufacturer.name)' ]) - ->add('storelocation', TextColumn::class, [ + ->add('storelocation', HTMLColumn::class, [ 'label' => $this->translator->trans('part.table.storeLocations'), //We need to use a aggregate function to get the first store location, as we have a one-to-many relation 'orderField' => 'NATSORT(MIN(_storelocations.name))', - 'render' => fn($value, Part $context) => $this->partDataTableHelper->renderStorageLocations($context), + 'data' => fn(Part $context) => $this->partDataTableHelper->renderStorageLocations($context), ], alias: 'storage_location') - ->add('amount', TextColumn::class, [ + ->add('amount', HTMLColumn::class, [ 'label' => $this->translator->trans('part.table.amount'), - 'render' => fn($value, Part $context) => $this->partDataTableHelper->renderAmount($context), + 'data' => fn(Part $context) => $this->partDataTableHelper->renderAmount($context), 'orderField' => 'amountSum' ]) ->add('minamount', TextColumn::class, [ 'label' => $this->translator->trans('part.table.minamount'), - 'render' => fn($value, Part $context): string => htmlspecialchars($this->amountFormatter->format( + 'data' => fn(Part $context, $value): string => $this->amountFormatter->format( $value, $context->getPartUnit() - )), + ), ]) ->add('partUnit', TextColumn::class, [ 'label' => $this->translator->trans('part.table.partUnit'), 'orderField' => 'NATSORT(_partUnit.name)', - 'render' => function ($value, Part $context): string { + 'data' => function (Part $context): string { $partUnit = $context->getPartUnit(); if ($partUnit === null) { return ''; } - $tmp = htmlspecialchars($partUnit->getName()); + $tmp = $partUnit->getName(); if ($partUnit->getUnit()) { - $tmp .= ' (' . htmlspecialchars($partUnit->getUnit()) . ')'; + $tmp .= ' (' . $partUnit->getUnit() . ')'; } return $tmp; } @@ -195,14 +201,14 @@ final class PartsDataTable implements DataTableTypeInterface ->add('partCustomState', TextColumn::class, [ 'label' => $this->translator->trans('part.table.partCustomState'), 'orderField' => 'NATSORT(_partCustomState.name)', - 'render' => function($value, Part $context): string { + 'data' => function(Part $context): string { $partCustomState = $context->getPartCustomState(); if ($partCustomState === null) { return ''; } - return htmlspecialchars($partCustomState->getName()); + return $partCustomState->getName(); } ]) ->add('addedDate', LocaleDateTimeColumn::class, [ @@ -248,25 +254,25 @@ final class PartsDataTable implements DataTableTypeInterface ]) ->add('eda_reference', TextColumn::class, [ 'label' => $this->translator->trans('part.table.eda_reference'), - 'render' => static fn($value, Part $context) => htmlspecialchars($context->getEdaInfo()->getReferencePrefix() ?? ''), + 'data' => static fn(Part $context) => $context->getEdaInfo()->getReferencePrefix() ?? '', 'orderField' => 'NATSORT(part.eda_info.reference_prefix)' ]) ->add('eda_value', TextColumn::class, [ 'label' => $this->translator->trans('part.table.eda_value'), - 'render' => static fn($value, Part $context) => htmlspecialchars($context->getEdaInfo()->getValue() ?? ''), + 'data' => static fn(Part $context) => $context->getEdaInfo()->getValue() ?? '', 'orderField' => 'NATSORT(part.eda_info.value)' ]) - ->add('eda_status', TextColumn::class, [ + ->add('eda_status', HTMLColumn::class, [ 'label' => $this->translator->trans('part.table.eda_status'), - 'render' => fn($value, Part $context) => $this->partDataTableHelper->renderEdaStatus($context), + 'data' => fn(Part $context) => $this->partDataTableHelper->renderEdaStatus($context), 'className' => 'text-center', ]); //Add a column to list the projects where the part is used, when the user has the permission to see the projects if ($this->security->isGranted('read', Project::class)) { - $this->csh->add('projects', TextColumn::class, [ + $this->csh->add('projects', HTMLColumn::class, [ 'label' => $this->translator->trans('project.labelp'), - 'render' => function ($value, Part $context): string { + 'data' => function (Part $context): string { //Only show the first 5 projects names $projects = $context->getProjects(); $tmp = ""; @@ -286,7 +292,7 @@ final class PartsDataTable implements DataTableTypeInterface } return $tmp; - } + }, ]); } diff --git a/src/DataTables/ProjectBomEntriesDataTable.php b/src/DataTables/ProjectBomEntriesDataTable.php index b5beeca0..f65f0df7 100644 --- a/src/DataTables/ProjectBomEntriesDataTable.php +++ b/src/DataTables/ProjectBomEntriesDataTable.php @@ -25,6 +25,7 @@ namespace App\DataTables; use App\DataTables\Adapters\TwoStepORMAdapter; use App\DataTables\Column\EntityColumn; use App\DataTables\Column\EnumColumn; +use App\DataTables\Column\HTMLColumn; use App\DataTables\Column\LocaleDateTimeColumn; use App\DataTables\Column\MarkdownColumn; use App\DataTables\Helpers\PartDataTableHelper; @@ -48,7 +49,7 @@ use Omines\DataTablesBundle\DataTable; use Omines\DataTablesBundle\DataTableTypeInterface; use Symfony\Contracts\Translation\TranslatorInterface; -class ProjectBomEntriesDataTable implements DataTableTypeInterface +final readonly class ProjectBomEntriesDataTable implements DataTableTypeInterface { public function __construct( protected EntityURLGenerator $entityURLGenerator, @@ -63,17 +64,22 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface public function configure(DataTable $dataTable, array $options): void { + /************************************************************************************************************* + * Avoid using render, as it has no escaping, and is a potential security risk. Use data on TextColumn or the + * HTMLColumn, if necessary + ************************************************************************************************************/ + $dataTable //->add('select', SelectColumn::class) - ->add('picture', TextColumn::class, [ + ->add('picture', HTMLColumn::class, [ 'label' => '', 'className' => 'no-colvis', - 'render' => function ($value, ProjectBOMEntry $context) { + 'data' => function (ProjectBOMEntry $context) { if(!$context->getPart() instanceof Part) { return ''; } return $this->partDataTableHelper->renderPicture($context->getPart()); - } + }, ]) ->add('id', TextColumn::class, [ @@ -85,27 +91,27 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface 'label' => $this->translator->trans('project.bom.quantity'), 'className' => 'text-center', 'orderField' => 'bom_entry.quantity', - 'render' => function ($value, ProjectBOMEntry $context): float|string { + 'data' => function (ProjectBOMEntry $context): float|string { //If we have a non-part entry, only show the rounded quantity if (!$context->getPart() instanceof Part) { return round($context->getQuantity()); } //Otherwise use the unit of the part to format the quantity - return htmlspecialchars($this->amountFormatter->format($context->getQuantity(), $context->getPart()->getPartUnit())); + return $this->amountFormatter->format($context->getQuantity(), $context->getPart()->getPartUnit()); }, ]) ->add('partId', TextColumn::class, [ 'label' => $this->translator->trans('project.bom.part_id'), 'visible' => true, 'orderField' => 'part.id', - 'render' => function ($value, ProjectBOMEntry $context) { + 'data' => function (ProjectBOMEntry $context) { return $context->getPart() instanceof Part ? (string) $context->getPart()->getId() : ''; }, ]) - ->add('name', TextColumn::class, [ + ->add('name', HTMLColumn::class, [ 'label' => $this->translator->trans('part.table.name'), 'orderField' => 'NATSORT(part.name)', - 'render' => function ($value, ProjectBOMEntry $context) { + 'data' => function (ProjectBOMEntry $context) { if(!$context->getPart() instanceof Part) { return htmlspecialchars((string) $context->getName()); } @@ -123,11 +129,7 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface 'label' => $this->translator->trans('part.table.ipn'), 'orderField' => 'NATSORT(part.ipn)', 'visible' => false, - 'render' => function ($value, ProjectBOMEntry $context) { - if($context->getPart() instanceof Part) { - return $context->getPart()->getIpn(); - } - } + 'data' => fn (ProjectBOMEntry $context) => $context->getPart()?->getIpn() ]) ->add('description', MarkdownColumn::class, [ 'label' => $this->translator->trans('part.table.description'), @@ -172,9 +174,9 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface }, ]) - ->add('mountnames', TextColumn::class, [ + ->add('mountnames', HTMLColumn::class, [ 'label' => 'project.bom.mountnames', - 'render' => function ($value, ProjectBOMEntry $context) { + 'data' => function (ProjectBOMEntry $context) { $html = ''; foreach (explode(',', $context->getMountnames()) as $mountname) { @@ -184,34 +186,34 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface }, ]) - ->add('instockAmount', TextColumn::class, [ + ->add('instockAmount', HTMLColumn::class, [ 'label' => 'project.bom.instockAmount', 'visible' => false, - 'render' => function ($value, ProjectBOMEntry $context) { + 'data' => function (ProjectBOMEntry $context) { if ($context->getPart() !== null) { return $this->partDataTableHelper->renderAmount($context->getPart()); } return ''; - } + }, ]) - ->add('storelocation', TextColumn::class, [ + ->add('storelocation', HTMLColumn::class, [ 'label' => $this->translator->trans('part.table.storeLocations'), //We need to use a aggregate function to get the first store location, as we have a one-to-many relation 'orderField' => 'NATSORT(MIN(_storelocations.name))', 'visible' => false, - 'render' => function ($value, ProjectBOMEntry $context) { + 'data' => function (ProjectBOMEntry $context) { if ($context->getPart() !== null) { return $this->partDataTableHelper->renderStorageLocations($context->getPart()); } return ''; - } + }, ]) ->add('price', TextColumn::class, [ 'label' => 'project.bom.price', 'visible' => false, - 'render' => function ($value, ProjectBOMEntry $context) { + 'data' => function (ProjectBOMEntry $context) { $price = $this->projectBuildHelper->getEntryUnitPrice($context); return $this->moneyFormatter->format($price->toScale(2, RoundingMode::Up)->toFloat(), null, 2, true); }, @@ -219,7 +221,7 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface ->add('ext_price', TextColumn::class, [ 'label' => 'project.bom.ext_price', 'visible' => false, - 'render' => function ($value, ProjectBOMEntry $context) { + 'data' => function (ProjectBOMEntry $context) { $price = $this->projectBuildHelper->getEntryUnitPrice($context); return $this->moneyFormatter->format( $price->multipliedBy(BigDecimal::fromFloatShortest($context->getQuantity())) diff --git a/src/Doctrine/Middleware/SQLiteRegexExtensionMiddlewareDriver.php b/src/Doctrine/Middleware/SQLiteRegexExtensionMiddlewareDriver.php index aa6108c9..f721f986 100644 --- a/src/Doctrine/Middleware/SQLiteRegexExtensionMiddlewareDriver.php +++ b/src/Doctrine/Middleware/SQLiteRegexExtensionMiddlewareDriver.php @@ -27,6 +27,7 @@ use App\Doctrine\Functions\SiValueSort; use App\Exceptions\InvalidRegexException; use Doctrine\DBAL\Driver\Connection; use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware; +use Pdo\Sqlite; /** * This middleware is used to add the regexp operator to the SQLite platform. @@ -44,17 +45,30 @@ class SQLiteRegexExtensionMiddlewareDriver extends AbstractDriverMiddleware if ($params['driver'] === 'pdo_sqlite') { $native_connection = $connection->getNativeConnection(); - //Ensure that the function really exists on the connection, as it is marked as experimental according to PHP documentation if($native_connection instanceof \PDO) { - $native_connection->sqliteCreateFunction('REGEXP', self::regexp(...), 2, \PDO::SQLITE_DETERMINISTIC); - $native_connection->sqliteCreateFunction('FIELD', self::field(...), -1, \PDO::SQLITE_DETERMINISTIC); - $native_connection->sqliteCreateFunction('FIELD2', self::field2(...), 2, \PDO::SQLITE_DETERMINISTIC); - //Create a new collation for natural sorting - $native_connection->sqliteCreateCollation('NATURAL_CMP', strnatcmp(...)); + //Use the new PDO::createFunction and PDO::createCollation methods if available (PHP 8.4+) + if (is_a($native_connection, Sqlite::class)) { #TODO: Remove this check when PHP 8.4 is the minimum requirement + $native_connection->createFunction('REGEXP', self::regexp(...), 2, Sqlite::DETERMINISTIC); + $native_connection->createFunction('FIELD', self::field(...), -1, Sqlite::DETERMINISTIC); + $native_connection->createFunction('FIELD2', self::field2(...), 2, Sqlite::DETERMINISTIC); - //Create a function for SI prefix value sorting - $native_connection->sqliteCreateFunction('SI_VALUE', SiValueSort::sqliteSiValue(...), 1, \PDO::SQLITE_DETERMINISTIC); + //Create a new collation for natural sorting + $native_connection->createCollation('NATURAL_CMP', strnatcmp(...)); + + //Create a function for SI prefix value sorting + $native_connection->createFunction('SI_VALUE', SiValueSort::sqliteSiValue(...), 1, Sqlite::DETERMINISTIC); + } else { + $native_connection->sqliteCreateFunction('REGEXP', self::regexp(...), 2, \PDO::SQLITE_DETERMINISTIC); + $native_connection->sqliteCreateFunction('FIELD', self::field(...), -1, \PDO::SQLITE_DETERMINISTIC); + $native_connection->sqliteCreateFunction('FIELD2', self::field2(...), 2, \PDO::SQLITE_DETERMINISTIC); + + //Create a new collation for natural sorting + $native_connection->sqliteCreateCollation('NATURAL_CMP', strnatcmp(...)); + + //Create a function for SI prefix value sorting + $native_connection->sqliteCreateFunction('SI_VALUE', SiValueSort::sqliteSiValue(...), 1, \PDO::SQLITE_DETERMINISTIC); + } } } @@ -118,4 +132,4 @@ class SQLiteRegexExtensionMiddlewareDriver extends AbstractDriverMiddleware return $index + 1; } -} \ No newline at end of file +} diff --git a/src/Doctrine/Middleware/SetSQLModeMiddlewareDriver.php b/src/Doctrine/Middleware/SetSQLModeMiddlewareDriver.php index d05b6b9c..b8e70ff2 100644 --- a/src/Doctrine/Middleware/SetSQLModeMiddlewareDriver.php +++ b/src/Doctrine/Middleware/SetSQLModeMiddlewareDriver.php @@ -35,8 +35,10 @@ class SetSQLModeMiddlewareDriver extends AbstractDriverMiddleware { //Only set this on MySQL connections, as other databases don't support this parameter if($params['driver'] === 'pdo_mysql') { - //1002 is \PDO::MYSQL_ATTR_INIT_COMMAND constant value - $params['driverOptions'][\PDO::MYSQL_ATTR_INIT_COMMAND] = 'SET SESSION sql_mode=(SELECT REPLACE(@@sql_mode, \'ONLY_FULL_GROUP_BY\', \'\'))'; + //PDO::MYSQL_ATTR_INIT_COMMAND is deprecated since PHP 8.5 in favor of Pdo\Mysql::ATTR_INIT_COMMAND, + //but the Pdo\Mysql class only exists since PHP 8.4. Both constants have the same value (1002). + $initCommandAttr = class_exists(\Pdo\Mysql::class) ? \Pdo\Mysql::ATTR_INIT_COMMAND : \PDO::MYSQL_ATTR_INIT_COMMAND; + $params['driverOptions'][$initCommandAttr] = 'SET SESSION sql_mode=(SELECT REPLACE(@@sql_mode, \'ONLY_FULL_GROUP_BY\', \'\'))'; } return parent::connect($params); diff --git a/src/Entity/Attachments/AttachmentType.php b/src/Entity/Attachments/AttachmentType.php index 7a314ffe..03bb8031 100644 --- a/src/Entity/Attachments/AttachmentType.php +++ b/src/Entity/Attachments/AttachmentType.php @@ -65,21 +65,16 @@ use Symfony\Component\Validator\Constraints as Assert; new Post(securityPostDenormalize: 'is_granted("create", object)'), new Patch(security: 'is_granted("edit", object)'), new Delete(security: 'is_granted("delete", object)'), + new GetCollection( + uriTemplate: '/attachment_types/{id}/children.{_format}', + uriVariables: ['id' => new Link(fromProperty: 'children', fromClass: AttachmentType::class)], + openapi: new Operation(summary: 'Retrieves the children elements of an attachment type.'), + security: 'is_granted("@attachment_types.read")' + ), ], normalizationContext: ['groups' => ['attachment_type:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'], denormalizationContext: ['groups' => ['attachment_type:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'], )] -#[ApiResource( - uriTemplate: '/attachment_types/{id}/children.{_format}', - operations: [ - new GetCollection(openapi: new Operation(summary: 'Retrieves the children elements of an attachment type.'), - security: 'is_granted("@attachment_types.read")') - ], - uriVariables: [ - 'id' => new Link(fromProperty: 'children', fromClass: AttachmentType::class) - ], - normalizationContext: ['groups' => ['attachment_type:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'] -)] #[ApiFilter(PropertyFilter::class)] #[ApiFilter(LikeFilter::class, properties: ["name", "comment"])] #[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] diff --git a/src/Entity/Parts/Category.php b/src/Entity/Parts/Category.php index 22f8a3e4..316e116f 100644 --- a/src/Entity/Parts/Category.php +++ b/src/Entity/Parts/Category.php @@ -33,6 +33,8 @@ use ApiPlatform\Metadata\Delete; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\McpTool; +use ApiPlatform\Metadata\McpToolCollection; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; use ApiPlatform\OpenApi\Model\Operation; @@ -40,7 +42,12 @@ use ApiPlatform\Serializer\Filter\PropertyFilter; use App\ApiPlatform\Filter\LikeFilter; use App\Entity\Attachments\Attachment; use App\Entity\EDA\EDACategoryInfo; +use App\Mcp\DTO\ElementByIdInput; +use App\Mcp\DTO\StructuralElementOverview; +use App\Mcp\DTO\StructuralElementSearchInput; use App\Repository\Parts\CategoryRepository; +use App\State\Mcp\GetStructuralElementDetailsProcessor; +use App\State\Mcp\ListStructuralElementsProcessor; use Doctrine\DBAL\Types\Types; use Doctrine\Common\Collections\ArrayCollection; use App\Entity\Attachments\CategoryAttachment; @@ -68,22 +75,37 @@ use Symfony\Component\Validator\Constraints as Assert; new Post(securityPostDenormalize: 'is_granted("create", object)'), new Patch(security: 'is_granted("edit", object)'), new Delete(security: 'is_granted("delete", object)'), + new GetCollection( + uriTemplate: '/categories/{id}/children.{_format}', + uriVariables: ['id' => new Link(fromProperty: 'children', fromClass: Category::class)], + openapi: new Operation(summary: 'Retrieves the children elements of a category.'), + security: 'is_granted("@categories.read")' + ), ], normalizationContext: ['groups' => ['category:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'], denormalizationContext: ['groups' => ['category:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'], -)] -#[ApiResource( - uriTemplate: '/categories/{id}/children.{_format}', - operations: [ - new GetCollection( - openapi: new Operation(summary: 'Retrieves the children elements of a category.'), - security: 'is_granted("@categories.read")' - ) + mcp: [ + 'list_categories' => new McpToolCollection( + title: 'List/search categories', + description: 'List all part categories, optionally filtered by a keyword matched against the name and comment. Categories are used to group parts by their function. Each entry includes its full hierarchical path, and results are sorted by that path so parents are immediately followed by their own children, making it easy to derive the tree structure from the flat list.', + annotations: ['readOnlyHint' => true, 'destructiveHint' => false, 'idempotentHint' => true, 'openWorldHint' => false], + output: StructuralElementOverview::class, + normalizationContext: ['groups' => ['mcp_structural_overview:read']], + input: StructuralElementSearchInput::class, + security: 'is_granted("@categories.read")', + processor: ListStructuralElementsProcessor::class, + ), + 'get_category_details' => new McpTool( + title: 'Get category details by ID', + description: 'Get detailed information about a specific part category by its database ID.', + annotations: ['readOnlyHint' => true, 'destructiveHint' => false, 'idempotentHint' => true, 'openWorldHint' => false], + normalizationContext: ['groups' => ['category:read', 'api:basic:read']], + input: ElementByIdInput::class, + security: 'is_granted("@categories.read")', + validate: true, + processor: GetStructuralElementDetailsProcessor::class, + ), ], - uriVariables: [ - 'id' => new Link(fromProperty: 'children', fromClass: Category::class) - ], - normalizationContext: ['groups' => ['category:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'] )] #[ApiFilter(PropertyFilter::class)] #[ApiFilter(LikeFilter::class, properties: ["name", "comment"])] diff --git a/src/Entity/Parts/Footprint.php b/src/Entity/Parts/Footprint.php index 3d8be686..54c85734 100644 --- a/src/Entity/Parts/Footprint.php +++ b/src/Entity/Parts/Footprint.php @@ -33,6 +33,8 @@ use ApiPlatform\Metadata\Delete; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\McpTool; +use ApiPlatform\Metadata\McpToolCollection; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; use ApiPlatform\OpenApi\Model\Operation; @@ -40,7 +42,12 @@ use ApiPlatform\Serializer\Filter\PropertyFilter; use App\ApiPlatform\Filter\LikeFilter; use App\Entity\Attachments\Attachment; use App\Entity\EDA\EDAFootprintInfo; +use App\Mcp\DTO\ElementByIdInput; +use App\Mcp\DTO\StructuralElementOverview; +use App\Mcp\DTO\StructuralElementSearchInput; use App\Repository\Parts\FootprintRepository; +use App\State\Mcp\GetStructuralElementDetailsProcessor; +use App\State\Mcp\ListStructuralElementsProcessor; use App\Entity\Base\AbstractStructuralDBElement; use Doctrine\Common\Collections\ArrayCollection; use App\Entity\Attachments\FootprintAttachment; @@ -67,22 +74,37 @@ use Symfony\Component\Validator\Constraints as Assert; new Post(securityPostDenormalize: 'is_granted("create", object)'), new Patch(security: 'is_granted("edit", object)'), new Delete(security: 'is_granted("delete", object)'), + new GetCollection( + uriTemplate: '/footprints/{id}/children.{_format}', + uriVariables: ['id' => new Link(fromProperty: 'children', fromClass: Footprint::class)], + openapi: new Operation(summary: 'Retrieves the children elements of a footprint.'), + security: 'is_granted("@footprints.read")' + ), ], normalizationContext: ['groups' => ['footprint:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'], denormalizationContext: ['groups' => ['footprint:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'], -)] -#[ApiResource( - uriTemplate: '/footprints/{id}/children.{_format}', - operations: [ - new GetCollection( - openapi: new Operation(summary: 'Retrieves the children elements of a footprint.'), - security: 'is_granted("@footprints.read")' - ) + mcp: [ + 'list_footprints' => new McpToolCollection( + title: 'List/search footprints', + description: 'List all footprints, optionally filtered by a keyword matched against the name and comment. Footprints describe the physical package/shape of a part. Each entry includes its full hierarchical path, and results are sorted by that path so parents are immediately followed by their own children, making it easy to derive the tree structure from the flat list.', + annotations: ['readOnlyHint' => true, 'destructiveHint' => false, 'idempotentHint' => true, 'openWorldHint' => false], + output: StructuralElementOverview::class, + normalizationContext: ['groups' => ['mcp_structural_overview:read']], + input: StructuralElementSearchInput::class, + security: 'is_granted("@footprints.read")', + processor: ListStructuralElementsProcessor::class, + ), + 'get_footprint_details' => new McpTool( + title: 'Get footprint details by ID', + description: 'Get detailed information about a specific footprint by its database ID.', + annotations: ['readOnlyHint' => true, 'destructiveHint' => false, 'idempotentHint' => true, 'openWorldHint' => false], + normalizationContext: ['groups' => ['footprint:read', 'api:basic:read']], + input: ElementByIdInput::class, + security: 'is_granted("@footprints.read")', + validate: true, + processor: GetStructuralElementDetailsProcessor::class, + ), ], - uriVariables: [ - 'id' => new Link(fromProperty: 'children', fromClass: Footprint::class) - ], - normalizationContext: ['groups' => ['footprint:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'] )] #[ApiFilter(PropertyFilter::class)] #[ApiFilter(LikeFilter::class, properties: ["name", "comment"])] diff --git a/src/Entity/Parts/InfoProviderReference.php b/src/Entity/Parts/InfoProviderReference.php index 810aef0c..8f6874b9 100644 --- a/src/Entity/Parts/InfoProviderReference.php +++ b/src/Entity/Parts/InfoProviderReference.php @@ -28,9 +28,11 @@ use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping\Column; use Doctrine\ORM\Mapping\Embeddable; use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Validator\Constraints as Assert; +use Symfony\Component\Validator\Context\ExecutionContextInterface; /** - * This class represents a reference to a info provider inside a part. + * This class represents a reference to an info provider inside a part. * @see \App\Tests\Entity\Parts\InfoProviderReferenceTest */ #[Embeddable] @@ -157,4 +159,44 @@ class InfoProviderReference $ref->last_updated = new \DateTimeImmutable(); return $ref; } + + /** + * Creates a reference to an info provider based on the given parameters. + * @param string|null $provider_key + * @param string|null $provider_id + * @param string|null $provider_url + * @param \DateTimeImmutable|null $last_updated + * @return self + */ + public static function create(?string $provider_key, ?string $provider_id, ?string $provider_url, ?\DateTimeImmutable $last_updated): self + { + $ref = new InfoProviderReference(); + $ref->provider_key = $provider_key; + $ref->provider_id = $provider_id; + $ref->provider_url = $provider_url; + $ref->last_updated = $last_updated; + return $ref; + } + + #[Assert\Callback()] + public function validate(ExecutionContextInterface $context, mixed $payload): void + { + if ($this->provider_key === null && $this->provider_id !== null) { + $context->buildViolation('info_providers.validation.provider_id_without_key') + ->atPath('provider_key') + ->addViolation(); + } + + if ($this->provider_key === null && $this->provider_url !== null) { + $context->buildViolation('info_providers.validation.provider_url_without_key') + ->atPath('provider_url') + ->addViolation(); + } + + if ($this->provider_key !== null && $this->provider_id === null) { + $context->buildViolation('info_providers.validation.provider_key_without_id') + ->atPath('provider_id') + ->addViolation(); + } + } } diff --git a/src/Entity/Parts/Manufacturer.php b/src/Entity/Parts/Manufacturer.php index 0edf8232..343ce685 100644 --- a/src/Entity/Parts/Manufacturer.php +++ b/src/Entity/Parts/Manufacturer.php @@ -33,13 +33,20 @@ use ApiPlatform\Metadata\Delete; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\McpTool; +use ApiPlatform\Metadata\McpToolCollection; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; use ApiPlatform\OpenApi\Model\Operation; use ApiPlatform\Serializer\Filter\PropertyFilter; use App\ApiPlatform\Filter\LikeFilter; use App\Entity\Attachments\Attachment; +use App\Mcp\DTO\ElementByIdInput; +use App\Mcp\DTO\StructuralElementOverview; +use App\Mcp\DTO\StructuralElementSearchInput; use App\Repository\Parts\ManufacturerRepository; +use App\State\Mcp\GetStructuralElementDetailsProcessor; +use App\State\Mcp\ListStructuralElementsProcessor; use App\Entity\Base\AbstractStructuralDBElement; use Doctrine\Common\Collections\ArrayCollection; use App\Entity\Attachments\ManufacturerAttachment; @@ -66,22 +73,37 @@ use Symfony\Component\Validator\Constraints as Assert; new Post(securityPostDenormalize: 'is_granted("create", object)'), new Patch(security: 'is_granted("edit", object)'), new Delete(security: 'is_granted("delete", object)'), + new GetCollection( + uriTemplate: '/manufacturers/{id}/children.{_format}', + uriVariables: ['id' => new Link(fromProperty: 'children', fromClass: Manufacturer::class)], + openapi: new Operation(summary: 'Retrieves the children elements of a manufacturer.'), + security: 'is_granted("@manufacturers.read")' + ), ], normalizationContext: ['groups' => ['manufacturer:read', 'company:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'], denormalizationContext: ['groups' => ['manufacturer:write', 'company:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'], -)] -#[ApiResource( - uriTemplate: '/manufacturers/{id}/children.{_format}', - operations: [ - new GetCollection( - openapi: new Operation(summary: 'Retrieves the children elements of a manufacturer.'), - security: 'is_granted("@manufacturers.read")' - ) + mcp: [ + 'list_manufacturers' => new McpToolCollection( + title: 'List/search manufacturers', + description: 'List all manufacturers, optionally filtered by a keyword matched against the name and comment. Each entry includes its full hierarchical path, and results are sorted by that path so parents are immediately followed by their own children, making it easy to derive the tree structure from the flat list.', + annotations: ['readOnlyHint' => true, 'destructiveHint' => false, 'idempotentHint' => true, 'openWorldHint' => false], + output: StructuralElementOverview::class, + normalizationContext: ['groups' => ['mcp_structural_overview:read']], + input: StructuralElementSearchInput::class, + security: 'is_granted("@manufacturers.read")', + processor: ListStructuralElementsProcessor::class, + ), + 'get_manufacturer_details' => new McpTool( + title: 'Get manufacturer details by ID', + description: 'Get detailed information about a specific manufacturer by its database ID.', + annotations: ['readOnlyHint' => true, 'destructiveHint' => false, 'idempotentHint' => true, 'openWorldHint' => false], + normalizationContext: ['groups' => ['manufacturer:read', 'company:read', 'api:basic:read']], + input: ElementByIdInput::class, + security: 'is_granted("@manufacturers.read")', + validate: true, + processor: GetStructuralElementDetailsProcessor::class, + ), ], - uriVariables: [ - 'id' => new Link(fromProperty: 'children', fromClass: Manufacturer::class) - ], - normalizationContext: ['groups' => ['manufacturer:read', 'company:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'] )] #[ApiFilter(PropertyFilter::class)] #[ApiFilter(LikeFilter::class, properties: ["name", "comment"])] diff --git a/src/Entity/Parts/MeasurementUnit.php b/src/Entity/Parts/MeasurementUnit.php index 6dd0b9f2..dbf8036b 100644 --- a/src/Entity/Parts/MeasurementUnit.php +++ b/src/Entity/Parts/MeasurementUnit.php @@ -33,13 +33,20 @@ use ApiPlatform\Metadata\Delete; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\McpTool; +use ApiPlatform\Metadata\McpToolCollection; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; use ApiPlatform\OpenApi\Model\Operation; use ApiPlatform\Serializer\Filter\PropertyFilter; use App\ApiPlatform\Filter\LikeFilter; use App\Entity\Attachments\Attachment; +use App\Mcp\DTO\ElementByIdInput; +use App\Mcp\DTO\StructuralElementOverview; +use App\Mcp\DTO\StructuralElementSearchInput; use App\Repository\Parts\MeasurementUnitRepository; +use App\State\Mcp\GetStructuralElementDetailsProcessor; +use App\State\Mcp\ListStructuralElementsProcessor; use Doctrine\DBAL\Types\Types; use App\Entity\Base\AbstractStructuralDBElement; use Doctrine\Common\Collections\ArrayCollection; @@ -71,22 +78,37 @@ use Symfony\Component\Validator\Constraints\Length; new Post(securityPostDenormalize: 'is_granted("create", object)'), new Patch(security: 'is_granted("edit", object)'), new Delete(security: 'is_granted("delete", object)'), + new GetCollection( + uriTemplate: '/measurement_units/{id}/children.{_format}', + uriVariables: ['id' => new Link(fromProperty: 'children', fromClass: MeasurementUnit::class)], + openapi: new Operation(summary: 'Retrieves the children elements of a MeasurementUnit.'), + security: 'is_granted("@measurement_units.read")' + ), ], normalizationContext: ['groups' => ['measurement_unit:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'], denormalizationContext: ['groups' => ['measurement_unit:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'], -)] -#[ApiResource( - uriTemplate: '/measurement_units/{id}/children.{_format}', - operations: [ - new GetCollection( - openapi: new Operation(summary: 'Retrieves the children elements of a MeasurementUnit.'), - security: 'is_granted("@measurement_units.read")' - ) + mcp: [ + 'list_measurement_units' => new McpToolCollection( + title: 'List/search measurement units', + description: 'List all measurement units, optionally filtered by a keyword matched against the name and comment. Measurement units describe how the amount of a part is measured (e.g. pieces, meters, grams). Each entry includes its full hierarchical path, and results are sorted by that path so parents are immediately followed by their own children, making it easy to derive the tree structure from the flat list.', + annotations: ['readOnlyHint' => true, 'destructiveHint' => false, 'idempotentHint' => true, 'openWorldHint' => false], + output: StructuralElementOverview::class, + normalizationContext: ['groups' => ['mcp_structural_overview:read']], + input: StructuralElementSearchInput::class, + security: 'is_granted("@measurement_units.read")', + processor: ListStructuralElementsProcessor::class, + ), + 'get_measurement_unit_details' => new McpTool( + title: 'Get measurement unit details by ID', + description: 'Get detailed information about a specific measurement unit by its database ID.', + annotations: ['readOnlyHint' => true, 'destructiveHint' => false, 'idempotentHint' => true, 'openWorldHint' => false], + normalizationContext: ['groups' => ['measurement_unit:read', 'api:basic:read']], + input: ElementByIdInput::class, + security: 'is_granted("@measurement_units.read")', + validate: true, + processor: GetStructuralElementDetailsProcessor::class, + ), ], - uriVariables: [ - 'id' => new Link(fromProperty: 'children', fromClass: MeasurementUnit::class) - ], - normalizationContext: ['groups' => ['measurement_unit:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'] )] #[ApiFilter(PropertyFilter::class)] #[ApiFilter(LikeFilter::class, properties: ["name", "comment", "unit"])] diff --git a/src/Entity/Parts/Part.php b/src/Entity/Parts/Part.php index 5ac81b60..bbc0f8d1 100644 --- a/src/Entity/Parts/Part.php +++ b/src/Entity/Parts/Part.php @@ -32,6 +32,8 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Delete; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\McpTool; +use ApiPlatform\Metadata\McpToolCollection; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; use ApiPlatform\Serializer\Filter\PropertyFilter; @@ -39,6 +41,7 @@ use App\ApiPlatform\Filter\EntityFilter; use App\ApiPlatform\Filter\LikeFilter; use App\ApiPlatform\Filter\PartStoragelocationFilter; use App\ApiPlatform\Filter\TagFilter; +use App\DataTables\Filters\PartSearchFilter; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentContainingDBElement; use App\Entity\Attachments\PartAttachment; @@ -55,7 +58,10 @@ use App\Entity\Parts\PartTraits\ManufacturerTrait; use App\Entity\Parts\PartTraits\OrderTrait; use App\Entity\Parts\PartTraits\ProjectTrait; use App\EntityListeners\TreeCacheInvalidationListener; +use App\Mcp\DTO\ElementByIdInput; use App\Repository\PartRepository; +use App\State\Mcp\GetPartByIdProcessor; +use App\State\Mcp\SearchPartsProcessor; use App\Validator\Constraints\UniqueObjectCollection; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; @@ -80,7 +86,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; #[ORM\Index(columns: ['datetime_added', 'name', 'last_modified', 'id', 'needs_review'], name: 'parts_idx_datet_name_last_id_needs')] #[ORM\Index(columns: ['name'], name: 'parts_idx_name')] #[ORM\Index(columns: ['ipn'], name: 'parts_idx_ipn')] -#[ORM\Index(columns: ['gtin'], name: 'parts_idx_gtin')] +#[ORM\Index(name: 'parts_idx_gtin', columns: ['gtin'])] #[ApiResource( operations: [ new Get(normalizationContext: [ @@ -104,6 +110,27 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; ], normalizationContext: ['groups' => ['part:read', 'provider_reference:read', 'api:basic:read', 'part_lot:read'], 'openapi_definition_name' => 'Read'], denormalizationContext: ['groups' => ['part:write', 'api:basic:write', 'eda_info:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'], + mcp: [ + 'search_parts' => new McpToolCollection( + title: "Search parts by keyword", + description: 'Search for parts', + annotations: ['readOnlyHint' => true, 'destructiveHint' => false, 'idempotentHint' => true, 'openWorldHint' => false], + input: PartSearchFilter::class, + processor: SearchPartsProcessor::class, + ), + 'get_part_details' => new McpTool( + title: 'Get part details by ID', + description: 'Get detailed information about a specific part by its database ID', + annotations: ['readOnlyHint' => true, 'destructiveHint' => false, 'idempotentHint' => true, 'openWorldHint' => false], + normalizationContext: [ + 'groups' => ['part:read', 'provider_reference:read', 'api:basic:read', 'part_lot:read', 'orderdetail:read', 'pricedetail:read', 'parameter:read', 'attachment:read', 'eda_info:read'], + 'item_uri_template' => '/api/parts/{id}', + ], + input: ElementByIdInput::class, + validate: true, + processor: GetPartByIdProcessor::class + ), + ], )] #[ApiFilter(PropertyFilter::class)] #[ApiFilter(EntityFilter::class, properties: ["category", "footprint", "manufacturer", "partUnit", "partCustomState"])] diff --git a/src/Entity/Parts/PartCustomState.php b/src/Entity/Parts/PartCustomState.php index 136ff984..b5007e89 100644 --- a/src/Entity/Parts/PartCustomState.php +++ b/src/Entity/Parts/PartCustomState.php @@ -33,6 +33,8 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Delete; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\McpTool; +use ApiPlatform\Metadata\McpToolCollection; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; use ApiPlatform\Serializer\Filter\PropertyFilter; @@ -40,7 +42,12 @@ use App\ApiPlatform\Filter\LikeFilter; use App\Entity\Base\AbstractPartsContainingDBElement; use App\Entity\Base\AbstractStructuralDBElement; use App\Entity\Parameters\PartCustomStateParameter; +use App\Mcp\DTO\ElementByIdInput; +use App\Mcp\DTO\StructuralElementOverview; +use App\Mcp\DTO\StructuralElementSearchInput; use App\Repository\Parts\PartCustomStateRepository; +use App\State\Mcp\GetStructuralElementDetailsProcessor; +use App\State\Mcp\ListStructuralElementsProcessor; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Criteria; @@ -67,6 +74,28 @@ use Symfony\Component\Validator\Constraints as Assert; ], normalizationContext: ['groups' => ['part_custom_state:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'], denormalizationContext: ['groups' => ['part_custom_state:write', 'api:basic:write'], 'openapi_definition_name' => 'Write'], + mcp: [ + 'list_part_custom_states' => new McpToolCollection( + title: 'List/search part custom states', + description: 'List all part custom states, optionally filtered by a keyword matched against the name and comment. Custom states are used to mark parts with a custom status (e.g. "obsolete", "needs review"). Each entry includes its full hierarchical path, and results are sorted by that path so parents are immediately followed by their own children, making it easy to derive the tree structure from the flat list.', + annotations: ['readOnlyHint' => true, 'destructiveHint' => false, 'idempotentHint' => true, 'openWorldHint' => false], + output: StructuralElementOverview::class, + normalizationContext: ['groups' => ['mcp_structural_overview:read']], + input: StructuralElementSearchInput::class, + security: 'is_granted("@part_custom_states.read")', + processor: ListStructuralElementsProcessor::class, + ), + 'get_part_custom_state_details' => new McpTool( + title: 'Get part custom state details by ID', + description: 'Get detailed information about a specific part custom state by its database ID.', + annotations: ['readOnlyHint' => true, 'destructiveHint' => false, 'idempotentHint' => true, 'openWorldHint' => false], + normalizationContext: ['groups' => ['part_custom_state:read', 'api:basic:read']], + input: ElementByIdInput::class, + security: 'is_granted("@part_custom_states.read")', + validate: true, + processor: GetStructuralElementDetailsProcessor::class, + ), + ], )] #[ApiFilter(PropertyFilter::class)] #[ApiFilter(LikeFilter::class, properties: ["name"])] diff --git a/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php b/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php index 065469b5..9fa41f93 100644 --- a/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php +++ b/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php @@ -75,6 +75,7 @@ trait AdvancedPropertyTrait */ #[ORM\Embedded(class: InfoProviderReference::class, columnPrefix: 'provider_reference_')] #[Groups(['full', 'part:read'])] + #[Assert\Valid()] protected InfoProviderReference $providerReference; /** diff --git a/src/Entity/Parts/StorageLocation.php b/src/Entity/Parts/StorageLocation.php index 6c455ae5..a7c7176e 100644 --- a/src/Entity/Parts/StorageLocation.php +++ b/src/Entity/Parts/StorageLocation.php @@ -33,13 +33,20 @@ use ApiPlatform\Metadata\Delete; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\McpTool; +use ApiPlatform\Metadata\McpToolCollection; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; use ApiPlatform\OpenApi\Model\Operation; use ApiPlatform\Serializer\Filter\PropertyFilter; use App\ApiPlatform\Filter\LikeFilter; use App\Entity\Attachments\Attachment; +use App\Mcp\DTO\ElementByIdInput; +use App\Mcp\DTO\StructuralElementOverview; +use App\Mcp\DTO\StructuralElementSearchInput; use App\Repository\Parts\StorelocationRepository; +use App\State\Mcp\GetStructuralElementDetailsProcessor; +use App\State\Mcp\ListStructuralElementsProcessor; use Doctrine\DBAL\Types\Types; use Doctrine\Common\Collections\ArrayCollection; use App\Entity\Attachments\StorageLocationAttachment; @@ -67,22 +74,37 @@ use Symfony\Component\Validator\Constraints as Assert; new Post(securityPostDenormalize: 'is_granted("create", object)'), new Patch(security: 'is_granted("edit", object)'), new Delete(security: 'is_granted("delete", object)'), + new GetCollection( + uriTemplate: '/storage_locations/{id}/children.{_format}', + uriVariables: ['id' => new Link(fromProperty: 'children', fromClass: StorageLocation::class)], + openapi: new Operation(summary: 'Retrieves the children elements of a storage location.'), + security: 'is_granted("@storelocations.read")' + ), ], normalizationContext: ['groups' => ['location:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'], denormalizationContext: ['groups' => ['location:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'], -)] -#[ApiResource( - uriTemplate: '/storage_locations/{id}/children.{_format}', - operations: [ - new GetCollection( - openapi: new Operation(summary: 'Retrieves the children elements of a storage location.'), - security: 'is_granted("@storelocations.read")' - ) + mcp: [ + 'list_storage_locations' => new McpToolCollection( + title: 'List/search storage locations', + description: 'List all storage locations, optionally filtered by a keyword matched against the name and comment. Storage locations describe where parts are physically stored. Each entry includes its full hierarchical path, and results are sorted by that path so parents are immediately followed by their own children, making it easy to derive the tree structure from the flat list.', + annotations: ['readOnlyHint' => true, 'destructiveHint' => false, 'idempotentHint' => true, 'openWorldHint' => false], + output: StructuralElementOverview::class, + normalizationContext: ['groups' => ['mcp_structural_overview:read']], + input: StructuralElementSearchInput::class, + security: 'is_granted("@storelocations.read")', + processor: ListStructuralElementsProcessor::class, + ), + 'get_storage_location_details' => new McpTool( + title: 'Get storage location details by ID', + description: 'Get detailed information about a specific storage location by its database ID.', + annotations: ['readOnlyHint' => true, 'destructiveHint' => false, 'idempotentHint' => true, 'openWorldHint' => false], + normalizationContext: ['groups' => ['location:read', 'api:basic:read']], + input: ElementByIdInput::class, + security: 'is_granted("@storelocations.read")', + validate: true, + processor: GetStructuralElementDetailsProcessor::class, + ), ], - uriVariables: [ - 'id' => new Link(fromProperty: 'children', fromClass: Manufacturer::class) - ], - normalizationContext: ['groups' => ['location:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'] )] #[ApiFilter(PropertyFilter::class)] #[ApiFilter(LikeFilter::class, properties: ["name", "comment"])] diff --git a/src/Entity/Parts/Supplier.php b/src/Entity/Parts/Supplier.php index 2c004e9e..466f1789 100644 --- a/src/Entity/Parts/Supplier.php +++ b/src/Entity/Parts/Supplier.php @@ -33,13 +33,20 @@ use ApiPlatform\Metadata\Delete; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\McpTool; +use ApiPlatform\Metadata\McpToolCollection; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; use ApiPlatform\OpenApi\Model\Operation; use ApiPlatform\Serializer\Filter\PropertyFilter; use App\ApiPlatform\Filter\LikeFilter; use App\Entity\Attachments\Attachment; +use App\Mcp\DTO\ElementByIdInput; +use App\Mcp\DTO\StructuralElementOverview; +use App\Mcp\DTO\StructuralElementSearchInput; use App\Repository\Parts\SupplierRepository; +use App\State\Mcp\GetStructuralElementDetailsProcessor; +use App\State\Mcp\ListStructuralElementsProcessor; use App\Entity\PriceInformations\Orderdetail; use Doctrine\Common\Collections\ArrayCollection; use App\Entity\Attachments\SupplierAttachment; @@ -71,20 +78,37 @@ use Symfony\Component\Validator\Constraints as Assert; new Post(securityPostDenormalize: 'is_granted("create", object)'), new Patch(security: 'is_granted("edit", object)'), new Delete(security: 'is_granted("delete", object)'), + new GetCollection( + uriTemplate: '/suppliers/{id}/children.{_format}', + uriVariables: ['id' => new Link(fromProperty: 'children', fromClass: Supplier::class)], + openapi: new Operation(summary: 'Retrieves the children elements of a supplier.'), + security: 'is_granted("@manufacturers.read")' + ), ], normalizationContext: ['groups' => ['supplier:read', 'company:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'], denormalizationContext: ['groups' => ['supplier:write', 'company:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'], -)] -#[ApiResource( - uriTemplate: '/suppliers/{id}/children.{_format}', - operations: [new GetCollection( - openapi: new Operation(summary: 'Retrieves the children elements of a supplier.'), - security: 'is_granted("@manufacturers.read")' - )], - uriVariables: [ - 'id' => new Link(fromProperty: 'children', fromClass: Supplier::class) + mcp: [ + 'list_suppliers' => new McpToolCollection( + title: 'List/search suppliers', + description: 'List all suppliers, optionally filtered by a keyword matched against the name and comment. Each entry includes its full hierarchical path, and results are sorted by that path so parents are immediately followed by their own children, making it easy to derive the tree structure from the flat list.', + annotations: ['readOnlyHint' => true, 'destructiveHint' => false, 'idempotentHint' => true, 'openWorldHint' => false], + output: StructuralElementOverview::class, + normalizationContext: ['groups' => ['mcp_structural_overview:read']], + input: StructuralElementSearchInput::class, + security: 'is_granted("@suppliers.read")', + processor: ListStructuralElementsProcessor::class, + ), + 'get_supplier_details' => new McpTool( + title: 'Get supplier details by ID', + description: 'Get detailed information about a specific supplier by its database ID.', + annotations: ['readOnlyHint' => true, 'destructiveHint' => false, 'idempotentHint' => true, 'openWorldHint' => false], + normalizationContext: ['groups' => ['supplier:read', 'company:read', 'api:basic:read']], + input: ElementByIdInput::class, + security: 'is_granted("@suppliers.read")', + validate: true, + processor: GetStructuralElementDetailsProcessor::class, + ), ], - normalizationContext: ['groups' => ['supplier:read', 'company:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'] )] #[ApiFilter(PropertyFilter::class)] #[ApiFilter(LikeFilter::class, properties: ["name", "comment"])] diff --git a/src/Entity/PriceInformations/Currency.php b/src/Entity/PriceInformations/Currency.php index 4a811aa0..507ec810 100644 --- a/src/Entity/PriceInformations/Currency.php +++ b/src/Entity/PriceInformations/Currency.php @@ -71,23 +71,16 @@ use Symfony\Component\Validator\Constraints as Assert; new Post(securityPostDenormalize: 'is_granted("create", object)'), new Patch(security: 'is_granted("edit", object)'), new Delete(security: 'is_granted("delete", object)'), + new GetCollection( + uriTemplate: '/currencies/{id}/children.{_format}', + uriVariables: ['id' => new Link(fromProperty: 'children', fromClass: Currency::class)], + openapi: new Operation(summary: 'Retrieves the children elements of a currency.'), + security: 'is_granted("@currencies.read")' + ), ], normalizationContext: ['groups' => ['currency:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'], denormalizationContext: ['groups' => ['currency:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'], )] -#[ApiResource( - uriTemplate: '/currencies/{id}/children.{_format}', - operations: [ - new GetCollection( - openapi: new Operation(summary: 'Retrieves the children elements of a currency.'), - security: 'is_granted("@currencies.read")' - ) - ], - uriVariables: [ - 'id' => new Link(fromProperty: 'children', fromClass: Currency::class) - ], - normalizationContext: ['groups' => ['currency:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'] -)] #[ApiFilter(PropertyFilter::class)] #[ApiFilter(LikeFilter::class, properties: ["name", "comment", "iso_code"])] #[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] diff --git a/src/Entity/PriceInformations/Orderdetail.php b/src/Entity/PriceInformations/Orderdetail.php index 56428e3a..8b5c9f7b 100644 --- a/src/Entity/PriceInformations/Orderdetail.php +++ b/src/Entity/PriceInformations/Orderdetail.php @@ -71,23 +71,17 @@ use Symfony\Component\Validator\Constraints\Length; new Post(securityPostDenormalize: 'is_granted("create", object)'), new Patch(security: 'is_granted("edit", object)'), new Delete(security: 'is_granted("delete", object)'), + new GetCollection( + uriTemplate: '/parts/{id}/orderdetails.{_format}', + uriVariables: ['id' => new Link(toProperty: 'part', fromClass: Part::class)], + normalizationContext: ['groups' => ['orderdetail:read', 'pricedetail:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'], + openapi: new Operation(summary: 'Retrieves the orderdetails of a part.'), + security: 'is_granted("@parts.read")' + ), ], normalizationContext: ['groups' => ['orderdetail:read', 'orderdetail:read:standalone', 'api:basic:read', 'pricedetail:read'], 'openapi_definition_name' => 'Read'], denormalizationContext: ['groups' => ['orderdetail:write', 'api:basic:write'], 'openapi_definition_name' => 'Write'], )] -#[ApiResource( - uriTemplate: '/parts/{id}/orderdetails.{_format}', - operations: [ - new GetCollection( - openapi: new Operation(summary: 'Retrieves the orderdetails of a part.'), - security: 'is_granted("@parts.read")' - ) - ], - uriVariables: [ - 'id' => new Link(toProperty: 'part', fromClass: Part::class) - ], - normalizationContext: ['groups' => ['orderdetail:read', 'pricedetail:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'] -)] #[ApiFilter(PropertyFilter::class)] #[ApiFilter(PropertyFilter::class)] #[ApiFilter(LikeFilter::class, properties: ["supplierpartnr", "supplier_product_url"])] diff --git a/src/Entity/ProjectSystem/Project.php b/src/Entity/ProjectSystem/Project.php index a103d694..e170b376 100644 --- a/src/Entity/ProjectSystem/Project.php +++ b/src/Entity/ProjectSystem/Project.php @@ -31,13 +31,20 @@ use ApiPlatform\Metadata\Delete; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\McpTool; +use ApiPlatform\Metadata\McpToolCollection; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; use ApiPlatform\OpenApi\Model\Operation; use ApiPlatform\Serializer\Filter\PropertyFilter; use App\ApiPlatform\Filter\LikeFilter; use App\Entity\Attachments\Attachment; +use App\Mcp\DTO\ElementByIdInput; +use App\Mcp\DTO\StructuralElementOverview; +use App\Mcp\DTO\StructuralElementSearchInput; use App\Repository\Parts\DeviceRepository; +use App\State\Mcp\GetStructuralElementDetailsProcessor; +use App\State\Mcp\ListStructuralElementsProcessor; use App\Validator\Constraints\UniqueObjectCollection; use Doctrine\DBAL\Types\Types; use App\Entity\Attachments\ProjectAttachment; @@ -66,22 +73,37 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; new Post(securityPostDenormalize: 'is_granted("create", object)'), new Patch(security: 'is_granted("edit", object)'), new Delete(security: 'is_granted("delete", object)'), + new GetCollection( + uriTemplate: '/projects/{id}/children.{_format}', + uriVariables: ['id' => new Link(fromProperty: 'children', fromClass: Project::class)], + openapi: new Operation(summary: 'Retrieves the children elements of a project.'), + security: 'is_granted("@projects.read")' + ), ], normalizationContext: ['groups' => ['project:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'], denormalizationContext: ['groups' => ['project:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'], -)] -#[ApiResource( - uriTemplate: '/projects/{id}/children.{_format}', - operations: [ - new GetCollection( - openapi: new Operation(summary: 'Retrieves the children elements of a project.'), - security: 'is_granted("@projects.read")' - ) + mcp: [ + 'list_projects' => new McpToolCollection( + title: 'List/search projects', + description: 'List all projects, optionally filtered by a keyword matched against the name and comment. Each entry includes its full hierarchical path, and results are sorted by that path so parents are immediately followed by their own children, making it easy to derive the tree structure from the flat list. Use get_project_details for a specific project to retrieve its BOM entries, status and other details.', + annotations: ['readOnlyHint' => true, 'destructiveHint' => false, 'idempotentHint' => true, 'openWorldHint' => false], + output: StructuralElementOverview::class, + normalizationContext: ['groups' => ['mcp_structural_overview:read']], + input: StructuralElementSearchInput::class, + security: 'is_granted("@projects.read")', + processor: ListStructuralElementsProcessor::class, + ), + 'get_project_details' => new McpTool( + title: 'Get project details by ID', + description: 'Get detailed information about a specific project by its database ID, including its BOM entries, status, description and associated build part.', + annotations: ['readOnlyHint' => true, 'destructiveHint' => false, 'idempotentHint' => true, 'openWorldHint' => false], + normalizationContext: ['groups' => ['project:read', 'api:basic:read', 'mcp_project_details:read']], + input: ElementByIdInput::class, + security: 'is_granted("@projects.read")', + validate: true, + processor: GetStructuralElementDetailsProcessor::class, + ), ], - uriVariables: [ - 'id' => new Link(fromProperty: 'children', fromClass: Project::class) - ], - normalizationContext: ['groups' => ['project:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'] )] #[ApiFilter(PropertyFilter::class)] #[ApiFilter(LikeFilter::class, properties: ["name", "comment"])] @@ -105,7 +127,7 @@ class Project extends AbstractStructuralDBElement * @var Collection */ #[Assert\Valid] - #[Groups(['extended', 'full', 'import'])] + #[Groups(['extended', 'full', 'import', 'mcp_project_details:read'])] #[ORM\OneToMany(mappedBy: 'project', targetEntity: ProjectBOMEntry::class, cascade: ['persist', 'remove'], orphanRemoval: true)] #[UniqueObjectCollection(message: 'project.bom_entry.part_already_in_bom', fields: ['part'])] #[UniqueObjectCollection(message: 'project.bom_entry.name_already_in_bom', fields: ['name'])] @@ -117,7 +139,7 @@ class Project extends AbstractStructuralDBElement /** * @var string|null The current status of the project */ - #[Assert\Choice(['draft', 'planning', 'in_production', 'finished', 'archived'])] + #[Assert\Choice(choices: ['draft', 'planning', 'in_production', 'finished', 'archived'])] #[Groups(['extended', 'full', 'project:read', 'project:write', 'import'])] #[ORM\Column(type: Types::STRING, length: 64, nullable: true)] protected ?string $status = null; diff --git a/src/Entity/ProjectSystem/ProjectBOMEntry.php b/src/Entity/ProjectSystem/ProjectBOMEntry.php index 2a7862ec..b5fe61e7 100644 --- a/src/Entity/ProjectSystem/ProjectBOMEntry.php +++ b/src/Entity/ProjectSystem/ProjectBOMEntry.php @@ -63,23 +63,16 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; new Post(uriTemplate: '/project_bom_entries.{_format}', securityPostDenormalize: 'is_granted("create", object)',), new Patch(uriTemplate: '/project_bom_entries/{id}.{_format}', security: 'is_granted("edit", object)',), new Delete(uriTemplate: '/project_bom_entries/{id}.{_format}', security: 'is_granted("delete", object)',), + new GetCollection( + uriTemplate: '/projects/{id}/bom.{_format}', + uriVariables: ['id' => new Link(fromProperty: 'bom_entries', fromClass: Project::class)], + openapi: new Operation(summary: 'Retrieves the BOM entries of the given project.'), + security: 'is_granted("@projects.read")' + ), ], normalizationContext: ['groups' => ['bom_entry:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'], denormalizationContext: ['groups' => ['bom_entry:write', 'api:basic:write'], 'openapi_definition_name' => 'Write'], )] -#[ApiResource( - uriTemplate: '/projects/{id}/bom.{_format}', - operations: [ - new GetCollection( - openapi: new Operation(summary: 'Retrieves the BOM entries of the given project.'), - security: 'is_granted("@projects.read")' - ) - ], - uriVariables: [ - 'id' => new Link(fromProperty: 'bom_entries', fromClass: Project::class) - ], - normalizationContext: ['groups' => ['bom_entry:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'] -)] #[ApiFilter(PropertyFilter::class)] #[ApiFilter(LikeFilter::class, properties: ["name", "comment", 'mountnames'])] #[ApiFilter(RangeFilter::class, properties: ['quantity'])] @@ -90,14 +83,14 @@ class ProjectBOMEntry extends AbstractDBElement implements UniqueValidatableInte #[Assert\Positive] #[ORM\Column(name: 'quantity', type: Types::FLOAT)] - #[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'simple', 'extended', 'full'])] + #[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'simple', 'extended', 'full', 'mcp_project_details:read'])] protected float $quantity = 1.0; /** * @var string A comma separated list of the names, where this parts should be placed */ #[ORM\Column(name: 'mountnames', type: Types::TEXT)] - #[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'simple', 'extended', 'full'])] + #[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'simple', 'extended', 'full', 'mcp_project_details:read'])] protected string $mountnames = ''; /** @@ -105,14 +98,14 @@ class ProjectBOMEntry extends AbstractDBElement implements UniqueValidatableInte */ #[Assert\Expression('this.getPart() !== null or this.getName() !== null', message: 'validator.project.bom_entry.name_or_part_needed')] #[ORM\Column(type: Types::STRING, nullable: true)] - #[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'simple', 'extended', 'full'])] + #[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'simple', 'extended', 'full', 'mcp_project_details:read'])] protected ?string $name = null; /** * @var string An optional comment for this BOM entry */ #[ORM\Column(type: Types::TEXT)] - #[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'extended', 'full'])] + #[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'extended', 'full', 'mcp_project_details:read'])] protected string $comment = ''; /** @@ -128,7 +121,7 @@ class ProjectBOMEntry extends AbstractDBElement implements UniqueValidatableInte */ #[ORM\ManyToOne(targetEntity: Part::class, inversedBy: 'project_bom_entries')] #[ORM\JoinColumn(name: 'id_part')] - #[Groups(['bom_entry:read', 'bom_entry:write', 'full'])] + #[Groups(['bom_entry:read', 'bom_entry:write', 'full', 'mcp_project_details:read'])] protected ?Part $part = null; /** @@ -136,7 +129,7 @@ class ProjectBOMEntry extends AbstractDBElement implements UniqueValidatableInte */ #[Assert\AtLeastOneOf([new BigDecimalPositive(), new Assert\IsNull()])] #[ORM\Column(type: 'big_decimal', precision: 11, scale: 5, nullable: true)] - #[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'extended', 'full'])] + #[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'extended', 'full', 'mcp_project_details:read'])] protected ?BigDecimal $price = null; /** diff --git a/src/EventSubscriber/McpAccessSubscriber.php b/src/EventSubscriber/McpAccessSubscriber.php new file mode 100644 index 00000000..c7cab6e2 --- /dev/null +++ b/src/EventSubscriber/McpAccessSubscriber.php @@ -0,0 +1,60 @@ +. + */ + +declare(strict_types=1); + +namespace App\EventSubscriber; + +use App\Settings\AISettings\McpSettings; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\Event\RequestEvent; +use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException; +use Symfony\Component\HttpKernel\KernelEvents; + +readonly class McpAccessSubscriber implements EventSubscriberInterface +{ + public function __construct(private McpSettings $mcpSettings) + { + } + + public static function getSubscribedEvents(): array + { + return [ + KernelEvents::REQUEST => ['onKernelRequest', 10], + ]; + } + + public function onKernelRequest(RequestEvent $event): void + { + if (!$event->isMainRequest()) { + return; + } + + $path = $event->getRequest()->getPathInfo(); + + if (!str_starts_with($path, '/mcp')) { + return; + } + + if (!$this->mcpSettings->enabled) { + throw new ServiceUnavailableHttpException(null, 'The MCP endpoint is disabled. Enable it in the system settings.'); + } + } +} diff --git a/src/Exceptions/InfoProviderNotActiveException.php b/src/Exceptions/InfoProviderNotActiveException.php index 02f7cfb7..16e71af8 100644 --- a/src/Exceptions/InfoProviderNotActiveException.php +++ b/src/Exceptions/InfoProviderNotActiveException.php @@ -43,6 +43,7 @@ class InfoProviderNotActiveException extends \RuntimeException */ public static function fromProvider(InfoProviderInterface $provider): self { - return new self($provider->getProviderKey(), $provider->getProviderInfo()['name'] ?? '???'); + $info = $provider->getProviderInfo(); + return new self($info->key, $info->name); } } diff --git a/src/Form/AttachmentFormType.php b/src/Form/AttachmentFormType.php index d9fe2cd2..2773a950 100644 --- a/src/Form/AttachmentFormType.php +++ b/src/Form/AttachmentFormType.php @@ -209,7 +209,7 @@ class AttachmentFormType extends AbstractType public function finishView(FormView $view, FormInterface $form, array $options): void { - $view->vars['max_upload_size'] = $this->submitHandler->getMaximumAllowedUploadSize(); + $view->vars['max_upload_size'] = $this->submitHandler->getMaximumEffectiveUploadSize(); } public function getBlockPrefix(): string diff --git a/src/Form/Extension/DateTimeModelTimezoneExtension.php b/src/Form/Extension/DateTimeModelTimezoneExtension.php new file mode 100644 index 00000000..3c4818ea --- /dev/null +++ b/src/Form/Extension/DateTimeModelTimezoneExtension.php @@ -0,0 +1,79 @@ +. + */ + +declare(strict_types=1); + +namespace App\Form\Extension; + +use Symfony\Component\Form\AbstractTypeExtension; +use Symfony\Component\Form\Extension\Core\Type\DateTimeType; +use Symfony\Component\Form\Extension\Core\Type\DateType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; + +/** + * Catches timezone mismatches between a DateTimeInterface model value and the effective + * model_timezone configured on the field. + * + * Doctrine's UTCDateTimeImmutableType always returns UTC DateTimeImmutable objects, so any + * date/datetime field that omits `model_timezone: 'UTC'` will silently corrupt stored values + * (the transformer treats the UTC instant as if it were in the user's local timezone). + * This extension throws a \LogicException early so the mistake is caught at development time. + */ +class DateTimeModelTimezoneExtension extends AbstractTypeExtension +{ + public static function getExtendedTypes(): iterable + { + return [DateTimeType::class, DateType::class]; + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder->addEventListener(FormEvents::POST_SET_DATA, static function (FormEvent $event) use ($options): void { + $data = $event->getData(); + + if (!$data instanceof \DateTimeInterface) { + return; + } + + // Resolve the effective model timezone: explicit option or the PHP default set at build time. + // This mirrors what BaseDateTimeTransformer does in its constructor. + $modelTimezone = $options['model_timezone'] ?? date_default_timezone_get(); + + $dataOffset = $data->getTimezone()->getOffset($data); + $modelOffset = (new \DateTimeZone($modelTimezone))->getOffset($data); + + if ($dataOffset !== $modelOffset) { + throw new \LogicException(sprintf( + 'Form field "%s" received a %s with timezone "%s" (UTC offset %+d s), ' + . 'but the effective model_timezone is "%s" (UTC offset %+d s). ' + . 'Set the "model_timezone" option to match the timezone of your data source.', + $event->getForm()->getName(), + get_debug_type($data), + $data->getTimezone()->getName(), + $dataOffset, + $modelTimezone, + $modelOffset + )); + } + }); + } +} diff --git a/src/Form/InfoProviderSystem/InfoProviderReferenceType.php b/src/Form/InfoProviderSystem/InfoProviderReferenceType.php new file mode 100644 index 00000000..73fc8fe3 --- /dev/null +++ b/src/Form/InfoProviderSystem/InfoProviderReferenceType.php @@ -0,0 +1,113 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Form\InfoProviderSystem; + +use App\Entity\Parts\InfoProviderReference; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\DataMapperInterface; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\Extension\Core\Type\UrlType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class InfoProviderReferenceType extends AbstractType implements DataMapperInterface +{ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->setDataMapper($this) + ->add('provider_key', ProviderSelectType::class, [ + 'label' => 'info_providers.provider_key', + 'input' => 'string', + 'multiple' => false, + 'required' => false, + 'only_active' => false, + ]) + ->add('provider_id', TextType::class, [ + 'label' => 'info_providers.provider_id', + 'required' => false, + ]) + ->add('provider_url', UrlType::class, [ + 'label' => 'info_providers.provider_url', + 'required' => false, + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => InfoProviderReference::class, + ]); + } + + + public function mapDataToForms(mixed $viewData, \Traversable $forms): void + { + if ($viewData === null) { + return; + } + + if (!$viewData instanceof InfoProviderReference) { + return; + } + + /** @var FormInterface[] $forms */ + $forms = iterator_to_array($forms); + + $forms['provider_key']->setData($viewData->getProviderKey()); + $forms['provider_id']->setData($viewData->getProviderId()); + $forms['provider_url']->setData($viewData->getProviderUrl()); + } + + public function mapFormsToData(\Traversable $forms, mixed &$viewData): void + { + /** @var FormInterface[] $forms */ + $forms = iterator_to_array($forms); + + $providerKey = $forms['provider_key']->getData(); + $providerId = $forms['provider_id']->getData(); + $providerUrl = $forms['provider_url']->getData(); + + if ($viewData === null) { + $viewData = InfoProviderReference::noProvider(); + } + + if (!$viewData instanceof InfoProviderReference) { + return; + } + + $oldDate = $viewData->getLastUpdated(); + + //If all fields are empty, we set the view data to a new instance without provider information + if ($providerKey === null && $providerId === null && $providerUrl === null) { + $viewData = InfoProviderReference::noProvider(); + return; + } + + $viewData = InfoProviderReference::create($providerKey, $providerId, $providerUrl, $oldDate); + + } +} diff --git a/src/Form/InfoProviderSystem/ProviderSelectType.php b/src/Form/InfoProviderSystem/ProviderSelectType.php index bad3edaa..a29ebdf2 100644 --- a/src/Form/InfoProviderSystem/ProviderSelectType.php +++ b/src/Form/InfoProviderSystem/ProviderSelectType.php @@ -31,12 +31,12 @@ use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Translation\StaticMessage; +use Symfony\Component\Translation\TranslatableMessage; class ProviderSelectType extends AbstractType { public function __construct(private readonly ProviderRegistry $providerRegistry) { - } public function getParent(): string @@ -46,43 +46,63 @@ class ProviderSelectType extends AbstractType public function configureOptions(OptionsResolver $resolver): void { - $providers = $this->providerRegistry->getActiveProviders(); - $resolver->setDefault('input', 'object'); $resolver->setAllowedTypes('input', 'string'); //Either the form returns the provider objects or their keys $resolver->setAllowedValues('input', ['object', 'string']); $resolver->setDefault('multiple', true); - $resolver->setDefault('choices', function (Options $options) use ($providers) { + //Only show active providers in the list, or also inactive ones + $resolver->setDefault('only_active', true); + $resolver->setAllowedTypes('only_active', 'bool'); + + + $resolver->setDefault('choices', function (Options $options) { + $providers = $options['only_active'] ? $this->providerRegistry->getActiveProviders() : $this->providerRegistry->getProviders(); + if ('object' === $options['input']) { - return $this->providerRegistry->getActiveProviders(); + return $providers; } $tmp = []; foreach ($providers as $provider) { - $name = $provider->getProviderInfo()['name']; - $tmp[$name] = $provider->getProviderKey(); + $info = $provider->getProviderInfo(); + $tmp[$info->name] = $info->key; } return $tmp; }); //The choice_label and choice_value only needs to be set if we want the objects - $resolver->setDefault('choice_label', function (Options $options){ + $resolver->setDefault('choice_label', function (Options $options) { if ('object' === $options['input']) { - return ChoiceList::label($this, static fn (?InfoProviderInterface $choice) => new StaticMessage($choice?->getProviderInfo()['name'])); + return ChoiceList::label($this, static fn(?InfoProviderInterface $choice + ) => new StaticMessage($choice?->getProviderInfo()->name)); } - return static fn ($choice, $key, $value) => new StaticMessage($key); + return static fn($choice, $key, $value) => new StaticMessage($key); }); $resolver->setDefault('choice_value', function (Options $options) { if ('object' === $options['input']) { - return ChoiceList::value($this, static fn(?InfoProviderInterface $choice) => $choice?->getProviderKey()); + return ChoiceList::value($this, + static fn(?InfoProviderInterface $choice) => $choice?->getProviderInfo()->key); } return null; }); + $resolver->setDefault('group_by', function (Options $options) { + //Do not show groups when only active providers are shown, because then all providers are active and the group would be useless + if ($options['only_active']) { + return null; + } + + return function ($choice, $key, string $value) { + if ($this->providerRegistry->getProviderByKey($value)->isActive()) { + return new TranslatableMessage('info_providers.providers_list.active'); + } + return new TranslatableMessage('info_providers.providers_list.disabled'); + }; + }); } } diff --git a/src/Form/Part/PartBaseType.php b/src/Form/Part/PartBaseType.php index a31f2469..afef8fdb 100644 --- a/src/Form/Part/PartBaseType.php +++ b/src/Form/Part/PartBaseType.php @@ -33,6 +33,7 @@ use App\Entity\Parts\Part; use App\Entity\Parts\PartCustomState; use App\Entity\PriceInformations\Orderdetail; use App\Form\AttachmentFormType; +use App\Form\InfoProviderSystem\InfoProviderReferenceType; use App\Form\ParameterType; use App\Form\Part\EDA\EDAPartInfoType; use App\Form\Type\MasterPictureAttachmentType; @@ -225,6 +226,10 @@ class PartBaseType extends AbstractType 'empty_data' => null, 'label' => 'part.gtin', ]) + ->add('providerReference', InfoProviderReferenceType::class, [ + 'label' => false, + 'required' => false, + ]) ; //Comment section diff --git a/src/Form/Part/PartLotType.php b/src/Form/Part/PartLotType.php index fc330bb1..ef49c57e 100644 --- a/src/Form/Part/PartLotType.php +++ b/src/Form/Part/PartLotType.php @@ -115,8 +115,10 @@ class PartLotType extends AbstractType $builder->add('last_stocktake_at', DateTimeType::class, [ 'label' => 'part_lot.edit.last_stocktake_at', 'widget' => 'single_text', + 'model_timezone' => 'UTC', // The database stores the datetime in UTC, so we need to set the model timezone to UTC 'disabled' => !$this->security->isGranted('@parts_stock.stocktake'), 'required' => false, + 'with_seconds' => true, ]); } diff --git a/src/Form/Type/AttachmentTypeType.php b/src/Form/Type/AttachmentTypeType.php index 099ed282..95b5a254 100644 --- a/src/Form/Type/AttachmentTypeType.php +++ b/src/Form/Type/AttachmentTypeType.php @@ -38,7 +38,7 @@ class AttachmentTypeType extends AbstractType return StructuralEntityType::class; } - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->define('attachment_filter_class')->allowedTypes('null', 'string')->default(null); diff --git a/src/Helpers/Projects/ProjectBuildRequest.php b/src/Helpers/Projects/ProjectBuildRequest.php index 430d37b5..d2dfe139 100644 --- a/src/Helpers/Projects/ProjectBuildRequest.php +++ b/src/Helpers/Projects/ProjectBuildRequest.php @@ -84,8 +84,10 @@ final class ProjectBuildRequest $remaining_amount = $this->getNeededAmountForBOMEntry($bom_entry); foreach($this->getPartLotsForBOMEntry($bom_entry) as $lot) { //If the lot has instock use it for the build - $this->withdraw_amounts[$lot->getID()] = min($remaining_amount, $lot->getAmount()); - $remaining_amount -= max(0, $this->withdraw_amounts[$lot->getID()]); + $id = $lot->getID() ?? throw new \RuntimeException("Part lot needs to have an ID!"); + + $this->withdraw_amounts[$id] = min($remaining_amount, $lot->getAmount()); + $remaining_amount -= max(0, $this->withdraw_amounts[$id]); } } } @@ -176,6 +178,10 @@ final class ProjectBuildRequest { $lot_id = $lot instanceof PartLot ? $lot->getID() : $lot; + if ($lot_id === null) { + throw new \InvalidArgumentException('The given lot must have an ID!'); + } + if (! array_key_exists($lot_id, $this->withdraw_amounts)) { throw new \InvalidArgumentException('The given lot is not in the withdraw amounts array!'); } @@ -192,10 +198,12 @@ final class ProjectBuildRequest { if ($lot instanceof PartLot) { $lot_id = $lot->getID(); - } elseif (is_int($lot)) { - $lot_id = $lot; } else { - throw new \InvalidArgumentException('The given lot must be an instance of PartLot or an ID of a PartLot!'); + $lot_id = $lot; + } + + if ($lot_id === null) { + throw new \InvalidArgumentException('The given lot must have an ID!'); } $this->withdraw_amounts[$lot_id] = $amount; @@ -296,7 +304,7 @@ final class ProjectBuildRequest * @param bool $dont_check_quantity * @return $this */ - public function setDontCheckQuantity(bool $dont_check_quantity): ProjectBuildRequest + public function setDontCheckQuantity(bool $dont_check_quantity): self { $this->dont_check_quantity = $dont_check_quantity; return $this; diff --git a/src/Mcp/DTO/ElementByIdInput.php b/src/Mcp/DTO/ElementByIdInput.php new file mode 100644 index 00000000..bade3af2 --- /dev/null +++ b/src/Mcp/DTO/ElementByIdInput.php @@ -0,0 +1,35 @@ +. + */ + +declare(strict_types=1); + +namespace App\Mcp\DTO; + +use Symfony\Component\Validator\Constraints as Assert; + +readonly class ElementByIdInput +{ + public function __construct( + #[Assert\NotNull] + #[Assert\Positive] + public int $id, + ) { + } +} diff --git a/src/Mcp/DTO/InfoProviderPartDetailsInput.php b/src/Mcp/DTO/InfoProviderPartDetailsInput.php new file mode 100644 index 00000000..549b2f55 --- /dev/null +++ b/src/Mcp/DTO/InfoProviderPartDetailsInput.php @@ -0,0 +1,38 @@ +. + */ + +declare(strict_types=1); + +namespace App\Mcp\DTO; + +use Symfony\Component\Validator\Constraints as Assert; + +readonly class InfoProviderPartDetailsInput +{ + public function __construct( + /** @var string The key of the info provider (e.g. "digikey", "mouser", "lcsc"), as returned by search_info_providers */ + #[Assert\NotBlank] + public string $provider_key, + /** @var string The provider-specific ID of the part, as returned by search_info_providers */ + #[Assert\NotBlank] + public string $provider_id, + ) { + } +} diff --git a/src/Mcp/DTO/InfoProviderSearchInput.php b/src/Mcp/DTO/InfoProviderSearchInput.php new file mode 100644 index 00000000..e588b6da --- /dev/null +++ b/src/Mcp/DTO/InfoProviderSearchInput.php @@ -0,0 +1,41 @@ +. + */ + +declare(strict_types=1); + +namespace App\Mcp\DTO; + +use Symfony\Component\Validator\Constraints as Assert; + +readonly class InfoProviderSearchInput +{ + public function __construct( + /** @var string The keyword to search for (e.g. a part name, manufacturer product number or GTIN) */ + #[Assert\NotBlank] + public string $keyword, + /** + * @var string[]|null The keys of the info providers to search in (e.g. "digikey", "mouser", "lcsc"). If not + * given, the default search providers configured in the system settings are used. + */ + #[Assert\All([new Assert\Type('string')])] + public ?array $providers = null, + ) { + } +} diff --git a/src/Mcp/DTO/ListInfoProvidersInput.php b/src/Mcp/DTO/ListInfoProvidersInput.php new file mode 100644 index 00000000..83534ba9 --- /dev/null +++ b/src/Mcp/DTO/ListInfoProvidersInput.php @@ -0,0 +1,30 @@ +. + */ + +declare(strict_types=1); + +namespace App\Mcp\DTO; + +/** + * This tool takes no parameters, it just lists the currently available info providers. + */ +readonly class ListInfoProvidersInput +{ +} diff --git a/src/Mcp/DTO/StructuralElementOverview.php b/src/Mcp/DTO/StructuralElementOverview.php new file mode 100644 index 00000000..40591b44 --- /dev/null +++ b/src/Mcp/DTO/StructuralElementOverview.php @@ -0,0 +1,50 @@ +. + */ + +declare(strict_types=1); + +namespace App\Mcp\DTO; + +use Symfony\Component\Serializer\Annotation\Groups; + +/** + * A lean overview projection of a structural "master data" element (category, footprint, manufacturer, ...), + * used by the list_X MCP tools. Use the corresponding get_X_details tool with the id to fetch full details. + * + * The properties are tagged with the 'mcp_structural_overview:read' group (and the list_X mcp tools set + * this as their normalizationContext), because otherwise the operation would silently fall back to the + * backing entity's own (unrelated) normalization groups, which don't match these plain properties and + * would filter them out entirely. + */ +readonly class StructuralElementOverview +{ + public function __construct( + /** @var int The database ID of the element, to be used with the corresponding get_X_details tool */ + #[Groups(['mcp_structural_overview:read'])] + public int $id, + /** @var string The name of the element */ + #[Groups(['mcp_structural_overview:read'])] + public string $name, + /** @var string The full path of the element (including the names of all its parent elements, e.g. "Parent → Child") */ + #[Groups(['mcp_structural_overview:read'])] + public string $full_path, + ) { + } +} diff --git a/src/Mcp/DTO/StructuralElementSearchInput.php b/src/Mcp/DTO/StructuralElementSearchInput.php new file mode 100644 index 00000000..22a164e2 --- /dev/null +++ b/src/Mcp/DTO/StructuralElementSearchInput.php @@ -0,0 +1,36 @@ +. + */ + +declare(strict_types=1); + +namespace App\Mcp\DTO; + +/** + * Generic input for listing/searching one of the structural "master data" entities (categories, footprints, + * manufacturers, storage locations, suppliers, measurement units, part custom states). + */ +readonly class StructuralElementSearchInput +{ + public function __construct( + /** @var string|null Optional keyword to filter by name or comment (case-insensitive substring match). If omitted, all elements are returned. */ + public ?string $keyword = null, + ) { + } +} diff --git a/src/Repository/DBElementRepository.php b/src/Repository/DBElementRepository.php index f737d91d..6e13aff7 100644 --- a/src/Repository/DBElementRepository.php +++ b/src/Repository/DBElementRepository.php @@ -158,7 +158,6 @@ class DBElementRepository extends EntityRepository { $reflection = new ReflectionClass($element::class); $property = $reflection->getProperty($field); - $property->setAccessible(true); $property->setValue($element, $new_value); } } diff --git a/src/Serializer/PartNormalizer.php b/src/Serializer/PartNormalizer.php index 8486a634..50f76132 100644 --- a/src/Serializer/PartNormalizer.php +++ b/src/Serializer/PartNormalizer.php @@ -63,7 +63,9 @@ class PartNormalizer implements NormalizerInterface, DenormalizerInterface, Norm 'eda_exclude_bom' => 'eda_exclude_from_bom', 'eda_exclude_board' => 'eda_exclude_from_board', 'eda_exclude_sim' => 'eda_exclude_from_sim', - 'eda_invisible' => 'eda_visibility', + //NOTE: "eda_invisible" is intentionally NOT mapped here: it is the *inverse* of + //"eda_visibility", so a plain key rename would store the wrong value. It is handled + //(and inverted) explicitly in applyEdaFields(). ]; public function __construct( @@ -235,6 +237,9 @@ class PartNormalizer implements NormalizerInterface, DenormalizerInterface, Norm } if (isset($data['eda_visibility']) && $data['eda_visibility'] !== '') { $edaInfo->setVisibility(filter_var($data['eda_visibility'], FILTER_VALIDATE_BOOLEAN)); + } elseif (isset($data['eda_invisible']) && $data['eda_invisible'] !== '') { + //"eda_invisible" is the inverse convenience alias of "eda_visibility" + $edaInfo->setVisibility(!filter_var($data['eda_invisible'], FILTER_VALIDATE_BOOLEAN)); } } diff --git a/src/Serializer/StructuralElementOverviewNormalizer.php b/src/Serializer/StructuralElementOverviewNormalizer.php new file mode 100644 index 00000000..04a3ee9c --- /dev/null +++ b/src/Serializer/StructuralElementOverviewNormalizer.php @@ -0,0 +1,69 @@ +. + */ + +namespace App\Serializer; + +use App\Mcp\DTO\StructuralElementOverview; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +/** + * Normalizes StructuralElementOverview as a plain {id, name, full_path} object, without the JSON-LD + * "@id"/"@type" metadata that ApiPlatform\JsonLd\Serializer\ItemNormalizer would otherwise add (as a + * blank/genid node, since this DTO is intentionally not an API resource). That metadata is useless + * clutter for the list_X MCP tools, whose whole point is a lean overview. + * + * @see \App\Tests\Serializer\StructuralElementOverviewNormalizerTest + */ +class StructuralElementOverviewNormalizer implements NormalizerInterface +{ + public function supportsNormalization($data, ?string $format = null, array $context = []): bool + { + return $data instanceof StructuralElementOverview; + } + + /** + * @return array{id: int, name: string, full_path: string} + */ + public function normalize($object, ?string $format = null, array $context = []): array + { + if (!$object instanceof StructuralElementOverview) { + throw new \InvalidArgumentException('This normalizer only supports StructuralElementOverview objects!'); + } + + return [ + 'id' => $object->id, + 'name' => $object->name, + 'full_path' => $object->full_path, + ]; + } + + /** + * @return bool[] + */ + public function getSupportedTypes(?string $format): array + { + return [ + StructuralElementOverview::class => true, + ]; + } +} diff --git a/src/Services/AI/AIPlatforms.php b/src/Services/AI/AIPlatforms.php index 2f4d6317..318819a2 100644 --- a/src/Services/AI/AIPlatforms.php +++ b/src/Services/AI/AIPlatforms.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace App\Services\AI; use App\Settings\AISettings\LMStudioSettings; +use App\Settings\AISettings\OllamaSettings; use App\Settings\AISettings\OpenRouterSettings; use Symfony\Contracts\Translation\TranslatableInterface; use Symfony\Contracts\Translation\TranslatorInterface; @@ -32,6 +33,7 @@ enum AIPlatforms: string implements TranslatableInterface { case OPENROUTER = 'openrouter'; case LMSTUDIO = 'lmstudio'; + case OLLAMA = 'ollama'; /** * Returns the name attribute of the service tag for this platform, which is used to register the platform in the AIPlatformRegistry @@ -52,6 +54,7 @@ enum AIPlatforms: string implements TranslatableInterface return match ($this) { self::LMSTUDIO => LMStudioSettings::class, self::OPENROUTER => OpenRouterSettings::class, + self::OLLAMA => OllamaSettings::class, }; } diff --git a/src/Services/Attachments/AttachmentSubmitHandler.php b/src/Services/Attachments/AttachmentSubmitHandler.php index 2e40f1f5..5433fcfa 100644 --- a/src/Services/Attachments/AttachmentSubmitHandler.php +++ b/src/Services/Attachments/AttachmentSubmitHandler.php @@ -217,6 +217,14 @@ class AttachmentSubmitHandler //If no file was uploaded, but we have base64 encoded data, create a file from it if (!$file && $upload->data !== null) { + if (strlen($upload->data) > $this->getMaximumUserConfiguredUploadSize() * 4 / 3) { //Base64 encoding increases the size of the data by 4/3, so we have to check for that + throw new RuntimeException( + sprintf( + 'The given base64 data is too big! Maximum size is %.1f MB!', + $this->getMaximumUserConfiguredUploadSize() / 1000 / 1000 + )); + } + $file = new UploadedBase64EncodedFile(new Base64EncodedFile($upload->data), $upload->filename ?? 'base64'); } @@ -225,6 +233,15 @@ class AttachmentSubmitHandler //When a file is given then upload it, otherwise check if we need to download the URL if ($file instanceof UploadedFile) { + //Check the file size, to avoid uploading too big files. + //The file is not necessarily validated as it can also come from an Base64 source + if ($file->getSize() > $this->getMaximumUserConfiguredUploadSize()) { + throw new RuntimeException( + sprintf( + 'The uploaded file is too big! Maximum size is %.1f MB!', + $this->getMaximumUserConfiguredUploadSize() / 1000 / 1000 + )); + } $this->upload($attachment, $file, $secure_attachment); } elseif ($upload->downloadUrl && $attachment->hasExternal()) { @@ -399,9 +416,30 @@ class AttachmentSubmitHandler //Open a temporary file in the attachment folder $fs->mkdir($attachment_folder); $fileHandler = fopen($tmp_path, 'wb'); + + $bytesDownloaded = 0; + $maxSize = $this->getMaximumUserConfiguredUploadSize(); //We use the maximum user configured size here, PHPs limits dont apply + //Write the downloaded data to file foreach ($this->httpClient->stream($response) as $chunk) { - fwrite($fileHandler, $chunk->getContent()); + $content = $chunk->getContent(); + $bytesDownloaded += strlen($content); + + //Ensure the size does not get too large to avoid filling up the disk easily. + //If the file is too big, cancel the download and delete the temporary file. + if ($bytesDownloaded > $maxSize) { + $response->cancel(); + fclose($fileHandler); + unlink($tmp_path); //Delete the temporary file, because it is too big + + throw new AttachmentDownloadException( + sprintf( + 'The downloaded file is too big! Maximum size is %.1f MB!', + $maxSize / 1000 / 1000 + )); + } + + fwrite($fileHandler, $content); } fclose($fileHandler); @@ -505,10 +543,10 @@ class AttachmentSubmitHandler } /* - * Returns the maximum allowed upload size in bytes. + * Returns the maximum effective upload size in bytes. * This is the minimum value of Part-DB max_file_size, and php.ini's post_max_size and upload_max_filesize. */ - public function getMaximumAllowedUploadSize(): int + public function getMaximumEffectiveUploadSize(): int { if ($this->max_upload_size_bytes) { return $this->max_upload_size_bytes; @@ -523,6 +561,15 @@ class AttachmentSubmitHandler return $this->max_upload_size_bytes; } + /** + * Returns the maximum user configured upload size in bytes. + * @return int + */ + public function getMaximumUserConfiguredUploadSize(): int + { + return $this->parseFileSizeString($this->settings->maxFileSize); + } + /** * Sanitizes the given SVG file, if the attachment is an internal SVG file. * @param Attachment $attachment @@ -543,8 +590,10 @@ class AttachmentSubmitHandler return $attachment; } + $guessed_mime_type = $this->mimeTypes->guessMimeType($path); + //Check if the file is an SVG - if ($attachment->getExtension() === "svg") { + if ($guessed_mime_type === "image/svg+xml" || $attachment->getExtension() === "svg") { $this->SVGSanitizer->sanitizeFile($path); } diff --git a/src/Services/ImportExportSystem/BOMValidationService.php b/src/Services/ImportExportSystem/BOMValidationService.php index 74f81fe3..9f4cf5b8 100644 --- a/src/Services/ImportExportSystem/BOMValidationService.php +++ b/src/Services/ImportExportSystem/BOMValidationService.php @@ -29,13 +29,13 @@ use Symfony\Contracts\Translation\TranslatorInterface; /** * Service for validating BOM import data with comprehensive validation rules - * and user-friendly error messages. + * and user-friendly error messages. The results are not HTML safe, and must be escaped before display! */ -class BOMValidationService +readonly class BOMValidationService { public function __construct( - private readonly EntityManagerInterface $entityManager, - private readonly TranslatorInterface $translator + private EntityManagerInterface $entityManager, + private TranslatorInterface $translator ) { } @@ -473,4 +473,4 @@ class BOMValidationService : 0, ]; } -} \ No newline at end of file +} diff --git a/src/Services/ImportExportSystem/EntityExporter.php b/src/Services/ImportExportSystem/EntityExporter.php index ab87a905..4cd92cc4 100644 --- a/src/Services/ImportExportSystem/EntityExporter.php +++ b/src/Services/ImportExportSystem/EntityExporter.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace App\Services\ImportExportSystem; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; +use PhpOffice\PhpSpreadsheet\Cell\DataType; use App\Entity\Base\AbstractNamedDBElement; use App\Entity\Base\AbstractStructuralDBElement; use App\Helpers\FilenameSanatizer; @@ -179,7 +180,11 @@ class EntityExporter foreach ($columns as $column) { $cellCoordinate = Coordinate::stringFromColumnIndex($colIndex) . $rowIndex; - $worksheet->setCellValue($cellCoordinate, $column); + if (is_numeric(trim($column, '"'))) { // Check if the column value is numeric after trimming quotes, as values are surrounded by quotes in CSV + $worksheet->setCellValueExplicit($cellCoordinate, $column, DataType::TYPE_NUMERIC); + } else { + $worksheet->setCellValueExplicit($cellCoordinate, $column, DataType::TYPE_STRING); + } $colIndex++; } $rowIndex++; @@ -268,7 +273,7 @@ class EntityExporter //Remove percent for fallback $fallback = str_replace("%", "_", $filename); - + // Create the disposition of the file $disposition = $response->headers->makeDisposition( ResponseHeaderBag::DISPOSITION_ATTACHMENT, diff --git a/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelperTrait.php b/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelperTrait.php index 08b1c301..cbfb4ee0 100644 --- a/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelperTrait.php +++ b/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelperTrait.php @@ -233,7 +233,6 @@ trait PKImportHelperTrait $reflectionClass = new \ReflectionClass($entity); $property = $reflectionClass->getProperty('addedDate'); - $property->setAccessible(true); $property->setValue($entity, $date); } diff --git a/src/Services/InfoProviderSystem/CreateFromUrlHelper.php b/src/Services/InfoProviderSystem/CreateFromUrlHelper.php index 0291142f..d844cdaa 100644 --- a/src/Services/InfoProviderSystem/CreateFromUrlHelper.php +++ b/src/Services/InfoProviderSystem/CreateFromUrlHelper.php @@ -72,7 +72,7 @@ final readonly class CreateFromUrlHelper $provider = $this->providerRegistry->getProviderHandlingDomain($host); - if ($provider !== null && $provider->isActive() && $provider->getProviderKey() !== $callingInfoProvider->getProviderKey()) { + if ($provider !== null && $provider->isActive() && $provider->getProviderInfo()->key !== $callingInfoProvider->getProviderInfo()->key) { try { $id = $provider->getIDFromURL($url); if ($id !== null) { diff --git a/src/Services/InfoProviderSystem/DTOJsonSchemaConverter.php b/src/Services/InfoProviderSystem/DTOJsonSchemaConverter.php index a61e7465..3c2cbf64 100644 --- a/src/Services/InfoProviderSystem/DTOJsonSchemaConverter.php +++ b/src/Services/InfoProviderSystem/DTOJsonSchemaConverter.php @@ -42,7 +42,7 @@ final class DTOJsonSchemaConverter public function getJSONSchema(): array { return [ - 'name' => 'clock', + 'name' => 'part_detail', 'strict' => true, 'schema' => [ 'type' => 'object', diff --git a/src/Services/InfoProviderSystem/DTOs/BulkSearchFieldMappingDTO.php b/src/Services/InfoProviderSystem/DTOs/BulkSearchFieldMappingDTO.php index 47d8ac69..e13d3b82 100644 --- a/src/Services/InfoProviderSystem/DTOs/BulkSearchFieldMappingDTO.php +++ b/src/Services/InfoProviderSystem/DTOs/BulkSearchFieldMappingDTO.php @@ -49,7 +49,7 @@ readonly class BulkSearchFieldMappingDTO //Ensure that providers are provided as keys foreach ($providers as &$provider) { if ($provider instanceof InfoProviderInterface) { - $provider = $provider->getProviderKey(); + $provider = $provider->getProviderInfo()->key; } if (!is_string($provider)) { throw new \InvalidArgumentException('Providers must be provided as strings or InfoProviderInterface instances'); diff --git a/src/Services/InfoProviderSystem/DTOs/PartDetailDTO.php b/src/Services/InfoProviderSystem/DTOs/PartDetailDTO.php index 9700ae57..829b8805 100644 --- a/src/Services/InfoProviderSystem/DTOs/PartDetailDTO.php +++ b/src/Services/InfoProviderSystem/DTOs/PartDetailDTO.php @@ -23,11 +23,41 @@ declare(strict_types=1); namespace App\Services\InfoProviderSystem\DTOs; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\McpTool; +use ApiPlatform\Metadata\Post; +use ApiPlatform\OpenApi\Model\Operation; use App\Entity\Parts\ManufacturingStatus; +use App\Mcp\DTO\InfoProviderPartDetailsInput; +use App\State\Mcp\GetInfoProviderPartDetailsProcessor; /** * This DTO represents a part with all its details. */ +#[ApiResource( + description: 'Detailed information about a part from an external info provider (e.g. a distributor or manufacturer catalog), including datasheets, images, parameters and purchase information.', + operations: [ + new Post( + uriTemplate: '/info_providers/details', + security: 'is_granted("@info_providers.create_parts")', + input: InfoProviderPartDetailsInput::class, + validate: true, + processor: GetInfoProviderPartDetailsProcessor::class, + openapi: new Operation(summary: 'Get full detailed information about a specific part from an external info provider.'), + ), + ], + mcp: [ + 'get_info_provider_part_details' => new McpTool( + title: 'Get part details from an info provider', + description: 'Get full detailed information (datasheets, images, parameters, prices, ...) about a specific part from an external info provider, identified by the provider key and the provider-specific part ID (both returned by search_info_providers).', + annotations: ['readOnlyHint' => true, 'destructiveHint' => false, 'idempotentHint' => true, 'openWorldHint' => true], + input: InfoProviderPartDetailsInput::class, + security: 'is_granted("@info_providers.create_parts")', + validate: true, + processor: GetInfoProviderPartDetailsProcessor::class, + ), + ], +)] class PartDetailDTO extends SearchResultDTO { public function __construct( diff --git a/src/Services/InfoProviderSystem/DTOs/PriceDTO.php b/src/Services/InfoProviderSystem/DTOs/PriceDTO.php index cf1f577d..8c80a1be 100644 --- a/src/Services/InfoProviderSystem/DTOs/PriceDTO.php +++ b/src/Services/InfoProviderSystem/DTOs/PriceDTO.php @@ -24,12 +24,14 @@ declare(strict_types=1); namespace App\Services\InfoProviderSystem\DTOs; use Brick\Math\BigDecimal; +use Symfony\Component\Serializer\Attribute\Ignore; /** * This DTO represents a price for a single unit in a certain discount range */ readonly class PriceDTO { + #[Ignore] private BigDecimal $price_as_big_decimal; public function __construct( @@ -54,6 +56,7 @@ readonly class PriceDTO * Gets the price as BigDecimal * @return BigDecimal */ + #[Ignore] public function getPriceAsBigDecimal(): BigDecimal { return $this->price_as_big_decimal; diff --git a/src/Services/InfoProviderSystem/DTOs/ProviderInfoDTO.php b/src/Services/InfoProviderSystem/DTOs/ProviderInfoDTO.php new file mode 100644 index 00000000..248bf997 --- /dev/null +++ b/src/Services/InfoProviderSystem/DTOs/ProviderInfoDTO.php @@ -0,0 +1,104 @@ +. + */ + +declare(strict_types=1); + +namespace App\Services\InfoProviderSystem\DTOs; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\McpToolCollection; +use ApiPlatform\OpenApi\Model\Operation; +use App\Mcp\DTO\ListInfoProvidersInput; +use App\Services\InfoProviderSystem\Providers\ProviderCapabilities; +use App\State\Mcp\ListInfoProvidersProcessor; +use Symfony\Component\Serializer\Annotation\Groups; + +/** + * Immutable, structured description of an info provider, returned by InfoProviderInterface::getProviderInfo() + * and (via the 'info_provider:read' group) exposed as the REST GET /api/info_providers collection and the + * list_info_providers MCP tool. + * + * disabledHelp, oauthAppName and settingsClass are internal-only (used by the settings UI) and deliberately + * not tagged with the 'info_provider:read' group, so they never appear in the API/MCP output. + */ +#[ApiResource( + uriTemplate: '/info_providers', + description: 'An info provider which can be used to search for parts and retrieve part details.', + operations: [ + new GetCollection( + openapi: new Operation(summary: 'List the info providers which are currently active and can be used for searching parts.'), + security: 'is_granted("@info_providers.create_parts")', + provider: ListInfoProvidersProcessor::class, + ), + ], + normalizationContext: ['groups' => ['info_provider:read']], + paginationEnabled: false, + mcp: [ + 'list_info_providers' => new McpToolCollection( + title: 'List available info providers', + description: 'List the info providers (e.g. distributors like Digikey, Mouser, LCSC) which are currently active and can be used with search_info_providers and get_info_provider_part_details. Ask for the user\'s confirmation before using expensive providers (e.g. distributors with strict rate limits or which cost money to use).', + annotations: ['readOnlyHint' => true, 'destructiveHint' => false, 'idempotentHint' => true, 'openWorldHint' => false], + normalizationContext: ['groups' => ['info_provider:read']], + security: 'is_granted("@info_providers.create_parts")', + input: ListInfoProvidersInput::class, + processor: ListInfoProvidersProcessor::class, + ), + ], +)] +readonly class ProviderInfoDTO +{ + public function __construct( + /** @var string A unique key for this provider (e.g. "digikey"), which is saved into the database and used to identify the provider */ + #[Groups(['info_provider:read'])] + public string $key, + /** @var string The (user friendly) name of the provider (e.g. "Digikey"), will be translated */ + #[Groups(['info_provider:read'])] + public string $name, + /** @var string|null A short description of the provider (e.g. "Digikey is a ..."), will be translated */ + #[Groups(['info_provider:read'])] + public ?string $description = null, + /** @var string|null The url of the provider (e.g. "https://www.digikey.com") */ + #[Groups(['info_provider:read'])] + public ?string $url = null, + /** @var string|null A help text which is shown when the provider is disabled, explaining how to enable it */ + public ?string $disabledHelp = null, + /** @var string|null The name of the OAuth app which is used for authentication (e.g. "ip_digikey_oauth"). If this is set a connect button will be shown */ + public ?string $oauthAppName = null, + /** @var class-string|null The class name of the settings class which contains the settings for this provider (e.g. "App\Settings\InfoProviderSettings\DigikeySettings"). If this is set a link to the settings will be shown */ + public ?string $settingsClass = null, + /** + * A list of capabilities this provider supports (which kind of data it can provide). + * Not every part have to contain all of these data, but the provider should be able to provide them in general. + * Currently, this list is purely informational and not used in functional checks. + * @var ProviderCapabilities[] + */ + #[Groups(['info_provider:read'])] + public array $capabilities = [], + /** + * @var bool True if this provider is considered "expensive" (e.g. it has a strict rate limit or costs money to use), false otherwise. + * Users should be asked for confirmation before using an expensive provider, when making multiple requests (e.g. when searching for multiple parts at once), + * and more careful caching should be used for expensive providers. + */ + #[Groups(['info_provider:read'])] + public bool $expensive = false, + ) { + } +} diff --git a/src/Services/InfoProviderSystem/DTOs/SearchResultDTO.php b/src/Services/InfoProviderSystem/DTOs/SearchResultDTO.php index 085ae17e..0fc64533 100644 --- a/src/Services/InfoProviderSystem/DTOs/SearchResultDTO.php +++ b/src/Services/InfoProviderSystem/DTOs/SearchResultDTO.php @@ -23,12 +23,42 @@ declare(strict_types=1); namespace App\Services\InfoProviderSystem\DTOs; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\McpToolCollection; +use ApiPlatform\Metadata\Post; +use ApiPlatform\OpenApi\Model\Operation; use App\Entity\Parts\ManufacturingStatus; +use App\Mcp\DTO\InfoProviderSearchInput; +use App\State\Mcp\SearchInfoProvidersProcessor; /** * This DTO represents a search result for a part. * @see \App\Tests\Services\InfoProviderSystem\DTOs\SearchResultDTOTest */ +#[ApiResource( + description: 'A search result for a part from an external info provider (e.g. a distributor or manufacturer catalog).', + operations: [ + new Post( + uriTemplate: '/info_providers/search', + security: 'is_granted("@info_providers.create_parts")', + input: InfoProviderSearchInput::class, + validate: true, + processor: SearchInfoProvidersProcessor::class, + openapi: new Operation(summary: 'Search external info providers (e.g. distributors like Digikey, Mouser, LCSC) for parts matching a keyword.'), + ), + ], + mcp: [ + 'search_info_providers' => new McpToolCollection( + title: 'Search external info providers', + description: 'Search external info providers (e.g. distributors like Digikey, Mouser, LCSC) for parts matching a keyword. Returns a list of search results, which can be passed to get_info_provider_part_details to retrieve full details for a specific result.', + annotations: ['readOnlyHint' => true, 'destructiveHint' => false, 'idempotentHint' => true, 'openWorldHint' => true], + input: InfoProviderSearchInput::class, + security: 'is_granted("@info_providers.create_parts")', + validate: true, + processor: SearchInfoProvidersProcessor::class, + ), + ], +)] class SearchResultDTO { /** @var string|null An URL to a preview image */ diff --git a/src/Services/InfoProviderSystem/PartInfoRetriever.php b/src/Services/InfoProviderSystem/PartInfoRetriever.php index f5ff144d..f99970af 100644 --- a/src/Services/InfoProviderSystem/PartInfoRetriever.php +++ b/src/Services/InfoProviderSystem/PartInfoRetriever.php @@ -103,7 +103,7 @@ final class PartInfoRetriever //Generate a hash for the options, to ensure that different options result in different cache entries $options_hash = hash('xxh3', json_encode($options_without_cache, JSON_THROW_ON_ERROR)); - $cache_key = "search_{$provider->getProviderKey()}_{$escaped_keyword}_{$options_hash}"; + $cache_key = "search_{$provider->getProviderInfo()->key}_{$escaped_keyword}_{$options_hash}"; //If no_cache is set, bypass the cache and get fresh results from the provider if ($no_cache) { @@ -126,6 +126,7 @@ final class PartInfoRetriever * @param array $options An associative array of options which can be used to modify the search behavior. The supported options depend on the provider and should be documented in the provider's documentation. * @return PartDetailDTO * @throws InfoProviderNotActiveException if the the given providers is not active + * @throws \InvalidArgumentException if the given provider key does not match any registered provider */ public function getDetails(string $provider_key, string $part_id, array $options = []): PartDetailDTO { diff --git a/src/Services/InfoProviderSystem/ProviderRegistry.php b/src/Services/InfoProviderSystem/ProviderRegistry.php index 18b8a37a..1b0b9785 100644 --- a/src/Services/InfoProviderSystem/ProviderRegistry.php +++ b/src/Services/InfoProviderSystem/ProviderRegistry.php @@ -72,7 +72,7 @@ final class ProviderRegistry private function initStructures(): void { foreach ($this->providers as $provider) { - $key = $provider->getProviderKey(); + $key = $provider->getProviderInfo()->key; if (isset($this->providers_by_name[$key])) { throw new \LogicException("Provider with key $key already registered"); diff --git a/src/Services/InfoProviderSystem/Providers/AIWebProvider.php b/src/Services/InfoProviderSystem/Providers/AIWebProvider.php index 6539e69b..feb94384 100644 --- a/src/Services/InfoProviderSystem/Providers/AIWebProvider.php +++ b/src/Services/InfoProviderSystem/Providers/AIWebProvider.php @@ -31,6 +31,7 @@ use App\Services\InfoProviderSystem\SubmittedPageStorage; use App\Services\InfoProviderSystem\CreateFromUrlHelper; use App\Services\InfoProviderSystem\DTOJsonSchemaConverter; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; +use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO; use App\Settings\InfoProviderSystem\AIExtractorSettings; use Jkphl\Micrometa; use League\HTMLToMarkdown\HtmlConverter; @@ -50,6 +51,8 @@ final class AIWebProvider implements InfoProviderInterface { use FixAndValidateUrlTrait; + public const PROVIDER_KEY = 'ai_web'; + private const DISTRIBUTOR_NAME = 'Website'; private readonly HttpClientInterface $httpClient; @@ -71,20 +74,23 @@ final class AIWebProvider implements InfoProviderInterface ); } - public function getProviderInfo(): array + public function getProviderInfo(): ProviderInfoDTO { - return [ - 'name' => 'AI Web Extractor', - 'description' => 'Extract part info from any URL using LLM', - //'url' => 'https://openrouter.ai', - 'disabled_help' => 'Configure AI settings', - 'settings_class' => AIExtractorSettings::class, - ]; - } - - public function getProviderKey(): string - { - return 'ai_web'; + return new ProviderInfoDTO( + key: self::PROVIDER_KEY, + name: 'AI Web Extractor', + description: 'Extract part info from any URL using LLM', + disabledHelp: 'Configure AI settings', + settingsClass: AIExtractorSettings::class, + capabilities: [ + ProviderCapabilities::BASIC, + ProviderCapabilities::PICTURE, + ProviderCapabilities::DATASHEET, + ProviderCapabilities::PRICE, + ProviderCapabilities::PARAMETERS, + ], + expensive: true, + ); } public function isActive(): bool @@ -166,7 +172,7 @@ final class AIWebProvider implements InfoProviderInterface $llmResponse = $this->callLLM($markdown, $url, $structuredData); // Build and return PartDetailDTO - $result = $this->jsonSchemaConverter->jsonToDTO($llmResponse, $this->getProviderKey(), $url, $url, self::DISTRIBUTOR_NAME); + $result = $this->jsonSchemaConverter->jsonToDTO($llmResponse, self::PROVIDER_KEY, $url, $url, self::DISTRIBUTOR_NAME); // Cache the result for future use, to improve performance and reduce costs. $cacheItem->set($result); @@ -256,17 +262,6 @@ final class AIWebProvider implements InfoProviderInterface return $converter->convert($htmlToConvert); } - public function getCapabilities(): array - { - return [ - ProviderCapabilities::BASIC, - ProviderCapabilities::PICTURE, - ProviderCapabilities::DATASHEET, - ProviderCapabilities::PRICE, - ProviderCapabilities::PARAMETERS, - ]; - } - private function callLLM(string $htmlContent, string $url, ?string $structuredData = null): array { $input = new MessageBag( @@ -282,6 +277,10 @@ final class AIWebProvider implements InfoProviderInterface try { $aiPlatform = $this->AIPlatformRegistry->getPlatform($this->settings->platform ?? throw new \RuntimeException('No AI platform selected') ); + // AI inference can take much longer than PHP's default max_execution_time (typically 30s). + // The HTTP client timeout already enforces the configured limit; disable PHP's constraint here. + set_time_limit(0); + //'openai/gpt-5-mini' $result = $aiPlatform->invoke($this->settings->model ?? throw new \RuntimeException('No model selected'), $input, [ 'response_format' => [ diff --git a/src/Services/InfoProviderSystem/Providers/BuerklinProvider.php b/src/Services/InfoProviderSystem/Providers/BuerklinProvider.php index ca6e26e1..9197df48 100644 --- a/src/Services/InfoProviderSystem/Providers/BuerklinProvider.php +++ b/src/Services/InfoProviderSystem/Providers/BuerklinProvider.php @@ -28,6 +28,7 @@ use App\Services\InfoProviderSystem\DTOs\FileDTO; use App\Services\InfoProviderSystem\DTOs\ParameterDTO; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\InfoProviderSystem\DTOs\PriceDTO; +use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO; use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; use App\Settings\InfoProviderSystem\BuerklinSettings; @@ -40,6 +41,7 @@ class BuerklinProvider implements BatchInfoProviderInterface, URLHandlerInfoProv private const ENDPOINT_URL = 'https://www.buerklin.com/buerklinws/v2/buerklin'; public const DISTRIBUTOR_NAME = 'Buerklin'; + public const PROVIDER_KEY = 'buerklin'; private const CACHE_TTL = 600; /** @@ -175,20 +177,24 @@ class BuerklinProvider implements BatchInfoProviderInterface, URLHandlerInfoProv } - public function getProviderInfo(): array + public function getProviderInfo(): ProviderInfoDTO { - return [ - 'name' => 'Buerklin', - 'description' => 'This provider uses the Buerklin API to search for parts.', - 'url' => 'https://www.buerklin.com/', - 'disabled_help' => 'Configure the API Client ID, Secret, Username and Password provided by Buerklin in the provider settings to enable.', - 'settings_class' => BuerklinSettings::class - ]; - } - - public function getProviderKey(): string - { - return 'buerklin'; + return new ProviderInfoDTO( + key: self::PROVIDER_KEY, + name: 'Buerklin', + description: 'This provider uses the Buerklin API to search for parts.', + url: 'https://www.buerklin.com/', + disabledHelp: 'Configure the API Client ID, Secret, Username and Password provided by Buerklin in the provider settings to enable.', + settingsClass: BuerklinSettings::class, + capabilities: [ + ProviderCapabilities::BASIC, + ProviderCapabilities::PICTURE, + //ProviderCapabilities::DATASHEET, // currently not implemented + ProviderCapabilities::PRICE, + ProviderCapabilities::FOOTPRINT, + ProviderCapabilities::PARAMETERS + ], + ); } // This provider is considered active if settings are present @@ -283,7 +289,7 @@ class BuerklinProvider implements BatchInfoProviderInterface, URLHandlerInfoProv } return new PartDetailDTO( - provider_key: $this->getProviderKey(), + provider_key: self::PROVIDER_KEY, provider_id: (string) ($product['code'] ?? $code), name: (string) ($product['manufacturerProductId'] ?? $code), @@ -509,17 +515,6 @@ class BuerklinProvider implements BatchInfoProviderInterface, URLHandlerInfoProv return $this->getPartDetail($response); } - public function getCapabilities(): array - { - return [ - ProviderCapabilities::BASIC, - ProviderCapabilities::PICTURE, - //ProviderCapabilities::DATASHEET, // currently not implemented - ProviderCapabilities::PRICE, - ProviderCapabilities::FOOTPRINT, - ]; - } - private function complianceToParameters(array $product, ?string $group = 'Compliance'): array { $params = []; diff --git a/src/Services/InfoProviderSystem/Providers/CanopyProvider.php b/src/Services/InfoProviderSystem/Providers/CanopyProvider.php index aee30d6b..3390bd98 100644 --- a/src/Services/InfoProviderSystem/Providers/CanopyProvider.php +++ b/src/Services/InfoProviderSystem/Providers/CanopyProvider.php @@ -26,6 +26,7 @@ namespace App\Services\InfoProviderSystem\Providers; use App\Services\InfoProviderSystem\DTOs\FileDTO; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\InfoProviderSystem\DTOs\PriceDTO; +use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO; use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; use App\Settings\InfoProviderSystem\BuerklinSettings; @@ -39,6 +40,7 @@ use Symfony\Contracts\HttpClient\HttpClientInterface; */ class CanopyProvider implements InfoProviderInterface { + public const PROVIDER_KEY = 'canopy'; public const BASE_URL = "https://rest.canopyapi.co/api"; public const SEARCH_API_URL = self::BASE_URL . "/amazon/search"; @@ -52,20 +54,22 @@ class CanopyProvider implements InfoProviderInterface } - public function getProviderInfo(): array + public function getProviderInfo(): ProviderInfoDTO { - return [ - 'name' => 'Amazon (Canopy)', - 'description' => 'Retrieves part infos from Amazon using the Canopy API', - 'url' => 'https://canopyapi.co', - 'disabled_help' => 'Set Canopy API key in the provider configuration to enable this provider', - 'settings_class' => CanopySettings::class - ]; - } - - public function getProviderKey(): string - { - return 'canopy'; + return new ProviderInfoDTO( + key: self::PROVIDER_KEY, + name: 'Amazon (Canopy)', + description: 'Retrieves part infos from Amazon using the Canopy API', + url: 'https://canopyapi.co', + disabledHelp: 'Set Canopy API key in the provider configuration to enable this provider', + settingsClass: CanopySettings::class, + capabilities: [ + ProviderCapabilities::BASIC, + ProviderCapabilities::PICTURE, + ProviderCapabilities::PRICE, + ], + expensive: true, + ); } public function isActive(): bool @@ -131,7 +135,7 @@ class CanopyProvider implements InfoProviderInterface $dto = new PartDetailDTO( - provider_key: $this->getProviderKey(), + provider_key: self::PROVIDER_KEY, provider_id: $result['asin'], name: $result["title"], description: "", @@ -209,7 +213,7 @@ class CanopyProvider implements InfoProviderInterface } return new PartDetailDTO( - provider_key: $this->getProviderKey(), + provider_key: self::PROVIDER_KEY, provider_id: $product['asin'], name: $product['title'], description: '', @@ -222,12 +226,4 @@ class CanopyProvider implements InfoProviderInterface ); } - public function getCapabilities(): array - { - return [ - ProviderCapabilities::BASIC, - ProviderCapabilities::PICTURE, - ProviderCapabilities::PRICE, - ]; - } } diff --git a/src/Services/InfoProviderSystem/Providers/ConradProvider.php b/src/Services/InfoProviderSystem/Providers/ConradProvider.php index 2e6708be..0fb1525f 100644 --- a/src/Services/InfoProviderSystem/Providers/ConradProvider.php +++ b/src/Services/InfoProviderSystem/Providers/ConradProvider.php @@ -27,6 +27,7 @@ use App\Services\InfoProviderSystem\DTOs\FileDTO; use App\Services\InfoProviderSystem\DTOs\ParameterDTO; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\InfoProviderSystem\DTOs\PriceDTO; +use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO; use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; use App\Settings\InfoProviderSystem\ConradSettings; @@ -38,6 +39,7 @@ readonly class ConradProvider implements InfoProviderInterface, URLHandlerInfoPr private const SEARCH_ENDPOINT = '/search/1/v3/facetSearch'; public const DISTRIBUTOR_NAME = 'Conrad'; + public const PROVIDER_KEY = 'conrad'; private HttpClientInterface $httpClient; @@ -51,20 +53,25 @@ readonly class ConradProvider implements InfoProviderInterface, URLHandlerInfoPr ]); } - public function getProviderInfo(): array + public function getProviderInfo(): ProviderInfoDTO { - return [ - 'name' => 'Conrad', - 'description' => 'Retrieves part information from conrad.de', - 'url' => 'https://www.conrad.de/', - 'disabled_help' => 'Set API key in settings', - 'settings_class' => ConradSettings::class, - ]; - } - - public function getProviderKey(): string - { - return 'conrad'; + return new ProviderInfoDTO( + key: self::PROVIDER_KEY, + name: 'Conrad', + description: 'Retrieves part information from conrad.de', + url: 'https://www.conrad.de/', + disabledHelp: 'Set API key in settings', + settingsClass: ConradSettings::class, + capabilities: [ + ProviderCapabilities::BASIC, + ProviderCapabilities::PICTURE, + ProviderCapabilities::DATASHEET, + ProviderCapabilities::PRICE, + ProviderCapabilities::FOOTPRINT, + ProviderCapabilities::GTIN, + ProviderCapabilities::PARAMETERS + ], + ); } public function isActive(): bool @@ -111,7 +118,7 @@ readonly class ConradProvider implements InfoProviderInterface, URLHandlerInfoPr foreach($results['hits'] as $result) { $out[] = new SearchResultDTO( - provider_key: $this->getProviderKey(), + provider_key: self::PROVIDER_KEY, provider_id: $result['productId'], name: $result['manufacturerId'] ?? $result['productId'], description: $result['title'] ?? '', @@ -293,7 +300,7 @@ readonly class ConradProvider implements InfoProviderInterface, URLHandlerInfoPr $data = $response->toArray(); return new PartDetailDTO( - provider_key: $this->getProviderKey(), + provider_key: self::PROVIDER_KEY, provider_id: $data['shortProductNumber'], name: $data['productFullInformation']['manufacturer']['name'] ?? $data['productFullInformation']['manufacturer']['id'] ?? $data['shortProductNumber'], description: $data['productShortInformation']['title'] ?? '', @@ -311,18 +318,6 @@ readonly class ConradProvider implements InfoProviderInterface, URLHandlerInfoPr ); } - public function getCapabilities(): array - { - return [ - ProviderCapabilities::BASIC, - ProviderCapabilities::PICTURE, - ProviderCapabilities::DATASHEET, - ProviderCapabilities::PRICE, - ProviderCapabilities::FOOTPRINT, - ProviderCapabilities::GTIN, - ]; - } - public function getHandledDomains(): array { $domains = []; diff --git a/src/Services/InfoProviderSystem/Providers/DigikeyProvider.php b/src/Services/InfoProviderSystem/Providers/DigikeyProvider.php index e7a62aa4..fedda225 100644 --- a/src/Services/InfoProviderSystem/Providers/DigikeyProvider.php +++ b/src/Services/InfoProviderSystem/Providers/DigikeyProvider.php @@ -29,6 +29,7 @@ use App\Services\InfoProviderSystem\DTOs\FileDTO; use App\Services\InfoProviderSystem\DTOs\ParameterDTO; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\InfoProviderSystem\DTOs\PriceDTO; +use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO; use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; use App\Services\OAuth\OAuthTokenManager; @@ -37,6 +38,7 @@ use Symfony\Contracts\HttpClient\HttpClientInterface; class DigikeyProvider implements InfoProviderInterface { + public const PROVIDER_KEY = 'digikey'; private const OAUTH_APP_NAME = 'ip_digikey_oauth'; @@ -72,32 +74,25 @@ class DigikeyProvider implements InfoProviderInterface ]); } - public function getProviderInfo(): array + public function getProviderInfo(): ProviderInfoDTO { - return [ - 'name' => 'DigiKey', - 'description' => 'This provider uses the DigiKey API to search for parts.', - 'url' => 'https://www.digikey.com/', - 'oauth_app_name' => self::OAUTH_APP_NAME, - 'disabled_help' => 'Set the Client ID and Secret in provider settings and connect OAuth to enable.', - 'settings_class' => DigikeySettings::class, - ]; - } - - public function getCapabilities(): array - { - return [ - ProviderCapabilities::BASIC, - ProviderCapabilities::FOOTPRINT, - ProviderCapabilities::PICTURE, - ProviderCapabilities::DATASHEET, - ProviderCapabilities::PRICE, - ]; - } - - public function getProviderKey(): string - { - return 'digikey'; + return new ProviderInfoDTO( + key: self::PROVIDER_KEY, + name: 'DigiKey', + description: 'This provider uses the DigiKey API to search for parts.', + url: 'https://www.digikey.com/', + disabledHelp: 'Set the Client ID and Secret in provider settings and connect OAuth to enable.', + oauthAppName: self::OAUTH_APP_NAME, + settingsClass: DigikeySettings::class, + capabilities: [ + ProviderCapabilities::BASIC, + ProviderCapabilities::FOOTPRINT, + ProviderCapabilities::PICTURE, + ProviderCapabilities::DATASHEET, + ProviderCapabilities::PRICE, + ProviderCapabilities::PARAMETERS + ], + ); } public function isActive(): bool @@ -128,7 +123,7 @@ class DigikeyProvider implements InfoProviderInterface } catch (\InvalidArgumentException $exception) { //Check if the exception was caused by an invalid or expired token if (str_contains($exception->getMessage(), 'access_token')) { - throw OAuthReconnectRequiredException::forProvider($this->getProviderKey()); + throw OAuthReconnectRequiredException::forProvider(self::PROVIDER_KEY); } throw $exception; @@ -141,7 +136,7 @@ class DigikeyProvider implements InfoProviderInterface foreach ($products as $product) { foreach ($product['ProductVariations'] as $variation) { $result[] = new SearchResultDTO( - provider_key: $this->getProviderKey(), + provider_key: self::PROVIDER_KEY, provider_id: $variation['DigiKeyProductNumber'], name: $product['ManufacturerProductNumber'], description: $product['Description']['DetailedDescription'] ?? $product['Description']['ProductDescription'], @@ -168,7 +163,7 @@ class DigikeyProvider implements InfoProviderInterface } catch (\InvalidArgumentException $exception) { //Check if the exception was caused by an invalid or expired token if (str_contains($exception->getMessage(), 'access_token')) { - throw OAuthReconnectRequiredException::forProvider($this->getProviderKey()); + throw OAuthReconnectRequiredException::forProvider(self::PROVIDER_KEY); } throw $exception; @@ -191,7 +186,7 @@ class DigikeyProvider implements InfoProviderInterface } return new PartDetailDTO( - provider_key: $this->getProviderKey(), + provider_key: self::PROVIDER_KEY, provider_id: $id, name: $product['ManufacturerProductNumber'], description: $product['Description']['DetailedDescription'] ?? $product['Description']['ProductDescription'], diff --git a/src/Services/InfoProviderSystem/Providers/Element14Provider.php b/src/Services/InfoProviderSystem/Providers/Element14Provider.php index 1d9e092c..ba8a7370 100644 --- a/src/Services/InfoProviderSystem/Providers/Element14Provider.php +++ b/src/Services/InfoProviderSystem/Providers/Element14Provider.php @@ -28,6 +28,7 @@ use App\Services\InfoProviderSystem\DTOs\FileDTO; use App\Services\InfoProviderSystem\DTOs\ParameterDTO; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\InfoProviderSystem\DTOs\PriceDTO; +use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO; use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; use App\Settings\InfoProviderSystem\Element14Settings; use Composer\CaBundle\CaBundle; @@ -35,6 +36,7 @@ use Symfony\Contracts\HttpClient\HttpClientInterface; class Element14Provider implements InfoProviderInterface, URLHandlerInfoProviderInterface { + public const PROVIDER_KEY = 'element14'; private const ENDPOINT_URL = 'https://api.element14.com/catalog/products'; private const API_VERSION_NUMBER = '1.4'; @@ -60,20 +62,23 @@ class Element14Provider implements InfoProviderInterface, URLHandlerInfoProvider ]); } - public function getProviderInfo(): array + public function getProviderInfo(): ProviderInfoDTO { - return [ - 'name' => 'Farnell element14', - 'description' => 'This provider uses the Farnell element14 API to search for parts.', - 'url' => 'https://www.element14.com/', - 'disabled_help' => 'Configure the API key in the provider settings to enable.', - 'settings_class' => Element14Settings::class, - ]; - } - - public function getProviderKey(): string - { - return 'element14'; + return new ProviderInfoDTO( + key: self::PROVIDER_KEY, + name: 'Farnell element14', + description: 'This provider uses the Farnell element14 API to search for parts.', + url: 'https://www.element14.com/', + disabledHelp: 'Configure the API key in the provider settings to enable.', + settingsClass: Element14Settings::class, + capabilities: [ + ProviderCapabilities::BASIC, + ProviderCapabilities::PICTURE, + ProviderCapabilities::DATASHEET, + ProviderCapabilities::PRICE, + ProviderCapabilities::PARAMETERS, + ], + ); } public function isActive(): bool @@ -113,7 +118,7 @@ class Element14Provider implements InfoProviderInterface, URLHandlerInfoProvider foreach ($products as $product) { $result[] = new PartDetailDTO( - provider_key: $this->getProviderKey(), provider_id: $product['sku'], + provider_key: self::PROVIDER_KEY, provider_id: $product['sku'], name: $product['translatedManufacturerPartNumber'], description: $this->displayNameToDescription($product['displayName'], $product['translatedManufacturerPartNumber']), manufacturer: $product['vendorName'] ?? $product['brandName'] ?? null, @@ -301,15 +306,6 @@ class Element14Provider implements InfoProviderInterface, URLHandlerInfoProvider return $tmp[0]; } - public function getCapabilities(): array - { - return [ - ProviderCapabilities::BASIC, - ProviderCapabilities::PICTURE, - ProviderCapabilities::DATASHEET, - ]; - } - public function getHandledDomains(): array { return ['element14.com', 'farnell.com', 'newark.com']; diff --git a/src/Services/InfoProviderSystem/Providers/EmptyProvider.php b/src/Services/InfoProviderSystem/Providers/EmptyProvider.php index 915a118c..eea14ed8 100644 --- a/src/Services/InfoProviderSystem/Providers/EmptyProvider.php +++ b/src/Services/InfoProviderSystem/Providers/EmptyProvider.php @@ -25,6 +25,7 @@ namespace App\Services\InfoProviderSystem\Providers; use App\Services\InfoProviderSystem\DTOs\FileDTO; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; +use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO; use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; use Symfony\Component\DependencyInjection\Attribute\When; @@ -34,19 +35,20 @@ use Symfony\Component\DependencyInjection\Attribute\When; #[When(env: 'test')] class EmptyProvider implements InfoProviderInterface { - public function getProviderInfo(): array - { - return [ - 'name' => 'Empty Provider', - 'description' => 'This is a test provider', - //'url' => 'https://example.com', - 'disabled_help' => 'This provider is disabled for testing purposes' - ]; - } + public const PROVIDER_KEY = 'empty'; - public function getProviderKey(): string + public function getProviderInfo(): ProviderInfoDTO { - return 'empty'; + return new ProviderInfoDTO( + key: self::PROVIDER_KEY, + name: 'Empty Provider', + description: 'This is a test provider', + disabledHelp: 'This provider is disabled for testing purposes', + capabilities: [ + ProviderCapabilities::BASIC, + ProviderCapabilities::FOOTPRINT, + ], + ); } public function isActive(): bool @@ -61,14 +63,6 @@ class EmptyProvider implements InfoProviderInterface ]; } - public function getCapabilities(): array - { - return [ - ProviderCapabilities::BASIC, - ProviderCapabilities::FOOTPRINT, - ]; - } - public function getDetails(string $id, array $options = []): PartDetailDTO { throw new \RuntimeException('No part details available'); diff --git a/src/Services/InfoProviderSystem/Providers/GenericWebProvider.php b/src/Services/InfoProviderSystem/Providers/GenericWebProvider.php index 45777f9e..420049b3 100644 --- a/src/Services/InfoProviderSystem/Providers/GenericWebProvider.php +++ b/src/Services/InfoProviderSystem/Providers/GenericWebProvider.php @@ -30,6 +30,7 @@ use App\Services\InfoProviderSystem\CreateFromUrlHelper; use App\Services\InfoProviderSystem\DTOs\ParameterDTO; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\InfoProviderSystem\DTOs\PriceDTO; +use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO; use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; use App\Services\InfoProviderSystem\PartInfoRetriever; @@ -53,6 +54,7 @@ class GenericWebProvider implements InfoProviderInterface use FixAndValidateUrlTrait; public const DISTRIBUTOR_NAME = 'Website'; + public const PROVIDER_KEY = 'generic_web'; private readonly HttpClientInterface $httpClient; @@ -69,20 +71,21 @@ class GenericWebProvider implements InfoProviderInterface ); } - public function getProviderInfo(): array + public function getProviderInfo(): ProviderInfoDTO { - return [ - 'name' => 'Generic Web URL', - 'description' => 'Tries to extract a part from a given product webpage URL using common metadata standards like JSON-LD and OpenGraph.', - //'url' => 'https://example.com', - 'disabled_help' => 'Enable in settings to use this provider', - 'settings_class' => GenericWebProviderSettings::class, - ]; - } - - public function getProviderKey(): string - { - return 'generic_web'; + return new ProviderInfoDTO( + key: self::PROVIDER_KEY, + name: 'Generic Web URL', + description: 'Tries to extract a part from a given product webpage URL using common metadata standards like JSON-LD and OpenGraph.', + disabledHelp: 'Enable in settings to use this provider', + settingsClass: GenericWebProviderSettings::class, + capabilities: [ + ProviderCapabilities::BASIC, + ProviderCapabilities::PICTURE, + ProviderCapabilities::PRICE, + ProviderCapabilities::GTIN, + ], + ); } public function isActive(): bool @@ -227,7 +230,7 @@ class GenericWebProvider implements InfoProviderInterface } return new PartDetailDTO( - provider_key: $this->getProviderKey(), + provider_key: self::PROVIDER_KEY, provider_id: $url, name: $product->name?->toString() ?? $product->alternateName?->toString() ?? $product->mpn?->toString() ?? 'Unknown Name', description: $this->getMetaContent($dom, 'og:description') ?? $this->getMetaContent($dom, 'description') ?? '', @@ -376,7 +379,7 @@ class GenericWebProvider implements InfoProviderInterface )]; return new PartDetailDTO( - provider_key: $this->getProviderKey(), + provider_key: self::PROVIDER_KEY, provider_id: $canonicalURL, name: $this->getMetaContent($dom, 'og:title') ?? $pageTitle, description: $this->getMetaContent($dom, 'og:description') ?? $this->getMetaContent($dom, 'description') ?? '', @@ -387,13 +390,4 @@ class GenericWebProvider implements InfoProviderInterface ); } - public function getCapabilities(): array - { - return [ - ProviderCapabilities::BASIC, - ProviderCapabilities::PICTURE, - ProviderCapabilities::PRICE, - ProviderCapabilities::GTIN, - ]; - } } diff --git a/src/Services/InfoProviderSystem/Providers/InfoProviderInterface.php b/src/Services/InfoProviderSystem/Providers/InfoProviderInterface.php index d3895795..cdc8086c 100644 --- a/src/Services/InfoProviderSystem/Providers/InfoProviderInterface.php +++ b/src/Services/InfoProviderSystem/Providers/InfoProviderInterface.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace App\Services\InfoProviderSystem\Providers; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; +use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO; use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; interface InfoProviderInterface @@ -34,26 +35,8 @@ interface InfoProviderInterface /** * Get information about this provider - * - * @return array An associative array with the following keys (? means optional): - * - name: The (user friendly) name of the provider (e.g. "Digikey"), will be translated - * - description?: A short description of the provider (e.g. "Digikey is a ..."), will be translated - * - logo?: The logo of the provider (e.g. "digikey.png") - * - url?: The url of the provider (e.g. "https://www.digikey.com") - * - disabled_help?: A help text which is shown when the provider is disabled, explaining how to enable it - * - oauth_app_name?: The name of the OAuth app which is used for authentication (e.g. "ip_digikey_oauth"). If this is set a connect button will be shown - * - settings_class?: The class name of the settings class which contains the settings for this provider (e.g. "App\Settings\InfoProviderSettings\DigikeySettings"). If this is set a link to the settings will be shown - * - * @phpstan-return array{ name: string, description?: string, logo?: string, url?: string, disabled_help?: string, oauth_app_name?: string, settings_class?: class-string } */ - public function getProviderInfo(): array; - - /** - * Returns a unique key for this provider, which will be saved into the database - * and used to identify the provider - * @return string A unique key for this provider (e.g. "digikey") - */ - public function getProviderKey(): string; + public function getProviderInfo(): ProviderInfoDTO; /** * Checks if this provider is enabled or not (meaning that it can be used for searching) @@ -76,12 +59,4 @@ interface InfoProviderInterface * @return PartDetailDTO */ public function getDetails(string $id, array $options = []): PartDetailDTO; - - /** - * A list of capabilities this provider supports (which kind of data it can provide). - * Not every part have to contain all of these data, but the provider should be able to provide them in general. - * Currently, this list is purely informational and not used in functional checks. - * @return ProviderCapabilities[] - */ - public function getCapabilities(): array; } diff --git a/src/Services/InfoProviderSystem/Providers/LCSCProvider.php b/src/Services/InfoProviderSystem/Providers/LCSCProvider.php index 5f251b43..de1906cc 100755 --- a/src/Services/InfoProviderSystem/Providers/LCSCProvider.php +++ b/src/Services/InfoProviderSystem/Providers/LCSCProvider.php @@ -28,6 +28,7 @@ use App\Services\InfoProviderSystem\DTOs\FileDTO; use App\Services\InfoProviderSystem\DTOs\ParameterDTO; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\InfoProviderSystem\DTOs\PriceDTO; +use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO; use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; use App\Settings\InfoProviderSystem\LCSCSettings; use Symfony\Component\HttpFoundation\Cookie; @@ -39,26 +40,31 @@ class LCSCProvider implements BatchInfoProviderInterface, URLHandlerInfoProvider private const ENDPOINT_URL = 'https://wmsc.lcsc.com/ftps/wm'; public const DISTRIBUTOR_NAME = 'LCSC'; + public const PROVIDER_KEY = 'lcsc'; public function __construct(private readonly HttpClientInterface $lcscClient, private readonly LCSCSettings $settings) { } - public function getProviderInfo(): array + public function getProviderInfo(): ProviderInfoDTO { - return [ - 'name' => 'LCSC', - 'description' => 'This provider uses the (unofficial) LCSC API to search for parts.', - 'url' => 'https://www.lcsc.com/', - 'disabled_help' => 'Enable this provider in the provider settings.', - 'settings_class' => LCSCSettings::class, - ]; - } - - public function getProviderKey(): string - { - return 'lcsc'; + return new ProviderInfoDTO( + key: self::PROVIDER_KEY, + name: 'LCSC', + description: 'This provider uses the (unofficial) LCSC API to search for parts.', + url: 'https://www.lcsc.com/', + disabledHelp: 'Enable this provider in the provider settings.', + settingsClass: LCSCSettings::class, + capabilities: [ + ProviderCapabilities::BASIC, + ProviderCapabilities::PICTURE, + ProviderCapabilities::DATASHEET, + ProviderCapabilities::PRICE, + ProviderCapabilities::FOOTPRINT, + ProviderCapabilities::PARAMETERS + ], + ); } // This provider is always active @@ -136,7 +142,9 @@ class LCSCProvider implements BatchInfoProviderInterface, URLHandlerInfoProvider } } - $response = $this->lcscClient->request('POST', self::ENDPOINT_URL . "/search/v2/global", [ + //First we try the search v3 endpoint, which seems to give better results including pictures, but it only works + //on quite exact mpn matches + $response = $this->lcscClient->request('POST', self::ENDPOINT_URL . "/search/v3/global", [ 'headers' => [ 'Cookie' => new Cookie('currencyCode', $this->settings->currency) ], @@ -147,20 +155,29 @@ class LCSCProvider implements BatchInfoProviderInterface, URLHandlerInfoProvider $arr = $response->toArray(); - // Get products list - $products = $arr['result']['productSearchResultVO']['productList'] ?? []; - // Get product tip - $tipProductCode = $arr['result']['tipProductDetailUrlVO']['productCode'] ?? null; + //If we get exact matches, use them + if (!empty($arr['result']['exactMatchResult'])) { + $products = $arr['result']['exactMatchResult']; + } else { //Otherwise fallback onto the third search endpoint, which has a worse data quality but is more likely to return results for vague search terms + $response = $this->lcscClient->request('POST', self::ENDPOINT_URL."/search/third", [ + 'headers' => [ + 'Cookie' => new Cookie('currencyCode', $this->settings->currency) + ], + 'json' => [ + 'keyword' => $term, + 'currentPage' => 1, + 'pageSize' => 10, + ], + ]); + + $arr = $response->toArray(); + + // Get products list + $products = $arr['result']['productList'] ?? []; + } $result = []; - // LCSC does not display LCSC codes in the search, instead taking you directly to the - // detailed product listing. It does so utilizing a product tip field. - // If product tip exists and there are no products in the product list try a detail query - if (count($products) === 0 && $tipProductCode !== null) { - $result[] = $this->queryDetail($tipProductCode, $lightweight); - } - foreach ($products as $product) { $result[] = $this->getPartDetail($product, $lightweight); } @@ -216,10 +233,10 @@ class LCSCProvider implements BatchInfoProviderInterface, URLHandlerInfoProvider } return new PartDetailDTO( - provider_key: $this->getProviderKey(), + provider_key: self::PROVIDER_KEY, provider_id: $product['productCode'], name: $product['productModel'], - description: $this->sanitizeField($product['productIntroEn']), + description: $this->sanitizeField($product['productIntroEn']) ?? '', category: $this->sanitizeField($category ?? null), manufacturer: $this->sanitizeField($product['brandNameEn'] ?? null), mpn: $this->sanitizeField($product['productModel'] ?? null), @@ -383,7 +400,7 @@ class LCSCProvider implements BatchInfoProviderInterface, URLHandlerInfoProvider ]); } else { // Search API call for other terms - $responses[$keyword] = $this->lcscClient->request('POST', self::ENDPOINT_URL . "/search/v2/global", [ + $responses[$keyword] = $this->lcscClient->request('POST', self::ENDPOINT_URL . "/search/v3/global", [ 'headers' => [ 'Cookie' => new Cookie('currencyCode', $this->settings->currency) ], @@ -444,17 +461,6 @@ class LCSCProvider implements BatchInfoProviderInterface, URLHandlerInfoProvider return $tmp[0]; } - public function getCapabilities(): array - { - return [ - ProviderCapabilities::BASIC, - ProviderCapabilities::PICTURE, - ProviderCapabilities::DATASHEET, - ProviderCapabilities::PRICE, - ProviderCapabilities::FOOTPRINT, - ]; - } - public function getHandledDomains(): array { return ['lcsc.com']; diff --git a/src/Services/InfoProviderSystem/Providers/MouserProvider.php b/src/Services/InfoProviderSystem/Providers/MouserProvider.php index 49ca2d50..c1b91faa 100644 --- a/src/Services/InfoProviderSystem/Providers/MouserProvider.php +++ b/src/Services/InfoProviderSystem/Providers/MouserProvider.php @@ -36,6 +36,7 @@ use App\Entity\Parts\ManufacturingStatus; use App\Services\InfoProviderSystem\DTOs\FileDTO; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\InfoProviderSystem\DTOs\PriceDTO; +use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO; use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; use App\Settings\InfoProviderSystem\MouserSettings; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -48,6 +49,7 @@ class MouserProvider implements InfoProviderInterface private const ENDPOINT_URL = 'https://api.mouser.com/api/v2/search'; public const DISTRIBUTOR_NAME = 'Mouser'; + public const PROVIDER_KEY = 'mouser'; public function __construct( private readonly HttpClientInterface $mouserClient, @@ -55,20 +57,22 @@ class MouserProvider implements InfoProviderInterface ) { } - public function getProviderInfo(): array + public function getProviderInfo(): ProviderInfoDTO { - return [ - 'name' => 'Mouser', - 'description' => 'This provider uses the Mouser API to search for parts.', - 'url' => 'https://www.mouser.com/', - 'disabled_help' => 'Configure the API key in the provider settings to enable.', - 'settings_class' => MouserSettings::class - ]; - } - - public function getProviderKey(): string - { - return 'mouser'; + return new ProviderInfoDTO( + key: self::PROVIDER_KEY, + name: 'Mouser', + description: 'This provider uses the Mouser API to search for parts.', + url: 'https://www.mouser.com/', + disabledHelp: 'Configure the API key in the provider settings to enable.', + settingsClass: MouserSettings::class, + capabilities: [ + ProviderCapabilities::BASIC, + ProviderCapabilities::PICTURE, + ProviderCapabilities::DATASHEET, + ProviderCapabilities::PRICE, + ], + ); } public function isActive(): bool @@ -207,17 +211,6 @@ class MouserProvider implements InfoProviderInterface return reset($tmp); } - public function getCapabilities(): array - { - return [ - ProviderCapabilities::BASIC, - ProviderCapabilities::PICTURE, - ProviderCapabilities::DATASHEET, - ProviderCapabilities::PRICE, - ]; - } - - /** * @param ResponseInterface $response * @return PartDetailDTO[] @@ -251,7 +244,7 @@ class MouserProvider implements InfoProviderInterface $result[] = new PartDetailDTO( - provider_key: $this->getProviderKey(), + provider_key: self::PROVIDER_KEY, provider_id: $product['MouserPartNumber'], name: $product['ManufacturerPartNumber'], description: $product['Description'], diff --git a/src/Services/InfoProviderSystem/Providers/OEMSecretsProvider.php b/src/Services/InfoProviderSystem/Providers/OEMSecretsProvider.php index 9764517b..edeb5f71 100644 --- a/src/Services/InfoProviderSystem/Providers/OEMSecretsProvider.php +++ b/src/Services/InfoProviderSystem/Providers/OEMSecretsProvider.php @@ -88,6 +88,7 @@ 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\Services\InfoProviderSystem\DTOs\ProviderInfoDTO; use App\Settings\InfoProviderSystem\OEMSecretsSettings; use App\Settings\InfoProviderSystem\OEMSecretsSortMode; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -96,6 +97,7 @@ use Psr\Cache\CacheItemPoolInterface; class OEMSecretsProvider implements InfoProviderInterface { + public const PROVIDER_KEY = 'oemsecrets'; private const ENDPOINT_URL = 'https://oemsecretsapi.com/partsearch'; @@ -227,37 +229,23 @@ class OEMSecretsProvider implements InfoProviderInterface private array $distributorCountryCodes = []; private array $countryCodeToRegionMap = []; - /** - * Get information about this provider - * - * @return array An associative array with the following keys (? means optional): - * - name: The (user friendly) name of the provider (e.g. "Digikey"), will be translated - * - description?: A short description of the provider (e.g. "Digikey is a ..."), will be translated - * - logo?: The logo of the provider (e.g. "digikey.png") - * - url?: The url of the provider (e.g. "https://www.digikey.com") - * - disabled_help?: A help text which is shown when the provider is disabled, explaining how to enable it - * - oauth_app_name?: The name of the OAuth app which is used for authentication (e.g. "ip_digikey_oauth"). If this is set a connect button will be shown - * - * @phpstan-return array{ name: string, description?: string, logo?: string, url?: string, disabled_help?: string, oauth_app_name?: string } - */ - public function getProviderInfo(): array + public function getProviderInfo(): ProviderInfoDTO { - return [ - 'name' => 'OEMSecrets', - 'description' => 'This provider uses the OEMSecrets API to search for parts.', - 'url' => 'https://www.oemsecrets.com/', - 'disabled_help' => 'Configure the API key in the provider settings to enable.', - 'settings_class' => OEMSecretsSettings::class - ]; - } - /** - * Returns a unique key for this provider, which will be saved into the database - * and used to identify the provider - * @return string A unique key for this provider (e.g. "digikey") - */ - public function getProviderKey(): string - { - return 'oemsecrets'; + return new ProviderInfoDTO( + key: self::PROVIDER_KEY, + name: 'OEMSecrets', + description: 'This provider uses the OEMSecrets API to search for parts.', + url: 'https://www.oemsecrets.com/', + disabledHelp: 'Configure the API key in the provider settings to enable.', + settingsClass: OEMSecretsSettings::class, + capabilities: [ + ProviderCapabilities::BASIC, + ProviderCapabilities::PICTURE, + ProviderCapabilities::DATASHEET, + ProviderCapabilities::PRICE, + ProviderCapabilities::PARAMETERS + ], + ); } /** @@ -456,17 +444,6 @@ class OEMSecretsProvider implements InfoProviderInterface * Currently, this list is purely informational and not used in functional checks. * @return ProviderCapabilities[] */ - public function getCapabilities(): array - { - return [ - ProviderCapabilities::BASIC, - ProviderCapabilities::PICTURE, - ProviderCapabilities::DATASHEET, - ProviderCapabilities::PRICE, - ]; - } - - /** * Processes a single product and updates arrays for basic information, datasheets, images, parameters, * and purchase information. Aggregates and organizes data received for a specific `part_number` and `manufacturer_id`. @@ -896,7 +873,7 @@ class OEMSecretsProvider implements InfoProviderInterface // If there is no existing basic info array, we create a new one if (is_null($existingBasicInfo)) { return [ - 'provider_key' => $this->getProviderKey(), + 'provider_key' => self::PROVIDER_KEY, 'provider_id' => $provider_id, 'name' => $product['part_number'], 'description' => $description, @@ -916,7 +893,7 @@ class OEMSecretsProvider implements InfoProviderInterface // Update fields only if empty or undefined, with additional check for preview_image_url return [ - 'provider_key' => $existingBasicInfo['provider_key'] ?? $this->getProviderKey(), + 'provider_key' => $existingBasicInfo['provider_key'] ?? self::PROVIDER_KEY, 'provider_id' => $existingBasicInfo['provider_id'] ?? $provider_id, 'name' => $existingBasicInfo['name'] ?? $product['part_number'], // Update description if it's null/empty @@ -1270,7 +1247,7 @@ class OEMSecretsProvider implements InfoProviderInterface */ private function generateInquiryUrl(string $partNumber, string $oemInquiry = 'compare/'): string { - $baseUrl = rtrim($this->getProviderInfo()['url'], '/') . '/'; + $baseUrl = rtrim($this->getProviderInfo()->url, '/') . '/'; $inquiryPath = trim($oemInquiry, '/') . '/'; $encodedPartNumber = urlencode(trim($partNumber)); return $baseUrl . $inquiryPath . $encodedPartNumber; diff --git a/src/Services/InfoProviderSystem/Providers/OctopartProvider.php b/src/Services/InfoProviderSystem/Providers/OctopartProvider.php index de404e18..14fdbfa5 100644 --- a/src/Services/InfoProviderSystem/Providers/OctopartProvider.php +++ b/src/Services/InfoProviderSystem/Providers/OctopartProvider.php @@ -28,6 +28,7 @@ use App\Services\InfoProviderSystem\DTOs\FileDTO; use App\Services\InfoProviderSystem\DTOs\ParameterDTO; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\InfoProviderSystem\DTOs\PriceDTO; +use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO; use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; use App\Services\OAuth\OAuthTokenManager; use App\Settings\InfoProviderSystem\OctopartSettings; @@ -43,6 +44,8 @@ use Symfony\Contracts\HttpClient\HttpClientInterface; */ class OctopartProvider implements InfoProviderInterface { + public const PROVIDER_KEY = 'octopart'; + private const OAUTH_APP_NAME = 'ip_octopart_oauth'; /** @@ -164,20 +167,25 @@ class OctopartProvider implements InfoProviderInterface return $response->toArray(true); } - public function getProviderInfo(): array + public function getProviderInfo(): ProviderInfoDTO { - return [ - 'name' => 'Octopart', - 'description' => 'This provider uses the Nexar/Octopart API to search for parts on Octopart.', - 'url' => 'https://www.octopart.com/', - 'disabled_help' => 'Set the Client ID and Secret in provider settings.', - 'settings_class' => OctopartSettings::class - ]; - } - - public function getProviderKey(): string - { - return 'octopart'; + return new ProviderInfoDTO( + key: self::PROVIDER_KEY, + name: 'Octopart', + description: 'This provider uses the Nexar/Octopart API to search for parts on Octopart.', + url: 'https://www.octopart.com/', + disabledHelp: 'Set the Client ID and Secret in provider settings.', + settingsClass: OctopartSettings::class, + capabilities: [ + ProviderCapabilities::BASIC, + ProviderCapabilities::FOOTPRINT, + ProviderCapabilities::PICTURE, + ProviderCapabilities::DATASHEET, + ProviderCapabilities::PRICE, + ProviderCapabilities::PARAMETERS + ], + expensive: true, + ); } public function isActive(): bool @@ -307,7 +315,7 @@ class OctopartProvider implements InfoProviderInterface } return new PartDetailDTO( - provider_key: $this->getProviderKey(), + provider_key: self::PROVIDER_KEY, provider_id: $part['id'], name: $part['mpn'], description: $part['shortDescription'] ?? null, @@ -397,14 +405,4 @@ class OctopartProvider implements InfoProviderInterface return $tmp; } - public function getCapabilities(): array - { - return [ - ProviderCapabilities::BASIC, - ProviderCapabilities::FOOTPRINT, - ProviderCapabilities::PICTURE, - ProviderCapabilities::DATASHEET, - ProviderCapabilities::PRICE, - ]; - } } diff --git a/src/Services/InfoProviderSystem/Providers/PollinProvider.php b/src/Services/InfoProviderSystem/Providers/PollinProvider.php index 7acecc3a..919db66b 100644 --- a/src/Services/InfoProviderSystem/Providers/PollinProvider.php +++ b/src/Services/InfoProviderSystem/Providers/PollinProvider.php @@ -29,6 +29,7 @@ use App\Services\InfoProviderSystem\DTOs\FileDTO; use App\Services\InfoProviderSystem\DTOs\ParameterDTO; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\InfoProviderSystem\DTOs\PriceDTO; +use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO; use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; use App\Settings\InfoProviderSystem\PollinSettings; @@ -38,6 +39,7 @@ use Symfony\Contracts\HttpClient\HttpClientInterface; class PollinProvider implements InfoProviderInterface, URLHandlerInfoProviderInterface { + public const PROVIDER_KEY = 'pollin'; public function __construct(private readonly HttpClientInterface $client, private readonly PollinSettings $settings, @@ -45,20 +47,23 @@ class PollinProvider implements InfoProviderInterface, URLHandlerInfoProviderInt { } - public function getProviderInfo(): array + public function getProviderInfo(): ProviderInfoDTO { - return [ - 'name' => 'Pollin', - 'description' => 'Webscraping from pollin.de to get part information', - 'url' => 'https://www.pollin.de/', - 'disabled_help' => 'Enable the provider in provider settings', - 'settings_class' => PollinSettings::class, - ]; - } - - public function getProviderKey(): string - { - return 'pollin'; + return new ProviderInfoDTO( + key: self::PROVIDER_KEY, + name: 'Pollin', + description: 'Webscraping from pollin.de to get part information', + url: 'https://www.pollin.de/', + disabledHelp: 'Enable the provider in provider settings', + settingsClass: PollinSettings::class, + capabilities: [ + ProviderCapabilities::BASIC, + ProviderCapabilities::PICTURE, + ProviderCapabilities::PRICE, + ProviderCapabilities::DATASHEET, + ProviderCapabilities::PARAMETERS + ], + ); } public function isActive(): bool @@ -88,7 +93,7 @@ class PollinProvider implements InfoProviderInterface, URLHandlerInfoProviderInt //Iterate over each div.product-box $dom->filter('div.product-box')->each(function (Crawler $node) use (&$results) { $results[] = new SearchResultDTO( - provider_key: $this->getProviderKey(), + provider_key: self::PROVIDER_KEY, provider_id: $node->filter('meta[itemprop="productID"]')->attr('content'), name: $node->filter('a.product-name')->text(), description: '', @@ -156,7 +161,7 @@ class PollinProvider implements InfoProviderInterface, URLHandlerInfoProviderInt $purchaseInfo = new PurchaseInfoDTO('Pollin', $orderId, $this->parsePrices($dom), $productPageUrl); return new PartDetailDTO( - provider_key: $this->getProviderKey(), + provider_key: self::PROVIDER_KEY, provider_id: $orderId, name: trim($dom->filter('meta[property="og:title"]')->attr('content')), description: $dom->filter('meta[property="og:description"]')->attr('content'), @@ -244,16 +249,6 @@ class PollinProvider implements InfoProviderInterface, URLHandlerInfoProviderInt ]; } - public function getCapabilities(): array - { - return [ - ProviderCapabilities::BASIC, - ProviderCapabilities::PICTURE, - ProviderCapabilities::PRICE, - ProviderCapabilities::DATASHEET - ]; - } - public function getHandledDomains(): array { return ['pollin.de']; diff --git a/src/Services/InfoProviderSystem/Providers/ProviderCapabilities.php b/src/Services/InfoProviderSystem/Providers/ProviderCapabilities.php index 3a7d03e9..a97aa347 100644 --- a/src/Services/InfoProviderSystem/Providers/ProviderCapabilities.php +++ b/src/Services/InfoProviderSystem/Providers/ProviderCapabilities.php @@ -26,28 +26,28 @@ namespace App\Services\InfoProviderSystem\Providers; /** * This enum contains all capabilities (which data it can provide) a provider can have. */ -enum ProviderCapabilities +enum ProviderCapabilities: string { /** Basic information about a part, like the name, description, part number, manufacturer etc */ - case BASIC; + case BASIC = 'BASIC'; /** Provider can provide a picture for a part */ - case PICTURE; + case PICTURE = 'PICTURE'; /** Provider can provide datasheets for a part */ - case DATASHEET; + case DATASHEET = 'DATASHEET'; /** Provider can provide prices for a part */ - case PRICE; + case PRICE = 'PRICE'; /** Information about the footprint of a part */ - case FOOTPRINT; + case FOOTPRINT = 'FOOTPRINT'; /** Provider can provide GTIN for a part */ - case GTIN; + case GTIN = 'GTIN'; /** Provider can provide parameters/specifications for a part */ - case PARAMETERS; + case PARAMETERS = 'PARAMETERS'; /** * Get the order index for displaying capabilities in a stable order. diff --git a/src/Services/InfoProviderSystem/Providers/ReicheltProvider.php b/src/Services/InfoProviderSystem/Providers/ReicheltProvider.php index 9dfb099f..901956e1 100644 --- a/src/Services/InfoProviderSystem/Providers/ReicheltProvider.php +++ b/src/Services/InfoProviderSystem/Providers/ReicheltProvider.php @@ -28,6 +28,7 @@ use App\Services\InfoProviderSystem\DTOs\FileDTO; use App\Services\InfoProviderSystem\DTOs\ParameterDTO; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\InfoProviderSystem\DTOs\PriceDTO; +use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO; use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; use App\Settings\InfoProviderSystem\ReicheltSettings; @@ -38,6 +39,7 @@ class ReicheltProvider implements InfoProviderInterface { public const DISTRIBUTOR_NAME = "Reichelt"; + public const PROVIDER_KEY = 'reichelt'; private readonly HttpClientInterface $client; @@ -48,20 +50,24 @@ class ReicheltProvider implements InfoProviderInterface $this->client = new RandomizeUseragentHttpClient($client); } - public function getProviderInfo(): array + public function getProviderInfo(): ProviderInfoDTO { - return [ - 'name' => 'Reichelt', - 'description' => 'Webscraping from reichelt.com to get part information', - 'url' => 'https://www.reichelt.com/', - 'disabled_help' => 'Enable provider in provider settings.', - 'settings_class' => ReicheltSettings::class, - ]; - } - - public function getProviderKey(): string - { - return 'reichelt'; + return new ProviderInfoDTO( + key: self::PROVIDER_KEY, + name: 'Reichelt', + description: 'Webscraping from reichelt.com to get part information', + url: 'https://www.reichelt.com/', + disabledHelp: 'Enable provider in provider settings.', + settingsClass: ReicheltSettings::class, + capabilities: [ + ProviderCapabilities::BASIC, + ProviderCapabilities::PICTURE, + ProviderCapabilities::DATASHEET, + ProviderCapabilities::PRICE, + ProviderCapabilities::GTIN, + ProviderCapabilities::PARAMETERS + ], + ); } public function isActive(): bool @@ -93,7 +99,7 @@ class ReicheltProvider implements InfoProviderInterface $pictureURL = $element->filter("div.al_artlogo img")->attr('src'); $results[] = new SearchResultDTO( - provider_key: $this->getProviderKey(), + provider_key: self::PROVIDER_KEY, provider_id: $artId, name: $productID, description: $name, @@ -173,7 +179,7 @@ class ReicheltProvider implements InfoProviderInterface //Create part object return new PartDetailDTO( - provider_key: $this->getProviderKey(), + provider_key: self::PROVIDER_KEY, provider_id: $id, name: $json[0]['article_artnr'], description: $json[0]['article_besch'], @@ -282,14 +288,4 @@ class ReicheltProvider implements InfoProviderInterface return 'https://www.reichelt.com/' . strtolower($this->settings->country) . '/' . strtolower($this->settings->language); } - public function getCapabilities(): array - { - return [ - ProviderCapabilities::BASIC, - ProviderCapabilities::PICTURE, - ProviderCapabilities::DATASHEET, - ProviderCapabilities::PRICE, - ProviderCapabilities::GTIN, - ]; - } } diff --git a/src/Services/InfoProviderSystem/Providers/TMEProvider.php b/src/Services/InfoProviderSystem/Providers/TMEProvider.php index 24ba0ea7..3d230a26 100644 --- a/src/Services/InfoProviderSystem/Providers/TMEProvider.php +++ b/src/Services/InfoProviderSystem/Providers/TMEProvider.php @@ -28,12 +28,14 @@ use App\Services\InfoProviderSystem\DTOs\FileDTO; use App\Services\InfoProviderSystem\DTOs\ParameterDTO; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\InfoProviderSystem\DTOs\PriceDTO; +use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO; use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; use App\Settings\InfoProviderSystem\TMESettings; class TMEProvider implements InfoProviderInterface, URLHandlerInfoProviderInterface { + public const PROVIDER_KEY = 'tme'; private const VENDOR_NAME = 'TME'; @@ -48,20 +50,24 @@ class TMEProvider implements InfoProviderInterface, URLHandlerInfoProviderInterf } } - public function getProviderInfo(): array + public function getProviderInfo(): ProviderInfoDTO { - return [ - 'name' => 'TME', - 'description' => 'This provider uses the API of TME (Transfer Multipart).', - 'url' => 'https://tme.eu/', - 'disabled_help' => 'Configure the API Token and secret in provider settings to use this provider.', - 'settings_class' => TMESettings::class - ]; - } - - public function getProviderKey(): string - { - return 'tme'; + return new ProviderInfoDTO( + key: self::PROVIDER_KEY, + name: 'TME', + description: 'This provider uses the API of TME (Transfer Multipart).', + url: 'https://tme.eu/', + disabledHelp: 'Configure the API Token and secret in provider settings to use this provider.', + settingsClass: TMESettings::class, + capabilities: [ + ProviderCapabilities::BASIC, + ProviderCapabilities::FOOTPRINT, + ProviderCapabilities::PICTURE, + ProviderCapabilities::DATASHEET, + ProviderCapabilities::PRICE, + ProviderCapabilities::PARAMETERS + ], + ); } public function isActive(): bool @@ -83,7 +89,7 @@ class TMEProvider implements InfoProviderInterface, URLHandlerInfoProviderInterf foreach($data['ProductList'] as $product) { $result[] = new SearchResultDTO( - provider_key: $this->getProviderKey(), + provider_key: self::PROVIDER_KEY, provider_id: $product['Symbol'], name: empty($product['OriginalSymbol']) ? $product['Symbol'] : $product['OriginalSymbol'], description: $product['Description'], @@ -119,7 +125,7 @@ class TMEProvider implements InfoProviderInterface, URLHandlerInfoProviderInterf $parameters = $this->getParameters($id, $footprint); return new PartDetailDTO( - provider_key: $this->getProviderKey(), + provider_key: self::PROVIDER_KEY, provider_id: $product['Symbol'], name: empty($product['OriginalSymbol']) ? $product['Symbol'] : $product['OriginalSymbol'], description: $product['Description'], @@ -290,17 +296,6 @@ class TMEProvider implements InfoProviderInterface, URLHandlerInfoProviderInterf return $url; } - public function getCapabilities(): array - { - return [ - ProviderCapabilities::BASIC, - ProviderCapabilities::FOOTPRINT, - ProviderCapabilities::PICTURE, - ProviderCapabilities::DATASHEET, - ProviderCapabilities::PRICE, - ]; - } - public function getHandledDomains(): array { return ['tme.eu']; diff --git a/src/Services/InfoProviderSystem/Providers/TestProvider.php b/src/Services/InfoProviderSystem/Providers/TestProvider.php index 42927abd..f4528d34 100644 --- a/src/Services/InfoProviderSystem/Providers/TestProvider.php +++ b/src/Services/InfoProviderSystem/Providers/TestProvider.php @@ -25,6 +25,7 @@ namespace App\Services\InfoProviderSystem\Providers; use App\Services\InfoProviderSystem\DTOs\FileDTO; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; +use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO; use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; use Symfony\Component\DependencyInjection\Attribute\When; @@ -34,20 +35,20 @@ use Symfony\Component\DependencyInjection\Attribute\When; #[When(env: 'test')] class TestProvider implements InfoProviderInterface { + public const PROVIDER_KEY = 'test'; - public function getProviderInfo(): array + public function getProviderInfo(): ProviderInfoDTO { - return [ - 'name' => 'Test Provider', - 'description' => 'This is a test provider', - //'url' => 'https://example.com', - 'disabled_help' => 'This provider is disabled for testing purposes' - ]; - } - - public function getProviderKey(): string - { - return 'test'; + return new ProviderInfoDTO( + key: self::PROVIDER_KEY, + name: 'Test Provider', + description: 'This is a test provider', + disabledHelp: 'This provider is disabled for testing purposes', + capabilities: [ + ProviderCapabilities::BASIC, + ProviderCapabilities::FOOTPRINT, + ], + ); } public function isActive(): bool @@ -58,24 +59,16 @@ class TestProvider implements InfoProviderInterface public function searchByKeyword(string $keyword, array $options = []): array { return [ - new SearchResultDTO(provider_key: $this->getProviderKey(), provider_id: 'element1', name: 'Element 1', description: 'fd'), - new SearchResultDTO(provider_key: $this->getProviderKey(), provider_id: 'element2', name: 'Element 2', description: 'fd'), - new SearchResultDTO(provider_key: $this->getProviderKey(), provider_id: 'element3', name: 'Element 3', description: 'fd'), - ]; - } - - public function getCapabilities(): array - { - return [ - ProviderCapabilities::BASIC, - ProviderCapabilities::FOOTPRINT, + new SearchResultDTO(provider_key: self::PROVIDER_KEY, provider_id: 'element1', name: 'Element 1', description: 'fd'), + new SearchResultDTO(provider_key: self::PROVIDER_KEY, provider_id: 'element2', name: 'Element 2', description: 'fd'), + new SearchResultDTO(provider_key: self::PROVIDER_KEY, provider_id: 'element3', name: 'Element 3', description: 'fd'), ]; } public function getDetails(string $id, array $options = []): PartDetailDTO { return new PartDetailDTO( - provider_key: $this->getProviderKey(), + provider_key: self::PROVIDER_KEY, provider_id: $id, name: 'Test Element', description: 'fd', diff --git a/src/Services/LabelSystem/Barcodes/BarcodeHelper.php b/src/Services/LabelSystem/Barcodes/BarcodeHelper.php index c9fe64f3..53e52662 100644 --- a/src/Services/LabelSystem/Barcodes/BarcodeHelper.php +++ b/src/Services/LabelSystem/Barcodes/BarcodeHelper.php @@ -67,8 +67,8 @@ class BarcodeHelper { $svg = $this->barcodeAsSVG($content, $type); $base64 = $this->dataUri($svg, 'image/svg+xml'); - $alt_text ??= $content; - + $alt_text ??= htmlspecialchars($content); + return ''.$alt_text.''; } @@ -94,4 +94,4 @@ class BarcodeHelper return $repr; } -} \ No newline at end of file +} diff --git a/src/Services/LabelSystem/PlaceholderProviders/AbstractDBElementProvider.php b/src/Services/LabelSystem/PlaceholderProviders/AbstractDBElementProvider.php index 081b3e91..0059a3fc 100644 --- a/src/Services/LabelSystem/PlaceholderProviders/AbstractDBElementProvider.php +++ b/src/Services/LabelSystem/PlaceholderProviders/AbstractDBElementProvider.php @@ -44,9 +44,9 @@ namespace App\Services\LabelSystem\PlaceholderProviders; use App\Entity\Base\AbstractDBElement; use App\Services\ElementTypeNameGenerator; -final class AbstractDBElementProvider implements PlaceholderProviderInterface +final readonly class AbstractDBElementProvider implements PlaceholderProviderInterface { - public function __construct(private readonly ElementTypeNameGenerator $elementTypeNameGenerator) + public function __construct(private ElementTypeNameGenerator $elementTypeNameGenerator) { } @@ -54,7 +54,7 @@ final class AbstractDBElementProvider implements PlaceholderProviderInterface { if ($label_target instanceof AbstractDBElement) { if ('[[TYPE]]' === $placeholder) { - return $this->elementTypeNameGenerator->getLocalizedTypeLabel($label_target); + return $this->elementTypeNameGenerator->typeLabel($label_target); } if ('[[ID]]' === $placeholder) { diff --git a/src/Services/LabelSystem/PlaceholderProviders/BarcodeProvider.php b/src/Services/LabelSystem/PlaceholderProviders/BarcodeProvider.php index 9719917a..745a8227 100644 --- a/src/Services/LabelSystem/PlaceholderProviders/BarcodeProvider.php +++ b/src/Services/LabelSystem/PlaceholderProviders/BarcodeProvider.php @@ -31,11 +31,11 @@ use App\Services\LabelSystem\LabelBarcodeGenerator; use App\Services\LabelSystem\Barcodes\BarcodeContentGenerator; use Com\Tecnick\Barcode\Exception; -final class BarcodeProvider implements PlaceholderProviderInterface +final readonly class BarcodeProvider implements PlaceholderProviderInterface { - public function __construct(private readonly LabelBarcodeGenerator $barcodeGenerator, - private readonly BarcodeContentGenerator $barcodeContentGenerator, - private readonly BarcodeHelper $barcodeHelper) + public function __construct(private LabelBarcodeGenerator $barcodeGenerator, + private BarcodeContentGenerator $barcodeContentGenerator, + private BarcodeHelper $barcodeHelper) { } diff --git a/src/Services/LabelSystem/PlaceholderProviders/GlobalProviders.php b/src/Services/LabelSystem/PlaceholderProviders/GlobalProviders.php index f14a5863..38b59bb4 100644 --- a/src/Services/LabelSystem/PlaceholderProviders/GlobalProviders.php +++ b/src/Services/LabelSystem/PlaceholderProviders/GlobalProviders.php @@ -53,11 +53,11 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface; * Provides Placeholders for infos about global infos like Installation name or datetimes. * @see \App\Tests\Services\LabelSystem\PlaceholderProviders\GlobalProvidersTest */ -final class GlobalProviders implements PlaceholderProviderInterface +final readonly class GlobalProviders implements PlaceholderProviderInterface { public function __construct( - private readonly Security $security, - private readonly UrlGeneratorInterface $url_generator, + private Security $security, + private UrlGeneratorInterface $url_generator, private CustomizationSettings $customizationSettings, ) { @@ -66,13 +66,13 @@ final class GlobalProviders implements PlaceholderProviderInterface public function replace(string $placeholder, object $label_target, array $options = []): ?string { if ('[[INSTALL_NAME]]' === $placeholder) { - return $this->customizationSettings->instanceName; + return htmlspecialchars($this->customizationSettings->instanceName); } $user = $this->security->getUser(); if ('[[USERNAME]]' === $placeholder) { if ($user instanceof User) { - return $user->getName(); + return htmlspecialchars($user->getName()); } return 'anonymous'; @@ -80,7 +80,7 @@ final class GlobalProviders implements PlaceholderProviderInterface if ('[[USERNAME_FULL]]' === $placeholder) { if ($user instanceof User) { - return $user->getFullName(true); + return htmlspecialchars($user->getFullName(true)); } return 'anonymous'; diff --git a/src/Services/LabelSystem/PlaceholderProviders/NamedElementProvider.php b/src/Services/LabelSystem/PlaceholderProviders/NamedElementProvider.php index d8d38120..519f5081 100644 --- a/src/Services/LabelSystem/PlaceholderProviders/NamedElementProvider.php +++ b/src/Services/LabelSystem/PlaceholderProviders/NamedElementProvider.php @@ -51,7 +51,7 @@ final class NamedElementProvider implements PlaceholderProviderInterface public function replace(string $placeholder, object $label_target, array $options = []): ?string { if ($label_target instanceof NamedElementInterface && '[[NAME]]' === $placeholder) { - return $label_target->getName(); + return htmlspecialchars($label_target->getName()); } return null; diff --git a/src/Services/LabelSystem/PlaceholderProviders/PartLotProvider.php b/src/Services/LabelSystem/PlaceholderProviders/PartLotProvider.php index 946b4892..7dee615b 100644 --- a/src/Services/LabelSystem/PlaceholderProviders/PartLotProvider.php +++ b/src/Services/LabelSystem/PlaceholderProviders/PartLotProvider.php @@ -52,9 +52,9 @@ use Locale; /** * @see \App\Tests\Services\LabelSystem\PlaceholderProviders\PartLotProviderTest */ -final class PartLotProvider implements PlaceholderProviderInterface +final readonly class PartLotProvider implements PlaceholderProviderInterface { - public function __construct(private readonly LabelTextReplacer $labelTextReplacer, private readonly AmountFormatter $amountFormatter) + public function __construct(private LabelTextReplacer $labelTextReplacer, private AmountFormatter $amountFormatter) { } @@ -66,11 +66,11 @@ final class PartLotProvider implements PlaceholderProviderInterface } if ('[[LOT_NAME]]' === $placeholder) { - return $label_target->getName(); + return htmlspecialchars($label_target->getName()); } if ('[[LOT_COMMENT]]' === $placeholder) { - return $label_target->getComment(); + return htmlspecialchars($label_target->getComment()); } if ('[[EXPIRATION_DATE]]' === $placeholder) { @@ -95,19 +95,19 @@ final class PartLotProvider implements PlaceholderProviderInterface } if ('[[LOCATION]]' === $placeholder) { - return $label_target->getStorageLocation() instanceof StorageLocation ? $label_target->getStorageLocation()->getName() : ''; + return $label_target->getStorageLocation() instanceof StorageLocation ? htmlspecialchars($label_target->getStorageLocation()->getName()) : ''; } if ('[[LOCATION_FULL]]' === $placeholder) { - return $label_target->getStorageLocation() instanceof StorageLocation ? $label_target->getStorageLocation()->getFullPath() : ''; + return $label_target->getStorageLocation() instanceof StorageLocation ? htmlspecialchars($label_target->getStorageLocation()->getFullPath()) : ''; } if ('[[OWNER]]' === $placeholder) { - return $label_target->getOwner() instanceof User ? $label_target->getOwner()->getFullName() : ''; + return $label_target->getOwner() instanceof User ? htmlspecialchars($label_target->getOwner()->getFullName()) : ''; } if ('[[OWNER_USERNAME]]' === $placeholder) { - return $label_target->getOwner() instanceof User ? $label_target->getOwner()->getUsername() : ''; + return $label_target->getOwner() instanceof User ? htmlspecialchars($label_target->getOwner()->getUsername()) : ''; } return $this->labelTextReplacer->handlePlaceholder($placeholder, $label_target->getPart()); diff --git a/src/Services/LabelSystem/PlaceholderProviders/PartProvider.php b/src/Services/LabelSystem/PlaceholderProviders/PartProvider.php index 7d9e4db5..857aa1ff 100644 --- a/src/Services/LabelSystem/PlaceholderProviders/PartProvider.php +++ b/src/Services/LabelSystem/PlaceholderProviders/PartProvider.php @@ -54,11 +54,11 @@ use Symfony\Contracts\Translation\TranslatorInterface; /** * @see \App\Tests\Services\LabelSystem\PlaceholderProviders\PartProviderTest */ -final class PartProvider implements PlaceholderProviderInterface +final readonly class PartProvider implements PlaceholderProviderInterface { - private readonly MarkdownConverter $inlineConverter; + private MarkdownConverter $inlineConverter; - public function __construct(private readonly SIFormatter $siFormatter, private readonly TranslatorInterface $translator) + public function __construct(private SIFormatter $siFormatter, private TranslatorInterface $translator) { $environment = new Environment(); $environment->addExtension(new InlinesOnlyExtension()); @@ -72,27 +72,27 @@ final class PartProvider implements PlaceholderProviderInterface } if ('[[CATEGORY]]' === $placeholder) { - return $part->getCategory() instanceof Category ? $part->getCategory()->getName() : ''; + return $part->getCategory() instanceof Category ? htmlspecialchars($part->getCategory()->getName()) : ''; } if ('[[CATEGORY_FULL]]' === $placeholder) { - return $part->getCategory() instanceof Category ? $part->getCategory()->getFullPath() : ''; + return $part->getCategory() instanceof Category ? htmlspecialchars($part->getCategory()->getFullPath()) : ''; } if ('[[MANUFACTURER]]' === $placeholder) { - return $part->getManufacturer() instanceof Manufacturer ? $part->getManufacturer()->getName() : ''; + return $part->getManufacturer() instanceof Manufacturer ? htmlspecialchars($part->getManufacturer()->getName()) : ''; } if ('[[MANUFACTURER_FULL]]' === $placeholder) { - return $part->getManufacturer() instanceof Manufacturer ? $part->getManufacturer()->getFullPath() : ''; + return $part->getManufacturer() instanceof Manufacturer ? htmlspecialchars($part->getManufacturer()->getFullPath()) : ''; } if ('[[FOOTPRINT]]' === $placeholder) { - return $part->getFootprint() instanceof Footprint ? $part->getFootprint()->getName() : ''; + return $part->getFootprint() instanceof Footprint ? htmlspecialchars($part->getFootprint()->getName()) : ''; } if ('[[FOOTPRINT_FULL]]' === $placeholder) { - return $part->getFootprint() instanceof Footprint ? $part->getFootprint()->getFullPath() : ''; + return $part->getFootprint() instanceof Footprint ? htmlspecialchars($part->getFootprint()->getFullPath()) : ''; } if ('[[MASS]]' === $placeholder) { @@ -100,15 +100,15 @@ final class PartProvider implements PlaceholderProviderInterface } if ('[[MPN]]' === $placeholder) { - return $part->getManufacturerProductNumber(); + return htmlspecialchars($part->getManufacturerProductNumber()); } if ('[[IPN]]' === $placeholder) { - return $part->getIpn() ?? ''; + return $part->getIpn() ? htmlspecialchars($part->getIpn()) : ''; } if ('[[TAGS]]' === $placeholder) { - return $part->getTags(); + return htmlspecialchars($part->getTags()); } if ('[[M_STATUS]]' === $placeholder) { diff --git a/src/Services/LabelSystem/PlaceholderProviders/PlaceholderProviderInterface.php b/src/Services/LabelSystem/PlaceholderProviders/PlaceholderProviderInterface.php index f5b1fc3b..3c1857d6 100644 --- a/src/Services/LabelSystem/PlaceholderProviders/PlaceholderProviderInterface.php +++ b/src/Services/LabelSystem/PlaceholderProviders/PlaceholderProviderInterface.php @@ -46,6 +46,7 @@ interface PlaceholderProviderInterface /** * Determines the real value of this placeholder. * If the placeholder is not supported, null must be returned. + * The placeholder provider has to handle HTML escaping, as the output of this function will be used directly in the label template. * * @param string $placeholder The placeholder (e.g. "%%PLACEHOLDER%%") that should be replaced * @param object $label_target The object that is targeted by the label diff --git a/src/Services/LabelSystem/PlaceholderProviders/StorelocationProvider.php b/src/Services/LabelSystem/PlaceholderProviders/StorelocationProvider.php index 4b4d8dcd..de791b94 100644 --- a/src/Services/LabelSystem/PlaceholderProviders/StorelocationProvider.php +++ b/src/Services/LabelSystem/PlaceholderProviders/StorelocationProvider.php @@ -31,11 +31,11 @@ class StorelocationProvider implements PlaceholderProviderInterface { if ($label_target instanceof StorageLocation) { if ('[[OWNER]]' === $placeholder) { - return $label_target->getOwner() instanceof User ? $label_target->getOwner()->getFullName() : ''; + return $label_target->getOwner() instanceof User ? htmlspecialchars($label_target->getOwner()->getFullName()) : ''; } if ('[[OWNER_USERNAME]]' === $placeholder) { - return $label_target->getOwner() instanceof User ? $label_target->getOwner()->getUsername() : ''; + return $label_target->getOwner() instanceof User ? htmlspecialchars($label_target->getOwner()->getUsername()) : ''; } } diff --git a/src/Services/LabelSystem/PlaceholderProviders/StructuralDBElementProvider.php b/src/Services/LabelSystem/PlaceholderProviders/StructuralDBElementProvider.php index f37f5901..c8c49cfe 100644 --- a/src/Services/LabelSystem/PlaceholderProviders/StructuralDBElementProvider.php +++ b/src/Services/LabelSystem/PlaceholderProviders/StructuralDBElementProvider.php @@ -55,13 +55,13 @@ final class StructuralDBElementProvider implements PlaceholderProviderInterface return strip_tags((string) $label_target->getComment()); } if ('[[FULL_PATH]]' === $placeholder) { - return $label_target->getFullPath(); + return htmlspecialchars($label_target->getFullPath()); } if ('[[PARENT]]' === $placeholder) { - return $label_target->getParent() instanceof AbstractStructuralDBElement ? $label_target->getParent()->getName() : ''; + return $label_target->getParent() instanceof AbstractStructuralDBElement ? htmlspecialchars($label_target->getParent()->getName()) : ''; } if ('[[PARENT_FULL_PATH]]' === $placeholder) { - return $label_target->getParent() instanceof AbstractStructuralDBElement ? $label_target->getParent()->getFullPath() : ''; + return $label_target->getParent() instanceof AbstractStructuralDBElement ? htmlspecialchars($label_target->getParent()->getFullPath()) : ''; } } diff --git a/src/Services/System/TrustedHostsChecker.php b/src/Services/System/TrustedHostsChecker.php new file mode 100644 index 00000000..49587dc9 --- /dev/null +++ b/src/Services/System/TrustedHostsChecker.php @@ -0,0 +1,45 @@ +. + */ + +declare(strict_types=1); + +namespace App\Services\System; + +use Symfony\Component\DependencyInjection\Attribute\Autowire; + +/** + * Checks whether the TRUSTED_HOSTS environment variable has been configured. + */ +final readonly class TrustedHostsChecker +{ + public function __construct( + #[Autowire('%env(TRUSTED_HOSTS)%')] + private string $trustedHosts, + ) { + } + + /** + * @return bool True if TRUSTED_HOSTS is not configured (meaning Part-DB accepts requests with any Host header), false otherwise. + */ + public function isTrustedHostsUnconfigured(): bool + { + return trim($this->trustedHosts) === ''; + } +} diff --git a/src/Services/System/TrustedUrlGenerator.php b/src/Services/System/TrustedUrlGenerator.php new file mode 100644 index 00000000..3e023a47 --- /dev/null +++ b/src/Services/System/TrustedUrlGenerator.php @@ -0,0 +1,96 @@ +. + */ + +declare(strict_types=1); + +namespace App\Services\System; + +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; + +/** + * Generates absolute URLs for routes whose host can be trusted, for use in contexts where the host must not + * be attacker-controllable (e.g. links sent out via email). + * + * If no TRUSTED_HOSTS is configured, Symfony does not validate the Host header of the incoming request in + * any way, so it must not be trusted to generate such links (otherwise an attacker could poison them via a + * forged Host header). In that case, the host/scheme/base path are forced to the ones configured via + * DEFAULT_URI instead of the ones from the current HTTP request. + * If TRUSTED_HOSTS is configured, Symfony already rejects requests with a non-matching Host header, so the + * request's host can be trusted and is used as usual (which allows the app to be reachable under multiple + * trusted hostnames). + */ +final class TrustedUrlGenerator +{ + public function __construct( + private readonly UrlGeneratorInterface $urlGenerator, + private readonly TrustedHostsChecker $trustedHostsChecker, + #[Autowire('%partdb.default_uri%')] + private readonly string $defaultUri, + ) { + } + + /** + * @param array $parameters + */ + public function generate(string $route, array $parameters = []): string + { + //If TRUSTED_HOSTS is configured, Symfony already validated the request's Host header, so it can be trusted + if (!$this->trustedHostsChecker->isTrustedHostsUnconfigured()) { + return $this->urlGenerator->generate($route, $parameters, UrlGeneratorInterface::ABSOLUTE_URL); + } + + $parts = parse_url(rtrim($this->defaultUri, '/')); + + $context = $this->urlGenerator->getContext(); + + //Backup the original context, so we can restore it after generating the URL + $original = [ + 'host' => $context->getHost(), + 'scheme' => $context->getScheme(), + 'httpPort' => $context->getHttpPort(), + 'httpsPort' => $context->getHttpsPort(), + 'baseUrl' => $context->getBaseUrl(), + ]; + + $scheme = $parts['scheme'] ?? 'https'; + $context->setScheme($scheme); + $context->setHost($parts['host'] ?? ''); + if (isset($parts['port'])) { + if ($scheme === 'https') { + $context->setHttpsPort($parts['port']); + } else { + $context->setHttpPort($parts['port']); + } + } + $context->setBaseUrl($parts['path'] ?? ''); + + try { + return $this->urlGenerator->generate($route, $parameters, UrlGeneratorInterface::ABSOLUTE_URL); + } finally { + //Restore the original context, so the rest of the request is not affected + $context->setHost($original['host']); + $context->setScheme($original['scheme']); + $context->setHttpPort($original['httpPort']); + $context->setHttpsPort($original['httpsPort']); + $context->setBaseUrl($original['baseUrl']); + } + } +} diff --git a/src/Services/UserSystem/PasswordResetManager.php b/src/Services/UserSystem/PasswordResetManager.php index 1a78cb60..d826703f 100644 --- a/src/Services/UserSystem/PasswordResetManager.php +++ b/src/Services/UserSystem/PasswordResetManager.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace App\Services\UserSystem; use App\Entity\UserSystem\User; +use App\Services\System\TrustedUrlGenerator; use DateTime; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bridge\Twig\Mime\TemplatedEmail; @@ -39,7 +40,8 @@ class PasswordResetManager public function __construct(protected MailerInterface $mailer, protected EntityManagerInterface $em, protected TranslatorInterface $translator, protected UserPasswordHasherInterface $userPasswordEncoder, - PasswordHasherFactoryInterface $encoderFactory) + PasswordHasherFactoryInterface $encoderFactory, + protected TrustedUrlGenerator $trustedUrlGenerator) { $this->passwordEncoder = $encoderFactory->getPasswordHasher(User::class); } @@ -63,6 +65,9 @@ class PasswordResetManager $user->setPwResetExpires($expiration_date); if ($user->getEmail() !== null && $user->getEmail() !== '') { + $reset_url = $this->trustedUrlGenerator->generate('pw_reset_new_pw', ['user' => $user->getName(), 'token' => $unencrypted_token]); + $reset_url_fallback = $this->trustedUrlGenerator->generate('pw_reset_new_pw'); + $address = new Address($user->getEmail(), $user->getFullName()); $mail = new TemplatedEmail(); $mail->to($address); @@ -72,6 +77,8 @@ class PasswordResetManager 'expiration_date' => $expiration_date, 'token' => $unencrypted_token, 'user' => $user, + 'reset_url' => $reset_url, + 'reset_url_fallback' => $reset_url_fallback, ]); //Send email diff --git a/src/Settings/AISettings/AISettings.php b/src/Settings/AISettings/AISettings.php index 732eb597..79abfda9 100644 --- a/src/Settings/AISettings/AISettings.php +++ b/src/Settings/AISettings/AISettings.php @@ -35,9 +35,17 @@ class AISettings { use SettingsTrait; + public const TIMEOUT_LIMIT = 600; + + #[EmbeddedSettings] + public ?McpSettings $mcp = null; + #[EmbeddedSettings] public ?OpenRouterSettings $openRouter = null; #[EmbeddedSettings] public ?LMStudioSettings $lmstudio = null; + + #[EmbeddedSettings] + public ?OllamaSettings $ollama = null; } diff --git a/src/Settings/AISettings/LMStudioSettings.php b/src/Settings/AISettings/LMStudioSettings.php index 2bdad06e..d92ce97e 100644 --- a/src/Settings/AISettings/LMStudioSettings.php +++ b/src/Settings/AISettings/LMStudioSettings.php @@ -23,16 +23,17 @@ declare(strict_types=1); namespace App\Settings\AISettings; -use App\Form\Type\APIKeyType; use App\Services\AI\AIPlatformSettingsInterface; use App\Settings\SettingsIcon; use Jbtronics\SettingsBundle\Metadata\EnvVarMode; use Jbtronics\SettingsBundle\Settings\Settings; use Jbtronics\SettingsBundle\Settings\SettingsParameter; use Jbtronics\SettingsBundle\Settings\SettingsTrait; +use Symfony\Component\Form\Extension\Core\Type\NumberType; use Symfony\Component\Form\Extension\Core\Type\UrlType; use Symfony\Component\Translation\StaticMessage; use Symfony\Component\Translation\TranslatableMessage as TM; +use Symfony\Component\Validator\Constraints as Assert; #[Settings(name: 'ai_lmstudio', label: new TM("settings.ai.lmstudio"))] #[SettingsIcon("fa-robot")] @@ -46,6 +47,14 @@ class LMStudioSettings implements AIPlatformSettingsInterface envVar: "AI_LMSTUDIO_HOSTURL", envVarMode: EnvVarMode::OVERWRITE)] public ?string $hostURL = null; + #[SettingsParameter(label: new TM("settings.ai.timeout"), + description: new TM("settings.ai.timeout.help"), + formType: NumberType::class, + formOptions: ["scale" => 0, "attr" => ["min" => 1]], + )] + #[Assert\Range(min: 1, max: AISettings::TIMEOUT_LIMIT)] + public int $timeout = 180; + public function isAIPlatformEnabled(): bool { return $this->hostURL !== null && $this->hostURL !== ""; diff --git a/src/Settings/AISettings/McpSettings.php b/src/Settings/AISettings/McpSettings.php new file mode 100644 index 00000000..929efa41 --- /dev/null +++ b/src/Settings/AISettings/McpSettings.php @@ -0,0 +1,45 @@ +. + */ + +declare(strict_types=1); + +namespace App\Settings\AISettings; + +use App\Settings\SettingsIcon; +use Jbtronics\SettingsBundle\Metadata\EnvVarMode; +use Jbtronics\SettingsBundle\Settings\Settings; +use Jbtronics\SettingsBundle\Settings\SettingsParameter; +use Jbtronics\SettingsBundle\Settings\SettingsTrait; +use Symfony\Component\Translation\TranslatableMessage as TM; + +#[Settings(label: new TM("settings.misc.mcp"))] +#[SettingsIcon("fa-robot")] +class McpSettings +{ + use SettingsTrait; + + #[SettingsParameter( + label: new TM("settings.misc.mcp.enabled"), + description: new TM("settings.misc.mcp.enabled.help"), + envVar: "bool:MCP_ENABLED", + envVarMode: EnvVarMode::OVERWRITE, + )] + public bool $enabled = false; +} diff --git a/src/Settings/AISettings/OllamaSettings.php b/src/Settings/AISettings/OllamaSettings.php new file mode 100644 index 00000000..7ca0e5a0 --- /dev/null +++ b/src/Settings/AISettings/OllamaSettings.php @@ -0,0 +1,68 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Settings\AISettings; + +use App\Form\Type\APIKeyType; +use App\Services\AI\AIPlatformSettingsInterface; +use App\Settings\SettingsIcon; +use Jbtronics\SettingsBundle\Metadata\EnvVarMode; +use Jbtronics\SettingsBundle\Settings\Settings; +use Jbtronics\SettingsBundle\Settings\SettingsParameter; +use Jbtronics\SettingsBundle\Settings\SettingsTrait; +use Symfony\Component\Form\Extension\Core\Type\NumberType; +use Symfony\Component\Form\Extension\Core\Type\UrlType; +use Symfony\Component\Translation\StaticMessage; +use Symfony\Component\Translation\TranslatableMessage as TM; +use Symfony\Component\Validator\Constraints as Assert; + +#[Settings(name: 'ai_ollama', label: new TM("settings.ai.ollama"))] +#[SettingsIcon("fa-robot")] +class OllamaSettings implements AIPlatformSettingsInterface +{ + use SettingsTrait; + + #[SettingsParameter(label: new TM("settings.ai.ollama.endpoint"), + formType: UrlType::class, + formOptions: ["attr" => ["placeholder" => new StaticMessage("http://localhost:11434")]], + envVar: "AI_OLLAMA_ENDPOINT", envVarMode: EnvVarMode::OVERWRITE)] + public ?string $endpoint = null; + + #[SettingsParameter(label: new TM("settings.ai.ollama.apiKey"), + formType: APIKeyType::class, + envVar: "AI_OLLAMA_API_KEY", envVarMode: EnvVarMode::OVERWRITE)] + public ?string $apiKey = null; + + #[SettingsParameter(label: new TM("settings.ai.timeout"), + description: new TM("settings.ai.timeout.help"), + formType: NumberType::class, + formOptions: ["scale" => 0, "attr" => ["min" => 1]] + )] + #[Assert\Range(min: 1, max: AISettings::TIMEOUT_LIMIT)] + public int $timeout = 180; + + public function isAIPlatformEnabled(): bool + { + return $this->endpoint !== null && $this->endpoint !== ""; + } +} diff --git a/src/Settings/AISettings/OpenRouterSettings.php b/src/Settings/AISettings/OpenRouterSettings.php index e083513a..16665554 100644 --- a/src/Settings/AISettings/OpenRouterSettings.php +++ b/src/Settings/AISettings/OpenRouterSettings.php @@ -30,7 +30,9 @@ use Jbtronics\SettingsBundle\Metadata\EnvVarMode; use Jbtronics\SettingsBundle\Settings\Settings; use Jbtronics\SettingsBundle\Settings\SettingsParameter; use Jbtronics\SettingsBundle\Settings\SettingsTrait; +use Symfony\Component\Form\Extension\Core\Type\NumberType; use Symfony\Component\Translation\TranslatableMessage as TM; +use Symfony\Component\Validator\Constraints as Assert; #[Settings(name: 'ai_openrouter', label: new TM("settings.ai.openrouter"), description: "settings.ai.openrouter.help")] #[SettingsIcon("fa-robot")] @@ -43,6 +45,14 @@ class OpenRouterSettings implements AIPlatformSettingsInterface formOptions: ["help_html" => true], envVar: "AI_OPENROUTER_KEY", envVarMode: EnvVarMode::OVERWRITE)] public ?string $apiKey = null; + #[SettingsParameter(label: new TM("settings.ai.timeout"), + description: new TM("settings.ai.timeout.help"), + formType: NumberType::class, + formOptions: ["scale" => 0, "attr" => ["min" => 1]], + envVar: "int:AI_OPENROUTER_TIMEOUT", envVarMode: EnvVarMode::OVERWRITE)] + #[Assert\Range(min: 1, max: AISettings::TIMEOUT_LIMIT)] + public int $timeout = 90; + public function isAIPlatformEnabled(): bool { return $this->apiKey !== null && $this->apiKey !== ""; diff --git a/src/Settings/InfoProviderSystem/CanopySettings.php b/src/Settings/InfoProviderSystem/CanopySettings.php index 3c97a80e..dd36fba7 100644 --- a/src/Settings/InfoProviderSystem/CanopySettings.php +++ b/src/Settings/InfoProviderSystem/CanopySettings.php @@ -72,7 +72,7 @@ class CanopySettings /** * @var string The domain used internally for the API requests. This is not necessarily the same as the domain shown to the user, which is determined by the keys of the ALLOWED_DOMAINS constant */ - #[SettingsParameter(label: new TM("settings.ips.tme.country"), formType: ChoiceType::class, formOptions: ["choices" => self::ALLOWED_DOMAINS, 'translation_domain' => false])] + #[SettingsParameter(label: new TM("settings.ips.tme.country"), formType: ChoiceType::class, formOptions: ["choices" => self::ALLOWED_DOMAINS, 'choice_translation_domain' => false])] public string $domain = "DE"; /** diff --git a/src/State/Mcp/GetInfoProviderPartDetailsProcessor.php b/src/State/Mcp/GetInfoProviderPartDetailsProcessor.php new file mode 100644 index 00000000..c7949256 --- /dev/null +++ b/src/State/Mcp/GetInfoProviderPartDetailsProcessor.php @@ -0,0 +1,51 @@ +. + */ + +declare(strict_types=1); + +namespace App\State\Mcp; + +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\ProcessorInterface; +use App\Exceptions\InfoProviderNotActiveException; +use App\Mcp\DTO\InfoProviderPartDetailsInput; +use App\Services\InfoProviderSystem\PartInfoRetriever; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; + +class GetInfoProviderPartDetailsProcessor implements ProcessorInterface +{ + public function __construct( + private readonly PartInfoRetriever $infoRetriever, + ) { + } + + public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []) + { + if (!$data instanceof InfoProviderPartDetailsInput) { + throw new BadRequestHttpException('Expected InfoProviderPartDetailsInput'); + } + + try { + return $this->infoRetriever->getDetails($data->provider_key, $data->provider_id); + } catch (InfoProviderNotActiveException|\InvalidArgumentException $e) { + throw new BadRequestHttpException($e->getMessage(), $e); + } + } +} diff --git a/src/State/Mcp/GetPartByIdProcessor.php b/src/State/Mcp/GetPartByIdProcessor.php new file mode 100644 index 00000000..b147ef68 --- /dev/null +++ b/src/State/Mcp/GetPartByIdProcessor.php @@ -0,0 +1,60 @@ +. + */ + +declare(strict_types=1); + +namespace App\State\Mcp; + +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\ProcessorInterface; +use App\Entity\Parts\Part; +use App\Mcp\DTO\ElementByIdInput; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; + +class GetPartByIdProcessor implements ProcessorInterface +{ + public function __construct( + private readonly EntityManagerInterface $entityManager, + private readonly AuthorizationCheckerInterface $authorizationChecker, + ) { + } + + public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []) + { + if (!$data instanceof ElementByIdInput) { + throw new \InvalidArgumentException('Expected PartByIdInput'); + } + + $part = $this->entityManager->find(Part::class, $data->id); + + if (!$part instanceof Part) { + throw new NotFoundHttpException(sprintf('Part with id %d not found', $data->id)); + } + + if (!$this->authorizationChecker->isGranted('read', $part)) { + throw new AccessDeniedException(sprintf('Access denied to part with id %d', $data->id)); + } + + return $part; + } +} diff --git a/src/State/Mcp/GetStructuralElementDetailsProcessor.php b/src/State/Mcp/GetStructuralElementDetailsProcessor.php new file mode 100644 index 00000000..27cab45a --- /dev/null +++ b/src/State/Mcp/GetStructuralElementDetailsProcessor.php @@ -0,0 +1,70 @@ +. + */ + +declare(strict_types=1); + +namespace App\State\Mcp; + +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\ProcessorInterface; +use App\Entity\Base\AbstractStructuralDBElement; +use App\Mcp\DTO\ElementByIdInput; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; + +/** + * Generic get-by-id processor shared by all structural "master data" entities (categories, footprints, + * manufacturers, storage locations, suppliers, measurement units, part custom states). The concrete entity + * class is determined from the operation, so this single processor can be reused for all of them. + */ +class GetStructuralElementDetailsProcessor implements ProcessorInterface +{ + public function __construct( + private readonly EntityManagerInterface $entityManager, + private readonly AuthorizationCheckerInterface $authorizationChecker, + ) { + } + + public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): AbstractStructuralDBElement + { + if (!$data instanceof ElementByIdInput) { + throw new \InvalidArgumentException('Expected ElementByIdInput'); + } + + $class = $operation->getClass(); + if (!is_a($class, AbstractStructuralDBElement::class, true)) { + throw new \LogicException(sprintf('%s can only be used for resources extending %s', self::class, AbstractStructuralDBElement::class)); + } + + $element = $this->entityManager->find($class, $data->id); + + if (!$element instanceof AbstractStructuralDBElement) { + throw new NotFoundHttpException(sprintf('%s with id %d not found', (new \ReflectionClass($class))->getShortName(), $data->id)); + } + + if (!$this->authorizationChecker->isGranted('read', $element)) { + throw new AccessDeniedException(sprintf('Access denied to %s with id %d', (new \ReflectionClass($class))->getShortName(), $data->id)); + } + + return $element; + } +} diff --git a/src/State/Mcp/ListInfoProvidersProcessor.php b/src/State/Mcp/ListInfoProvidersProcessor.php new file mode 100644 index 00000000..6b995a15 --- /dev/null +++ b/src/State/Mcp/ListInfoProvidersProcessor.php @@ -0,0 +1,71 @@ +. + */ + +declare(strict_types=1); + +namespace App\State\Mcp; + +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\ProcessorInterface; +use ApiPlatform\State\ProviderInterface; +use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO; +use App\Services\InfoProviderSystem\ProviderRegistry; + +/** + * Used both as the state processor for the MCP list_info_providers tool and as the state provider for the + * REST GET /api/info_providers collection endpoint. + */ +class ListInfoProvidersProcessor implements ProcessorInterface, ProviderInterface +{ + public function __construct( + private readonly ProviderRegistry $providerRegistry, + ) { + } + + /** + * @return ProviderInfoDTO[] + */ + public function provide(Operation $operation, array $uriVariables = [], array $context = []): array + { + return $this->listActiveProviders(); + } + + /** + * @return ProviderInfoDTO[] + */ + public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): array + { + return $this->listActiveProviders(); + } + + /** + * @return ProviderInfoDTO[] + */ + private function listActiveProviders(): array + { + $result = []; + + foreach ($this->providerRegistry->getActiveProviders() as $provider) { + $result[] = $provider->getProviderInfo(); + } + + return $result; + } +} diff --git a/src/State/Mcp/ListStructuralElementsProcessor.php b/src/State/Mcp/ListStructuralElementsProcessor.php new file mode 100644 index 00000000..115312b4 --- /dev/null +++ b/src/State/Mcp/ListStructuralElementsProcessor.php @@ -0,0 +1,98 @@ +. + */ + +declare(strict_types=1); + +namespace App\State\Mcp; + +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\ProcessorInterface; +use App\Entity\Base\AbstractStructuralDBElement; +use App\Mcp\DTO\StructuralElementOverview; +use App\Mcp\DTO\StructuralElementSearchInput; +use App\Services\Trees\NodesListBuilder; +use Doctrine\ORM\EntityManagerInterface; + +/** + * Generic list/search processor shared by all structural "master data" entities (categories, footprints, + * manufacturers, storage locations, suppliers, measurement units, part custom states). The concrete entity + * class is determined from the operation, so this single processor can be reused for all of them. + * Returns a lean id+name+full_path overview only; use the corresponding get_X_details tool for full details. + */ +readonly class ListStructuralElementsProcessor implements ProcessorInterface +{ + public function __construct( + private EntityManagerInterface $entityManager, + private NodesListBuilder $nodesListBuilder, + ) { + } + + /** + * @return StructuralElementOverview[] + */ + public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): array + { + if (!$data instanceof StructuralElementSearchInput) { + throw new \InvalidArgumentException('Expected StructuralElementSearchInput'); + } + + $class = $operation->getClass(); + if (!is_a($class, AbstractStructuralDBElement::class, true)) { + throw new \LogicException(sprintf('%s can only be used for resources extending %s', self::class, AbstractStructuralDBElement::class)); + } + + if ($data->keyword === null || $data->keyword === '') { + //Without a filter, use the (cached) NodesListBuilder, which already returns the elements in + //correct hierarchical tree order (parents immediately followed by their own children), instead + //of fetching everything and re-sorting it ourselves. + /** @var AbstractStructuralDBElement[] $elements */ + $elements = $this->nodesListBuilder->typeToNodesList($class); + + return array_map($this->toOverview(...), $elements); + } + + $qb = $this->entityManager->getRepository($class)->createQueryBuilder('element'); + + //Escape % and _ characters in the keyword, like PartSearchFilter does + $keyword = str_replace(['%', '_'], ['\%', '\_'], $data->keyword); + $qb->andWhere('ILIKE(element.name, :keyword) = TRUE OR ILIKE(element.comment, :keyword) = TRUE') + ->setParameter('keyword', '%'.$keyword.'%'); + + /** @var AbstractStructuralDBElement[] $elements */ + $elements = $qb->getQuery()->getResult(); + + $overviews = array_map($this->toOverview(...), $elements); + + //Sort by full path (rather than the DB query), so that parents are always immediately followed by + //their own children, allowing clients to derive the hierarchical structure from a flat list. + usort($overviews, static fn (StructuralElementOverview $a, StructuralElementOverview $b): int => strnatcasecmp($a->full_path, $b->full_path)); + + return $overviews; + } + + private function toOverview(AbstractStructuralDBElement $element): StructuralElementOverview + { + return new StructuralElementOverview( + id: $element->getID(), + name: $element->getName(), + full_path: $element->getFullPath(), + ); + } +} diff --git a/src/State/Mcp/SearchInfoProvidersProcessor.php b/src/State/Mcp/SearchInfoProvidersProcessor.php new file mode 100644 index 00000000..6cd76145 --- /dev/null +++ b/src/State/Mcp/SearchInfoProvidersProcessor.php @@ -0,0 +1,90 @@ +. + */ + +declare(strict_types=1); + +namespace App\State\Mcp; + +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\ProcessorInterface; +use App\Exceptions\InfoProviderNotActiveException; +use App\Mcp\DTO\InfoProviderSearchInput; +use App\Services\InfoProviderSystem\PartInfoRetriever; +use App\Services\InfoProviderSystem\ProviderRegistry; +use App\Settings\InfoProviderSystem\InfoProviderGeneralSettings; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; + +class SearchInfoProvidersProcessor implements ProcessorInterface +{ + public function __construct( + private readonly PartInfoRetriever $infoRetriever, + private readonly ProviderRegistry $providerRegistry, + private readonly InfoProviderGeneralSettings $infoProviderSettings, + ) { + } + + public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []) + { + if (!$data instanceof InfoProviderSearchInput) { + throw new BadRequestHttpException('Expected InfoProviderSearchInput'); + } + + $providers = $this->resolveProviderKeys($data->providers); + + try { + return $this->infoRetriever->searchByKeyword($data->keyword, $providers); + } catch (InfoProviderNotActiveException|\InvalidArgumentException $e) { + throw new BadRequestHttpException($e->getMessage(), $e); + } + } + + /** + * Resolves the provider keys to search in. If none are given, falls back to the active default search + * providers configured in the system settings (mirroring the behavior of the info providers search page). + * @param string[]|null $providerKeys + * @return string[] + */ + private function resolveProviderKeys(?array $providerKeys): array + { + if (!empty($providerKeys)) { + return $providerKeys; + } + + $providers = []; + foreach ($this->infoProviderSettings->defaultSearchProviders as $providerKey) { + try { + if ($this->providerRegistry->getProviderByKey($providerKey)->isActive()) { + $providers[] = $providerKey; + } + } catch (\InvalidArgumentException) { + //Ignore providers configured as default, which do not exist (anymore) + } + } + + if ($providers === []) { + throw new BadRequestHttpException(sprintf( + 'No providers were given and no default search providers are configured or active. Available provider keys: %s', + implode(', ', array_keys($this->providerRegistry->getActiveProviders())) + )); + } + + return $providers; + } +} diff --git a/src/State/Mcp/SearchPartsProcessor.php b/src/State/Mcp/SearchPartsProcessor.php new file mode 100644 index 00000000..8d922394 --- /dev/null +++ b/src/State/Mcp/SearchPartsProcessor.php @@ -0,0 +1,80 @@ +. + */ + +declare(strict_types=1); + + +namespace App\State\Mcp; + +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\ProcessorInterface; +use App\DataTables\Filters\PartSearchFilter; +use App\Entity\Parts\Part; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\QueryBuilder; + +class SearchPartsProcessor implements ProcessorInterface +{ + + public function __construct( + private readonly EntityManagerInterface $entityManager, + ) { + + } + + public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []) + { + if (!$data instanceof PartSearchFilter) { + return []; + } + + $qb = $this->entityManager->getRepository(Part::class)->createQueryBuilder('part'); + + $data->apply($qb); + $this->addJoins($qb); + + $qb->addGroupBy('part'); + + return $qb->getQuery()->getResult(); + } + + private function addJoins(QueryBuilder $qb): void + { + $dql = $qb->getDQL(); + + if (str_contains($dql, '_category')) { + $qb->leftJoin('part.category', '_category'); + } + if (str_contains($dql, '_storelocations')) { + $qb->leftJoin('part.partLots', '_partLots'); + $qb->leftJoin('_partLots.storage_location', '_storelocations'); + } + if (str_contains($dql, '_orderdetails') || str_contains($dql, '_suppliers')) { + $qb->leftJoin('part.orderdetails', '_orderdetails'); + $qb->leftJoin('_orderdetails.supplier', '_suppliers'); + } + if (str_contains($dql, '_manufacturer')) { + $qb->leftJoin('part.manufacturer', '_manufacturer'); + } + if (str_contains($dql, '_footprint')) { + $qb->leftJoin('part.footprint', '_footprint'); + } + } +} diff --git a/src/Twig/InfoProviderExtension.php b/src/Twig/InfoProviderExtension.php index 54dbf93a..4de13115 100644 --- a/src/Twig/InfoProviderExtension.php +++ b/src/Twig/InfoProviderExtension.php @@ -59,7 +59,7 @@ final readonly class InfoProviderExtension public function getInfoProviderName(string $key): ?string { try { - return $this->providerRegistry->getProviderByKey($key)->getProviderInfo()['name']; + return $this->providerRegistry->getProviderByKey($key)->getProviderInfo()->name; } catch (\InvalidArgumentException) { return null; } diff --git a/symfony.lock b/symfony.lock index f8f88675..67d42f71 100644 --- a/symfony.lock +++ b/symfony.lock @@ -411,6 +411,18 @@ "config/packages/ai_lm_studio_platform.yaml" ] }, + "symfony/ai-ollama-platform": { + "version": "0.10", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "0.1", + "ref": "2f0ac0a8bc59c4e46b47a962a3ad7fe8104457d6" + }, + "files": [ + "config/packages/ai_ollama_platform.yaml" + ] + }, "symfony/ai-open-router-platform": { "version": "0.8", "recipe": { @@ -585,6 +597,9 @@ "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f" } }, + "symfony/mcp-bundle": { + "version": "v0.8.0" + }, "symfony/mime": { "version": "v4.3.1" }, diff --git a/templates/bundles/TwigBundle/Exception/error400.html.twig b/templates/bundles/TwigBundle/Exception/error400.html.twig new file mode 100644 index 00000000..2ad31626 --- /dev/null +++ b/templates/bundles/TwigBundle/Exception/error400.html.twig @@ -0,0 +1,43 @@ +{% extends "bundles/TwigBundle/Exception/error.html.twig" %} + +{% set untrusted_host = exception.message starts with 'Untrusted Host' or exception.message starts with 'Invalid Host' %} +{% set seen_host = untrusted_host ? (exception.message|split('"'))[1]|default(null) : null %} + +{% block status_comment %} + {% if untrusted_host %} + Part-DB was accessed using a host name that is not marked as trusted.
+ {% if seen_host %} + The host name Part-DB saw was: {{ seen_host }} + {% else %} + {{ exception.message }} + {% endif %} + {% else %} + Your browser sent a request that the server could not understand. + {% endif %} +{% endblock %} + +{% block admin_info %} + {% if untrusted_host %} + Untrusted host name.
+

For security reasons (to prevent HTTP Host header attacks), Part-DB only responds to requests using host names that were explicitly marked as trusted, once more than one host name is used to access it (e.g. behind a reverse proxy).

+

Try following things:

+
    +
  • Ensure that you are using the correct URL to access Part-DB.
  • +
  • Set the TRUSTED_HOSTS environment variable to a regular expression matching all host names Part-DB should be reachable under. Note that the required quoting differs depending on where you set it: +
      + {% if seen_host %} +
    • In your .env.local file, the value must be wrapped in single quotes: TRUSTED_HOSTS='^({{ seen_host|replace({'.': '\\.'}) }})$' (if {{ seen_host }} is a host name you trust)
    • +
    • In docker-compose.yaml, the value must NOT be quoted: TRUSTED_HOSTS=^({{ seen_host|replace({'.': '\\.'}) }})$ (if {{ seen_host }} is a host name you trust)
    • + {% else %} +
    • In your .env.local file, the value must be wrapped in single quotes: TRUSTED_HOSTS='^(localhost|example\.com)$'
    • +
    • In docker-compose.yaml, the value must NOT be quoted: TRUSTED_HOSTS=^(localhost|example\.com)$
    • + {% endif %} +
    +
  • +
  • If Part-DB is running behind a reverse proxy, also make sure that TRUSTED_PROXIES is configured correctly, so the original host name is forwarded properly.
  • +
  • Run php bin/console cache:clear after changing the configuration.
  • +
+ {% else %} + {{ parent() }} + {% endif %} +{% endblock %} diff --git a/templates/homepage.html.twig b/templates/homepage.html.twig index 2a191f14..b05c20b3 100644 --- a/templates/homepage.html.twig +++ b/templates/homepage.html.twig @@ -95,6 +95,19 @@ {% endif %} + {% if trusted_hosts_unconfigured and is_granted('@system.server_infos') %} + + {% endif %} + {% if is_granted('@system.show_updates') %} {{ nv.new_version_alert(new_version_available, new_version, new_version_url) }} {% endif %} diff --git a/templates/info_providers/providers.macro.html.twig b/templates/info_providers/providers.macro.html.twig index bec8f24b..ad03f20a 100644 --- a/templates/info_providers/providers.macro.html.twig +++ b/templates/info_providers/providers.macro.html.twig @@ -8,35 +8,41 @@
- {% if provider.providerInfo.url is defined and provider.providerInfo.url is not empty %} + {% if provider.providerInfo.url is not empty %} {{ provider.providerInfo.name }} {% else %} - {{ provider.providerInfo.name | trans }} + {{ provider.providerInfo.name }} {% endif %}
- {% if provider.providerInfo.description is defined and provider.providerInfo.description is not null %} - {{ provider.providerInfo.description | trans }} + {% if provider.providerInfo.description is not null %} + {{ provider.providerInfo.description }} {% endif %}
- {% if provider.providerInfo.settings_class is defined %} - {% endif %} - {% for capability in provider.capabilities|sort((a, b) => a.orderIndex <=> b.orderIndex) %} + {% if provider.providerInfo.expensive %} + + + {% trans %}info_providers.expensive{% endtrans %} + + {% endif %} + {% for capability in provider.providerInfo.capabilities|sort((a, b) => a.orderIndex <=> b.orderIndex) %} {# @var capability \App\Services\InfoProviderSystem\Providers\ProviderCapabilities #} {{ capability.translationKey|trans }} {% endfor %} - {% if provider.providerInfo.oauth_app_name is defined and provider.providerInfo.oauth_app_name is not empty %} + {% if provider.providerInfo.oauthAppName is not empty %}
- {% trans %}oauth_client.connect.btn{% endtrans %} + {% trans %}oauth_client.connect.btn{% endtrans %} {% endif %}
@@ -44,9 +50,9 @@
{% trans %}info_providers.providers_list.disabled{% endtrans %} - {% if provider.providerInfo.disabled_help is defined and provider.providerInfo.disabled_help is not empty %} + {% if provider.providerInfo.disabledHelp is not empty %}
- {{ provider.providerInfo.disabled_help|trans }} + {{ provider.providerInfo.disabledHelp }} {% endif %}
diff --git a/templates/info_providers/settings/provider_settings.html.twig b/templates/info_providers/settings/provider_settings.html.twig index db942f8a..1159fbf3 100644 --- a/templates/info_providers/settings/provider_settings.html.twig +++ b/templates/info_providers/settings/provider_settings.html.twig @@ -10,7 +10,7 @@ {% block card_content %}

- {% if info_provider_info.url is defined %} + {% if info_provider_info.url is not empty %} {{ info_provider_info.name }} {% else %} {{ info_provider_info.name }} diff --git a/templates/log_system/details/_extra_collection_element_deleted.html.twig b/templates/log_system/details/_extra_collection_element_deleted.html.twig index 221fae95..1c2ce2f3 100644 --- a/templates/log_system/details/_extra_collection_element_deleted.html.twig +++ b/templates/log_system/details/_extra_collection_element_deleted.html.twig @@ -4,7 +4,7 @@

{% trans %}log.collection_deleted.deleted{% endtrans %}: - {{ entity_type_label(entry.deletedElementClass) }} #{{ entry.deletedElementID }} + {{ type_label(entry.deletedElementClass) }} #{{ entry.deletedElementID }} {% if entry.oldName is not empty %} ({{ entry.oldName }}) {% endif %} @@ -12,4 +12,4 @@

{% trans %}log.collection_deleted.on_collection{% endtrans %}: {{ log_helper.translate_field(entry.collectionName) }} -

\ No newline at end of file +

diff --git a/templates/log_system/details/log_details.html.twig b/templates/log_system/details/log_details.html.twig index aff127f4..2255dd97 100644 --- a/templates/log_system/details/log_details.html.twig +++ b/templates/log_system/details/log_details.html.twig @@ -58,7 +58,7 @@ {% trans %}log.target{% endtrans %} - {{ target_html|raw }} + {{ target_html|sanitize_html }} @@ -111,7 +111,7 @@ {% elseif log_entry is instanceof('App\\Entity\\LogSystem\\CollectionElementDeleted') %} {% include "log_system/details/_extra_collection_element_deleted.html.twig" %} {% else %} - {{ extra_html | raw }} + {{ extra_html | sanitize_html }} {% endif %}

-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/mail/pw_reset.html.twig b/templates/mail/pw_reset.html.twig index 18a867d6..f9b9662c 100644 --- a/templates/mail/pw_reset.html.twig +++ b/templates/mail/pw_reset.html.twig @@ -6,9 +6,9 @@

{% trans with {'%name%': user.fullName|escape } %}email.hi %name%{% endtrans %},

{% trans %}email.pw_reset.message{% endtrans %}
- +
- {% trans with {'%url%': url('pw_reset_new_pw') } %}email.pw_reset.fallback{% endtrans %}: + {% trans with {'%url%': reset_url_fallback } %}email.pw_reset.fallback{% endtrans %}: diff --git a/templates/parts/edit/_advanced.html.twig b/templates/parts/edit/_advanced.html.twig index 30479d11..f18dba58 100644 --- a/templates/parts/edit/_advanced.html.twig +++ b/templates/parts/edit/_advanced.html.twig @@ -15,3 +15,24 @@ {{ form_row(form.partUnit) }} {{ form_row(form.partCustomState) }} {{ form_row(form.gtin) }} + +
+
+
+

+ +

+
+
+
+ {% trans %}part.edit.provider_reference.warning{% endtrans %} +
+ + {{ form_widget(form.providerReference) }} +
+
+
+
+
diff --git a/templates/parts/info/_extended_infos.html.twig b/templates/parts/info/_extended_infos.html.twig index 9cb4e4e5..acc197e3 100644 --- a/templates/parts/info/_extended_infos.html.twig +++ b/templates/parts/info/_extended_infos.html.twig @@ -76,7 +76,7 @@ {% endif %} {{ info_provider_label(part.providerReference.providerKey)|default(part.providerReference.providerKey) }}: {{ part.providerReference.providerId }} - ({{ part.providerReference.lastUpdated | format_datetime() }}) + ({{ part.providerReference.lastUpdated ? (part.providerReference.lastUpdated | format_datetime()) : ("part.info_provider_reference.updated_never"|trans) }}) {% if part.providerReference.providerUrl %} {% endif %} diff --git a/templates/parts/info/_order_infos.html.twig b/templates/parts/info/_order_infos.html.twig index 9aa9d888..ef62d6f3 100644 --- a/templates/parts/info/_order_infos.html.twig +++ b/templates/parts/info/_order_infos.html.twig @@ -46,8 +46,9 @@ {{ detail.MinDiscountQuantity | format_amount(part.partUnit) }} - {{ detail.price | format_money(detail.currency) }} / {{ detail.PriceRelatedQuantity | format_amount(part.partUnit) }} - {% set tmp = pricedetail_helper.convertMoneyToCurrency(detail.price, detail.currency, app.user.currency ?? null) %} + {% set total = detail.PricePerUnit(detail.MinDiscountQuantity) %} + {{ total | format_money(detail.currency) }} / {{ detail.MinDiscountQuantity | format_amount(part.partUnit) }} + {% set tmp = pricedetail_helper.convertMoneyToCurrency(total, detail.currency, app.user.currency ?? null) %} {% if detail.currency != (app.user.currency ?? null) and tmp is not null and tmp.GreaterThan(0) %} ({{ tmp | format_money(app.user.currency ?? null) }}) {% endif %} diff --git a/templates/parts/info/_picture.html.twig b/templates/parts/info/_picture.html.twig index e6aa74b3..db6c59ca 100644 --- a/templates/parts/info/_picture.html.twig +++ b/templates/parts/info/_picture.html.twig @@ -19,7 +19,7 @@ {% endif %} @@ -41,4 +41,4 @@ {% else %} Part main image -{% endif %} \ No newline at end of file +{% endif %} diff --git a/templates/parts/info/show_part_info.html.twig b/templates/parts/info/show_part_info.html.twig index 96b5e209..b36ab047 100644 --- a/templates/parts/info/show_part_info.html.twig +++ b/templates/parts/info/show_part_info.html.twig @@ -22,7 +22,7 @@ ({{ timeTravel | format_datetime('short') }}) {% endif %} {% if part.projectBuildPart %} - ({{ entity_type_label(part.builtProject) }}: {{ part.builtProject.name }}) + ({{ type_label(part.builtProject) }}: {{ part.builtProject.name }}) {% endif %}
diff --git a/templates/projects/_bom_validation_results.html.twig b/templates/projects/_bom_validation_results.html.twig index 68f1b827..cb92e7bc 100644 --- a/templates/projects/_bom_validation_results.html.twig +++ b/templates/projects/_bom_validation_results.html.twig @@ -68,7 +68,7 @@

{% trans %}project.bom_import.validation.errors.description{% endtrans %}

    {% for error in validation_result.errors %} -
  • {{ error|raw }}
  • +
  • {{ error }}
  • {% endfor %}
@@ -80,7 +80,7 @@

{% trans %}project.bom_import.validation.warnings.description{% endtrans %}

    {% for warning in validation_result.warnings %} -
  • {{ warning|raw }}
  • +
  • {{ warning }}
  • {% endfor %}
@@ -91,7 +91,7 @@

{% trans %}project.bom_import.validation.info.title{% endtrans %}

    {% for info in validation_result.info %} -
  • {{ info|raw }}
  • +
  • {{ info }}
  • {% endfor %}
@@ -139,21 +139,21 @@ {% if line_result.errors is not empty %}
{% for error in line_result.errors %} -
{{ error|raw }}
+
{{ error }}
{% endfor %}
{% endif %} {% if line_result.warnings is not empty %}
{% for warning in line_result.warnings %} -
{{ warning|raw }}
+
{{ warning }}
{% endfor %}
{% endif %} {% if line_result.info is not empty %}
{% for info in line_result.info %} -
{{ info|raw }}
+
{{ info }}
{% endfor %}
{% endif %} diff --git a/templates/projects/import_bom_map_fields.html.twig b/templates/projects/import_bom_map_fields.html.twig index ee1e23ef..b272be64 100644 --- a/templates/projects/import_bom_map_fields.html.twig +++ b/templates/projects/import_bom_map_fields.html.twig @@ -132,8 +132,8 @@