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..8cd39f31 100644 --- a/.env +++ b/.env @@ -149,6 +149,16 @@ DISABLE_YEAR2038_BUG_CHECK=0 #TRUSTED_PROXIES=127.0.0.0/8,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 #TRUSTED_HOSTS='^(localhost|example\.com)$' +################################################################################### +# Logging settings +################################################################################### + +# The minimum level a deprecation notice must have to be written to the var/log/_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..5da304ec 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 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..cbc42d27 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 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5b756228..66086104 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 @@ -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..94f15e9c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.12.1 +2.13.1 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 547742ea..1c6ed09b 100644 --- a/assets/js/register_events.js +++ b/assets/js/register_events.js @@ -19,15 +19,14 @@ 'use strict'; -import {Dropdown} from "bootstrap"; +import {Dropdown, Modal, Tooltip} from "bootstrap"; import ClipboardJS from "clipboard"; -import {Modal} from "bootstrap"; class RegisterEventHelper { constructor() { this.registerTooltips(); this.configureDropdowns(); - + // Only register special character input if enabled in configuration const keybindingsEnabled = document.body.dataset.keybindingsSpecialCharacters !== 'false'; if (keybindingsEnabled) { @@ -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]') - //@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]'; + 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,242 +98,240 @@ 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; - }) + }); } } -export default new RegisterEventHelper(); \ No newline at end of file +export default new RegisterEventHelper(); 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..10c9b702 100644 --- a/composer.json +++ b/composer.json @@ -57,9 +57,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.10.0", + "symfony/ai-lm-studio-platform": "^v0.10.0", + "symfony/ai-ollama-platform": "^0.10.0", + "symfony/ai-open-router-platform": "^0.10.0", "symfony/apache-pack": "^1.0", "symfony/asset": "7.4.*", "symfony/console": "7.4.*", @@ -70,6 +71,7 @@ "symfony/flex": "^v2.3.1", "symfony/form": "7.4.*", "symfony/framework-bundle": "7.4.*", + "symfony/html-sanitizer": "7.4.*", "symfony/http-client": "7.4.*", "symfony/http-kernel": "7.4.*", "symfony/mailer": "7.4.*", diff --git a/composer.lock b/composer.lock index 1c2f7dda..dc8dc699 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": "6a79fa73091c2e15bce035c85c2d61ed", "packages": [ { "name": "amphp/amp", - "version": "v3.1.1", + "version": "v3.1.2", "source": { "type": "git", "url": "https://github.com/amphp/amp.git", - "reference": "fa0ab33a6f47a82929c38d03ca47ebb71086a93f" + "reference": "2f3ebed5a4f663968a0590dbb7654a8b32cb63cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/amp/zipball/fa0ab33a6f47a82929c38d03ca47ebb71086a93f", - "reference": "fa0ab33a6f47a82929c38d03ca47ebb71086a93f", + "url": "https://api.github.com/repos/amphp/amp/zipball/2f3ebed5a4f663968a0590dbb7654a8b32cb63cb", + "reference": "2f3ebed5a4f663968a0590dbb7654a8b32cb63cb", "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.2" }, "funding": [ { @@ -85,7 +85,7 @@ "type": "github" } ], - "time": "2025-08-27T21:42:00+00:00" + "time": "2026-06-21T13:59:44+00:00" }, { "name": "amphp/byte-stream", @@ -615,16 +615,16 @@ }, { "name": "amphp/pipeline", - "version": "v1.2.4", + "version": "v1.2.5", "source": { "type": "git", "url": "https://github.com/amphp/pipeline.git", - "reference": "a044733e080940d1483f56caff0c412ad6982776" + "reference": "92f121dde31cd1d89d5d0f9eba64ac40271b236e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/pipeline/zipball/a044733e080940d1483f56caff0c412ad6982776", - "reference": "a044733e080940d1483f56caff0c412ad6982776", + "url": "https://api.github.com/repos/amphp/pipeline/zipball/92f121dde31cd1d89d5d0f9eba64ac40271b236e", + "reference": "92f121dde31cd1d89d5d0f9eba64ac40271b236e", "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.5" }, "funding": [ { @@ -678,7 +678,7 @@ "type": "github" } ], - "time": "2026-05-06T05:37:57+00:00" + "time": "2026-06-27T14:17:20+00:00" }, { "name": "amphp/process", @@ -976,16 +976,16 @@ }, { "name": "api-platform/doctrine-common", - "version": "v4.3.10", + "version": "v4.3.15", "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.15" }, - "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.15", "source": { "type": "git", "url": "https://github.com/api-platform/doctrine-orm.git", - "reference": "db75bac977cc9fe76cee90c1e5361c744cfa2144" + "reference": "6af3eeefc7d483b83e56bcbdbff0dd0dde3c7fc1" }, "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/6af3eeefc7d483b83e56bcbdbff0dd0dde3c7fc1", + "reference": "6af3eeefc7d483b83e56bcbdbff0dd0dde3c7fc1", "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.15" }, - "time": "2026-06-04T13:58:17+00:00" + "time": "2026-06-16T13:14:05+00:00" }, { "name": "api-platform/documentation", - "version": "v4.3.10", + "version": "v4.3.15", "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.15", "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.15", "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.15" }, - "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.15", "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.15" }, - "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.15", "source": { "type": "git", "url": "https://github.com/api-platform/json-schema.git", - "reference": "6a0a2ddc4914bb1fdb1b71777c49aef388f6ea31" + "reference": "2136965a30643056d09b10ae5e9fab1f7232a028" }, "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/2136965a30643056d09b10ae5e9fab1f7232a028", + "reference": "2136965a30643056d09b10ae5e9fab1f7232a028", "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.4.0-alpha.1" }, - "time": "2026-06-03T14:46:27+00:00" + "time": "2026-06-13T05:06:55+00:00" }, { "name": "api-platform/jsonld", - "version": "v4.3.10", + "version": "v4.3.15", "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,22 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/jsonld/tree/v4.3.10" + "source": "https://github.com/api-platform/jsonld/tree/v4.3.15" }, - "time": "2026-04-30T12:21:24+00:00" + "time": "2026-06-13T05:11:46+00:00" }, { "name": "api-platform/metadata", - "version": "v4.3.10", + "version": "v4.3.15", "source": { "type": "git", "url": "https://github.com/api-platform/metadata.git", - "reference": "b835ce534861f9e7b2a316fa582f28afe4ad5fe6" + "reference": "86efa5375e994f4e46f3e4336dc4dc5ecc9456a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/metadata/zipball/b835ce534861f9e7b2a316fa582f28afe4ad5fe6", - "reference": "b835ce534861f9e7b2a316fa582f28afe4ad5fe6", + "url": "https://api.github.com/repos/api-platform/metadata/zipball/86efa5375e994f4e46f3e4336dc4dc5ecc9456a8", + "reference": "86efa5375e994f4e46f3e4336dc4dc5ecc9456a8", "shasum": "" }, "require": { @@ -1720,22 +1720,22 @@ "swagger" ], "support": { - "source": "https://github.com/api-platform/metadata/tree/v4.3.10" + "source": "https://github.com/api-platform/metadata/tree/v4.3.15" }, - "time": "2026-06-05T09:05:29+00:00" + "time": "2026-06-13T05:03:21+00:00" }, { "name": "api-platform/openapi", - "version": "v4.3.10", + "version": "v4.3.15", "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 +1753,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 +1811,22 @@ "swagger" ], "support": { - "source": "https://github.com/api-platform/openapi/tree/v4.3.10" + "source": "https://github.com/api-platform/openapi/tree/v4.3.15" }, - "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.15", "source": { "type": "git", "url": "https://github.com/api-platform/serializer.git", - "reference": "04d56abff2910390111613716cd75796f23e34d5" + "reference": "4c8f7507a13f8a9c0cace60efc245fece2259233" }, "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/4c8f7507a13f8a9c0cace60efc245fece2259233", + "reference": "4c8f7507a13f8a9c0cace60efc245fece2259233", "shasum": "" }, "require": { @@ -1905,22 +1905,22 @@ "serializer" ], "support": { - "source": "https://github.com/api-platform/serializer/tree/v4.3.10" + "source": "https://github.com/api-platform/serializer/tree/v4.3.15" }, - "time": "2026-06-05T12:07:51+00:00" + "time": "2026-06-26T09:59:36+00:00" }, { "name": "api-platform/state", - "version": "v4.3.10", + "version": "v4.3.15", "source": { "type": "git", "url": "https://github.com/api-platform/state.git", - "reference": "06067bf0efae09badc618fac8add67191f12f1e3" + "reference": "3bb76df0857dd1a6e706dd1c5c4b574432a2fa8d" }, "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/3bb76df0857dd1a6e706dd1c5c4b574432a2fa8d", + "reference": "3bb76df0857dd1a6e706dd1c5c4b574432a2fa8d", "shasum": "" }, "require": { @@ -1933,7 +1933,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 +2002,22 @@ "swagger" ], "support": { - "source": "https://github.com/api-platform/state/tree/v4.3.10" + "source": "https://github.com/api-platform/state/tree/v4.3.15" }, - "time": "2026-06-05T15:16:47+00:00" + "time": "2026-06-13T05:11:46+00:00" }, { "name": "api-platform/symfony", - "version": "v4.3.10", + "version": "v4.3.15", "source": { "type": "git", "url": "https://github.com/api-platform/symfony.git", - "reference": "70a1a4468d77c9761ac1a6cba681277fa986c39b" + "reference": "8b20ef6262b2d557e2c286df48b78e66e7f8c000" }, "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/8b20ef6262b2d557e2c286df48b78e66e7f8c000", + "reference": "8b20ef6262b2d557e2c286df48b78e66e7f8c000", "shasum": "" }, "require": { @@ -2028,7 +2028,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 +2131,13 @@ "symfony" ], "support": { - "source": "https://github.com/api-platform/symfony/tree/v4.3.10" + "source": "https://github.com/api-platform/symfony/tree/v4.3.15" }, - "time": "2026-06-05T15:17:34+00:00" + "time": "2026-06-17T18:14:46+00:00" }, { "name": "api-platform/validator", - "version": "v4.3.10", + "version": "v4.3.15", "source": { "type": "git", "url": "https://github.com/api-platform/validator.git", @@ -2207,22 +2207,22 @@ "validator" ], "support": { - "source": "https://github.com/api-platform/validator/tree/v4.3.10" + "source": "https://github.com/api-platform/validator/tree/v4.3.15" }, "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 +2274,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", @@ -2658,28 +2658,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 +2718,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 +2728,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", @@ -3349,16 +3346,16 @@ }, { "name": "doctrine/doctrine-bundle", - "version": "2.18.2", + "version": "2.18.3", "source": { "type": "git", "url": "https://github.com/doctrine/DoctrineBundle.git", - "reference": "0ff098b29b8b3c68307c8987dcaed7fd829c6546" + "reference": "241d61f6bbc77275d5a9f95d514241c058bf2d0a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/0ff098b29b8b3c68307c8987dcaed7fd829c6546", - "reference": "0ff098b29b8b3c68307c8987dcaed7fd829c6546", + "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/241d61f6bbc77275d5a9f95d514241c058bf2d0a", + "reference": "241d61f6bbc77275d5a9f95d514241c058bf2d0a", "shasum": "" }, "require": { @@ -3395,6 +3392,7 @@ "psr/log": "^1.1.4 || ^2.0 || ^3.0", "symfony/doctrine-messenger": "^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/security-bundle": "^6.4 || ^7.0", @@ -3450,7 +3448,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.18.3" }, "funding": [ { @@ -3466,7 +3464,7 @@ "type": "tidelift" } ], - "time": "2025-12-20T21:35:32+00:00" + "time": "2026-06-08T08:22:50+00:00" }, { "name": "doctrine/doctrine-migrations-bundle", @@ -4637,26 +4635,26 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.11.0", + "version": "7.12.3", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "c987f8ce84b8434fa430795eca0f3430663da72b" + "reference": "9aa17bcdd777ee31df9fc83c337ca4ca2340def3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/c987f8ce84b8434fa430795eca0f3430663da72b", - "reference": "c987f8ce84b8434fa430795eca0f3430663da72b", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/9aa17bcdd777ee31df9fc83c337ca4ca2340def3", + "reference": "9aa17bcdd777ee31df9fc83c337ca4ca2340def3", "shasum": "" }, "require": { "ext-json": "*", "guzzlehttp/promises": "^2.5", - "guzzlehttp/psr7": "^2.11", + "guzzlehttp/psr7": "^2.12.3", "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" @@ -4665,7 +4663,7 @@ "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", "guzzle/client-integration-tests": "3.0.2", - "guzzlehttp/test-server": "^0.4", + "guzzlehttp/test-server": "^0.5.1", "php-http/message-factory": "^1.1", "phpunit/phpunit": "^8.5.52 || ^9.6.34", "psr/log": "^1.1 || ^2.0 || ^3.0" @@ -4745,7 +4743,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.12.3" }, "funding": [ { @@ -4761,7 +4759,7 @@ "type": "tidelift" } ], - "time": "2026-06-02T12:40:51+00:00" + "time": "2026-06-23T15:29:02+00:00" }, { "name": "guzzlehttp/promises", @@ -4849,16 +4847,16 @@ }, { "name": "guzzlehttp/psr7", - "version": "2.11.0", + "version": "2.12.3", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "bbb5e61349fa5cb822b3e87842b951088b76b81f" + "reference": "7ec62dc3f44aa218487dbed81a9bf9bc647be55d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/bbb5e61349fa5cb822b3e87842b951088b76b81f", - "reference": "bbb5e61349fa5cb822b3e87842b951088b76b81f", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/7ec62dc3f44aa218487dbed81a9bf9bc647be55d", + "reference": "7ec62dc3f44aa218487dbed81a9bf9bc647be55d", "shasum": "" }, "require": { @@ -4867,7 +4865,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 +4946,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.12.3" }, "funding": [ { @@ -4964,7 +4962,7 @@ "type": "tidelift" } ], - "time": "2026-06-02T12:30:48+00:00" + "time": "2026-06-23T15:21:08+00:00" }, { "name": "hshn/base64-encoded-file", @@ -5364,16 +5362,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 +5416,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 +5424,7 @@ "type": "custom" } ], - "time": "2024-03-10T17:40:29+00:00" + "time": "2026-06-22T13:08:56+00:00" }, { "name": "jfcherng/php-mb-string", @@ -7008,16 +7006,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 +7023,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 +7067,9 @@ ], "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": "mf2/mf2", @@ -9999,16 +9997,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 +10017,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 +10033,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "9.4.x-dev" + "dev-main": "9.5.x-dev" } }, "autoload": { @@ -10073,9 +10071,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 +10138,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 +10181,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 +10249,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 +10302,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 +10353,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", @@ -10735,21 +10733,21 @@ }, { "name": "symfony/ai-bundle", - "version": "v0.9.0", + "version": "v0.10.0", "source": { "type": "git", "url": "https://github.com/symfony/ai-bundle.git", - "reference": "77fd1b513174770acf49abd68effa995fa518f7c" + "reference": "5f6d218ca26a4ac3c2b743e4bfae769c41c556c0" }, "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/5f6d218ca26a4ac3c2b743e4bfae769c41c556c0", + "reference": "5f6d218ca26a4ac3c2b743e4bfae769c41c556c0", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/ai-platform": "^0.9", + "symfony/ai-platform": "^0.10", "symfony/clock": "^7.3|^8.0", "symfony/config": "^7.3|^8.0", "symfony/console": "^7.3|^8.0", @@ -10764,74 +10762,74 @@ "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.10", + "symfony/ai-ai-ml-api-platform": "^0.10", + "symfony/ai-albert-platform": "^0.10", + "symfony/ai-amazee-ai-platform": "^0.10", + "symfony/ai-anthropic-platform": "^0.10", + "symfony/ai-azure-platform": "^0.10", + "symfony/ai-azure-search-store": "^0.10", + "symfony/ai-bedrock-platform": "^0.10", + "symfony/ai-cache-message-store": "^0.10", + "symfony/ai-cache-platform": "^0.10", + "symfony/ai-cache-store": "^0.10", + "symfony/ai-cartesia-platform": "^0.10", + "symfony/ai-cerebras-platform": "^0.10", + "symfony/ai-chat": "^0.10", + "symfony/ai-chroma-db-store": "^0.10", + "symfony/ai-click-house-store": "^0.10", + "symfony/ai-cloudflare-message-store": "^0.10", + "symfony/ai-cloudflare-store": "^0.10", + "symfony/ai-cohere-platform": "^0.10", + "symfony/ai-decart-platform": "^0.10", + "symfony/ai-deep-seek-platform": "^0.10", + "symfony/ai-docker-model-runner-platform": "^0.10", + "symfony/ai-doctrine-message-store": "^0.10", + "symfony/ai-elasticsearch-store": "^0.10", + "symfony/ai-eleven-labs-platform": "^0.10", + "symfony/ai-failover-platform": "^0.10", + "symfony/ai-gemini-platform": "^0.10", + "symfony/ai-generic-platform": "^0.10", + "symfony/ai-hugging-face-platform": "^0.10", + "symfony/ai-lm-studio-platform": "^0.10", + "symfony/ai-manticore-search-store": "^0.10", + "symfony/ai-maria-db-store": "^0.10", + "symfony/ai-meilisearch-message-store": "^0.10", + "symfony/ai-meilisearch-store": "^0.10", + "symfony/ai-meta-platform": "^0.10", + "symfony/ai-milvus-store": "^0.10", + "symfony/ai-mistral-platform": "^0.10", + "symfony/ai-mongo-db-message-store": "^0.10", + "symfony/ai-mongo-db-store": "^0.10", + "symfony/ai-neo4j-store": "^0.10", + "symfony/ai-ollama-platform": "^0.10", + "symfony/ai-open-ai-platform": "^0.10", + "symfony/ai-open-responses-platform": "^0.10", + "symfony/ai-open-router-platform": "^0.10", + "symfony/ai-open-search-store": "^0.10", + "symfony/ai-ovh-platform": "^0.10", + "symfony/ai-perplexity-platform": "^0.10", + "symfony/ai-pinecone-store": "^0.10", + "symfony/ai-pogocache-message-store": "^0.10", + "symfony/ai-postgres-store": "^0.10", + "symfony/ai-qdrant-store": "^0.10", + "symfony/ai-redis-message-store": "^0.10", + "symfony/ai-redis-store": "^0.10", + "symfony/ai-replicate-platform": "^0.10", + "symfony/ai-s3vectors-store": "^0.10", + "symfony/ai-scaleway-platform": "^0.10", + "symfony/ai-session-message-store": "^0.10", + "symfony/ai-sqlite-store": "^0.10", + "symfony/ai-store": "^0.10", + "symfony/ai-supabase-store": "^0.10", + "symfony/ai-surreal-db-message-store": "^0.10", + "symfony/ai-surreal-db-store": "^0.10", + "symfony/ai-transformers-php-platform": "^0.10", + "symfony/ai-typesense-store": "^0.10", + "symfony/ai-vektor-store": "^0.10", + "symfony/ai-vertex-ai-platform": "^0.10", + "symfony/ai-voyage-platform": "^0.10", + "symfony/ai-weaviate-store": "^0.10", "symfony/expression-language": "^7.3|^8.0", "symfony/security-core": "^7.3|^8.0", "symfony/translation": "^7.3|^8.0", @@ -10869,7 +10867,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.10.0" }, "funding": [ { @@ -10889,25 +10887,25 @@ "type": "tidelift" } ], - "time": "2026-05-16T08:40:45+00:00" + "time": "2026-06-16T07:10:08+00:00" }, { "name": "symfony/ai-generic-platform", - "version": "v0.9.0", + "version": "v0.10.0", "source": { "type": "git", "url": "https://github.com/symfony/ai-generic-platform.git", - "reference": "8887d12b8ea97d079c5c97de4aebb19f42c58dc5" + "reference": "a099d8a35ea9f7254a159b9c17bc7a4927f9ebb2" }, "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/a099d8a35ea9f7254a159b9c17bc7a4927f9ebb2", + "reference": "a099d8a35ea9f7254a159b9c17bc7a4927f9ebb2", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/ai-platform": "^0.9", + "symfony/ai-platform": "^0.10", "symfony/http-client": "^7.3|^8.0" }, "require-dev": { @@ -10954,7 +10952,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.10.0" }, "funding": [ { @@ -10974,26 +10972,26 @@ "type": "tidelift" } ], - "time": "2026-05-16T01:01:33+00:00" + "time": "2026-06-16T07:10:08+00:00" }, { "name": "symfony/ai-lm-studio-platform", - "version": "v0.9.0", + "version": "v0.10.0", "source": { "type": "git", "url": "https://github.com/symfony/ai-lm-studio-platform.git", - "reference": "9e53e56c8c3a04dddb955088b40904e747ec3981" + "reference": "b6c4a3a5bedf2fd613953447602945c104e75a30" }, "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/b6c4a3a5bedf2fd613953447602945c104e75a30", + "reference": "b6c4a3a5bedf2fd613953447602945c104e75a30", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/ai-generic-platform": "^0.9", - "symfony/ai-platform": "^0.9", + "symfony/ai-generic-platform": "^0.10", + "symfony/ai-platform": "^0.10", "symfony/http-client": "^7.3|^8.0" }, "require-dev": { @@ -11041,7 +11039,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.10.0" }, "funding": [ { @@ -11061,26 +11059,112 @@ "type": "tidelift" } ], - "time": "2026-05-16T01:01:33+00:00" + "time": "2026-06-15T22:48:31+00:00" }, { - "name": "symfony/ai-open-router-platform", - "version": "v0.9.0", + "name": "symfony/ai-ollama-platform", + "version": "v0.10.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": "1542f19b78362cafc034c219f5bc9a5a239a0ffb" }, "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/1542f19b78362cafc034c219f5bc9a5a239a0ffb", + "reference": "1542f19b78362cafc034c219f5bc9a5a239a0ffb", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/ai-generic-platform": "^0.9", - "symfony/ai-platform": "^0.9", + "symfony/ai-platform": "^0.10", + "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.10.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-06-15T22:48:31+00:00" + }, + { + "name": "symfony/ai-open-router-platform", + "version": "v0.10.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/ai-open-router-platform.git", + "reference": "cfadb7858fca98b28b968b032b1bbeb5c9cc985b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/ai-open-router-platform/zipball/cfadb7858fca98b28b968b032b1bbeb5c9cc985b", + "reference": "cfadb7858fca98b28b968b032b1bbeb5c9cc985b", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/ai-generic-platform": "^0.10", + "symfony/ai-platform": "^0.10", "symfony/http-client": "^7.3|^8.0" }, "require-dev": { @@ -11128,7 +11212,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.10.0" }, "funding": [ { @@ -11148,20 +11232,20 @@ "type": "tidelift" } ], - "time": "2026-05-16T01:01:33+00:00" + "time": "2026-06-16T08:09:36+00:00" }, { "name": "symfony/ai-platform", - "version": "v0.9.0", + "version": "v0.10.0", "source": { "type": "git", "url": "https://github.com/symfony/ai-platform.git", - "reference": "fb55ebdf20bbe30af6752a0ce6a25abc56b2b625" + "reference": "8100507aa9c46f3ad56d0272e8e46b558f451052" }, "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/8100507aa9c46f3ad56d0272e8e46b558f451052", + "reference": "8100507aa9c46f3ad56d0272e8e46b558f451052", "shasum": "" }, "require": { @@ -11260,7 +11344,7 @@ "voyage" ], "support": { - "source": "https://github.com/symfony/ai-platform/tree/v0.9.0" + "source": "https://github.com/symfony/ai-platform/tree/v0.10.0" }, "funding": [ { @@ -11280,7 +11364,7 @@ "type": "tidelift" } ], - "time": "2026-05-15T19:15:50+00:00" + "time": "2026-06-16T06:39:24+00:00" }, { "name": "symfony/apache-pack", @@ -11383,16 +11467,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 +11547,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 +11567,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 +11627,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 +11647,7 @@ "type": "tidelift" } ], - "time": "2026-05-05T15:33:14+00:00" + "time": "2026-06-05T06:23:12+00:00" }, { "name": "symfony/clock", @@ -11645,16 +11729,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 +11784,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 +11804,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 +11882,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 +11902,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 +11975,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 +12035,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 +12055,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 +12106,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 +12126,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 +12219,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 +12239,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 +12315,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 +12369,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 +12389,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 +12451,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 +12471,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 +12536,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 +12556,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 +12616,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 +12636,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 +12684,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 +12704,7 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-06-08T20:24:16+00:00" }, { "name": "symfony/filesystem", @@ -12694,16 +12778,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 +12822,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 +12842,7 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-06-27T08:31:18+00:00" }, { "name": "symfony/flex", @@ -12835,16 +12919,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 +12998,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 +13018,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 +13081,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 +13125,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 +13156,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 +13176,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 +13331,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 +13351,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 +13413,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 +13433,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 +13495,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 +13515,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 +13614,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 +13634,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 +13704,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 +13724,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 +13788,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 +13808,7 @@ "type": "tidelift" } ], - "time": "2026-05-20T07:20:23+00:00" + "time": "2026-06-13T08:51:35+00:00" }, { "name": "symfony/mime", @@ -14477,16 +14635,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 +14691,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 +14711,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 +15282,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 +15332,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 +15352,7 @@ "type": "tidelift" } ], - "time": "2026-05-23T16:05:06+00:00" + "time": "2026-06-08T20:24:16+00:00" }, { "name": "symfony/routing", @@ -15283,16 +15441,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 +15458,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 +15502,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 +15522,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 +15614,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 +15634,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 +15705,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 +15725,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 +15803,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 +15871,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 +15891,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 +15975,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 +15995,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 +16062,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 +16082,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 +16316,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 +16392,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 +16412,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 +16474,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 +16494,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 +16589,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 +16609,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 +16679,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 +16699,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 +17048,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 +17128,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 +17148,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 +17215,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 +17235,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 +17296,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 +17316,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 +17407,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 +17459,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 +17479,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 +17535,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 +17555,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.3", "source": { "type": "git", - "url": "https://github.com/easy-coding-standard/ecs.git", - "reference": "96294d652c17ccffabb7d36f9d116dd5f0e6b84e" + "url": "https://github.com/ecsphp/ecs.git", + "reference": "94f56bce0420d4e837a85c4b2c6501293a5974eb" }, "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/94f56bce0420d4e837a85c4b2c6501293a5974eb", + "reference": "94f56bce0420d4e837a85c4b2c6501293a5974eb", "shasum": "" }, "require": { @@ -17444,22 +17603,22 @@ "static analysis" ], "support": { - "source": "https://github.com/easy-coding-standard/ecs/tree/13.1.5" + "source": "https://github.com/ecsphp/ecs/tree/13.2.3" }, - "time": "2026-05-30T09:12:26+00:00" + "time": "2026-06-15T22:08:41+00:00" }, { "name": "tecnickcom/tc-lib-barcode", - "version": "2.7.0", + "version": "2.11.1", "source": { "type": "git", "url": "https://github.com/tecnickcom/tc-lib-barcode.git", - "reference": "4e53047a4ba4ed592ae677b3729ce9bfeae1cfbb" + "reference": "69238f94a1e46332ebc057fddf5bba2f776d56f6" }, "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/69238f94a1e46332ebc057fddf5bba2f776d56f6", + "reference": "69238f94a1e46332ebc057fddf5bba2f776d56f6", "shasum": "" }, "require": { @@ -17468,12 +17627,11 @@ "ext-gd": "*", "ext-pcre": "*", "php": ">=8.2", - "tecnickcom/tc-lib-color": "^2.7" + "tecnickcom/tc-lib-color": "^2.12" }, "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,20 +17703,20 @@ "type": "github" } ], - "time": "2026-05-22T07:09:18+00:00" + "time": "2026-06-26T10:37:56+00:00" }, { "name": "tecnickcom/tc-lib-color", - "version": "2.8.0", + "version": "2.12.2", "source": { "type": "git", "url": "https://github.com/tecnickcom/tc-lib-color.git", - "reference": "6947cc9fffe23a21642279b8ab73a43f3311c5f9" + "reference": "ba7d43d9bb06946c9717a4e3c6aa73346266cdbf" }, "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/ba7d43d9bb06946c9717a4e3c6aa73346266cdbf", + "reference": "ba7d43d9bb06946c9717a4e3c6aa73346266cdbf", "shasum": "" }, "require": { @@ -17567,8 +17725,7 @@ }, "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 +17770,7 @@ "type": "github" } ], - "time": "2026-05-22T06:55:57+00:00" + "time": "2026-06-26T10:26:00+00:00" }, { "name": "thecodingmachine/safe", @@ -18730,16 +18887,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 +18947,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", @@ -19493,21 +19650,21 @@ }, { "name": "phpstan/phpstan-doctrine", - "version": "2.0.25", + "version": "2.0.27", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-doctrine.git", - "reference": "e20e8bf3223ae6eba9c4b5987c391d922e094b3c" + "reference": "39b4ca45a07cdd6366eeefa2f7a993cddf3b9f9f" }, "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/39b4ca45a07cdd6366eeefa2f7a993cddf3b9f9f", + "reference": "39b4ca45a07cdd6366eeefa2f7a993cddf3b9f9f", "shasum": "" }, "require": { "php": "^7.4 || ^8.0", - "phpstan/phpstan": "^2.1.34" + "phpstan/phpstan": "^2.2.2" }, "conflict": { "doctrine/collections": "<1.0", @@ -19564,9 +19721,9 @@ ], "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.27" }, - "time": "2026-06-02T20:27:36+00:00" + "time": "2026-06-10T10:39:35+00:00" }, { "name": "phpstan/phpstan-strict-rules", @@ -19621,16 +19778,16 @@ }, { "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 +19846,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", @@ -20152,21 +20309,21 @@ }, { "name": "rector/rector", - "version": "2.4.5", + "version": "2.5.2", "source": { "type": "git", "url": "https://github.com/rectorphp/rector.git", - "reference": "cbd86024be5014d3c14d9f0b3f7aae8ecbffd62c" + "reference": "49ff6339174bdbdf50b0b35ecbcff14a05ac9e24" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/cbd86024be5014d3c14d9f0b3f7aae8ecbffd62c", - "reference": "cbd86024be5014d3c14d9f0b3f7aae8ecbffd62c", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/49ff6339174bdbdf50b0b35ecbcff14a05ac9e24", + "reference": "49ff6339174bdbdf50b0b35ecbcff14a05ac9e24", "shasum": "" }, "require": { "php": "^7.4|^8.0", - "phpstan/phpstan": "^2.1.56" + "phpstan/phpstan": "^2.2.2" }, "conflict": { "rector/rector-doctrine": "*", @@ -20200,7 +20357,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.2" }, "funding": [ { @@ -20208,7 +20365,7 @@ "type": "github" } ], - "time": "2026-05-26T21:03:22+00:00" + "time": "2026-06-22T11:39:33+00:00" }, { "name": "roave/security-advisories", @@ -20216,12 +20373,12 @@ "source": { "type": "git", "url": "https://github.com/Roave/SecurityAdvisories.git", - "reference": "b000295a5fd02609ee7ad31d084cfa904af8a69a" + "reference": "36ba91e82e1b493faef2c13277d6bd2669ea9f31" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/b000295a5fd02609ee7ad31d084cfa904af8a69a", - "reference": "b000295a5fd02609ee7ad31d084cfa904af8a69a", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/36ba91e82e1b493faef2c13277d6bd2669ea9f31", + "reference": "36ba91e82e1b493faef2c13277d6bd2669ea9f31", "shasum": "" }, "conflict": { @@ -20238,6 +20395,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", @@ -20322,13 +20480,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", @@ -20346,7 +20505,7 @@ "code16/sharp": "<9.22", "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", @@ -20354,7 +20513,7 @@ "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", + "concrete5/concrete5": "<9.5.1", "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", @@ -20366,12 +20525,13 @@ "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", @@ -20426,7 +20586,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", @@ -20497,10 +20657,11 @@ "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 +20707,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 +20725,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.12.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.1", "haffner/jh_captcha": "<=2.1.3|>=3,<=3.0.2", "handcraftedinthealps/goodby-csv": "<1.4.3", "harvesthq/chosen": "<1.8.7", @@ -20611,7 +20773,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 +20785,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", @@ -20666,7 +20829,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", @@ -20757,6 +20920,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", @@ -20826,6 +20990,7 @@ "paragonie/random_compat": "<2", "paragonie/sodium_compat": "<1.24|>=2,<2.5", "passbolt/passbolt_api": "<4.6.2", + "paymenter/paymenter": "<1.5", "paypal/adaptivepayments-sdk-php": "<=3.9.2", "paypal/invoice-sdk-php": "<=3.9", "paypal/merchant-sdk-php": "<3.12", @@ -20839,22 +21004,25 @@ "personnummer/personnummer": "<3.0.2", "ph7software/ph7builder": "<=17.9.1", "phanan/koel": "<=9.3.4", + "pheditor/pheditor": ">=2.0.1,<=2.0.3", "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.4|>=2,<=2.1.15|>=2.2,<=2.4.4|>=3,<=3.10.4|>=4,<=5.6", "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", @@ -20870,7 +21038,7 @@ "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", "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 +21046,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.4|>=4.3,<4.3.3", "pressbooks/pressbooks": "<5.18", "prestashop/autoupgrade": ">=4,<4.10.1", "prestashop/blockreassurance": "<=5.1.3", @@ -20893,8 +21063,8 @@ "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 +21117,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 +21125,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,7 +21138,8 @@ "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", @@ -20987,12 +21158,13 @@ "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", @@ -21001,6 +21173,7 @@ "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 +21182,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", @@ -21029,6 +21202,7 @@ "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", + "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 +21248,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", @@ -21094,7 +21270,7 @@ "thelia/thelia": ">=2.1,<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", @@ -21118,22 +21294,23 @@ "twig/twig": "<3.27", "typicms/core": "<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-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", - "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-form": "<10.4.57|>=11,<11.5.51|>=12,<12.4.46|>=13,<13.4.31|>=14,<14.3.3", "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 +21318,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", @@ -21173,8 +21350,11 @@ "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-symfony-bundle": "<5.3.4", "web-feet/coastercms": "==5.5", + "web-token/jwt-experimental": "<=4.1.6", + "web-token/jwt-framework": "<=4.2.99", + "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", @@ -21300,7 +21480,7 @@ "type": "tidelift" } ], - "time": "2026-06-05T20:39:10+00:00" + "time": "2026-06-26T23:29:05+00:00" }, { "name": "sebastian/cli-parser", @@ -22342,16 +22522,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 +22571,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 +22591,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 +22769,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 +22830,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 +22850,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 +22920,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 +22940,7 @@ "type": "tidelift" } ], - "time": "2026-05-23T16:05:06+00:00" + "time": "2026-06-05T06:22:21+00:00" }, { "name": "theseer/tokenizer", 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/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/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/parameters.yaml b/config/parameters.yaml index b1aa5314..e654a9b5 100644 --- a/config/parameters.yaml +++ b/config/parameters.yaml @@ -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/reference.php b/config/reference.php index 49c16f65..461a3578 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_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_guide-debian.md b/docs/installation/installation_guide-debian.md index e9f500b8..2915adbb 100644 --- a/docs/installation/installation_guide-debian.md +++ b/docs/installation/installation_guide-debian.md @@ -232,7 +232,7 @@ sudo ln -s /etc/apache2/sites-available/partdb.conf /etc/apache2/sites-enabled/p Configure apache to show pretty URL paths for Part-DB (`/label/dialog` instead of `/index.php/label/dialog`): ```bash -sudo a2enmod rewrite +sudo a2enmod rewrite headers ``` If you want to access Part-DB via the IP-Address of the server, instead of the domain name, you have to remove the 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/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/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..0493298f 100644 --- a/public/.htaccess +++ b/public/.htaccess @@ -119,9 +119,14 @@ DirectoryIndex index.php -# Set Content-Security-Policy for svg files (and compressed variants), to block embedded javascript in there + # Set a strict CSP for all static assets not handled by PHP. + # PHP responses already carry their own CSP via NelmioSecurityBundle, so setifempty leaves those untouched. + Header always setifempty Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; sandbox;" + Header always setifempty X-Content-Type-Options "nosniff" + + # SVG files get a slightly different CSP because they can embed resources and must not be framed. - Header set Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none';" + Header always set Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'; sandbox;" - \ No newline at end of file + diff --git a/public/kicad/footprints.txt b/public/kicad/footprints.txt index 5f691a41..08380de1 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 Jun 22 07:31:48 UTC 2026 # This file contains all footprints available in the offical KiCAD library Audio_Module:Reverb_BTDR-1H Audio_Module:Reverb_BTDR-1V @@ -8293,6 +8293,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 @@ -12012,7 +12013,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 +12358,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 @@ -13032,8 +13031,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..a24a1785 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 Jun 22 07:32:26 UTC 2026 # This file contains all symbols available in the offical KiCAD library 4xxx:14528 4xxx:14529 @@ -4954,6 +4954,7 @@ Converter_DCDC:RPMH15-1.5 Converter_DCDC:RPMH24-1.5 Converter_DCDC:RPMH3.3-1.5 Converter_DCDC:RPMH5.0-1.5 +Converter_DCDC:SMU02L-24N Converter_DCDC:TBA1-0310 Converter_DCDC:TBA1-0311 Converter_DCDC:TBA1-0510 @@ -5300,21 +5301,39 @@ Converter_DCDC:TMR-4812 Converter_DCDC:TMR-4821 Converter_DCDC:TMR-4822 Converter_DCDC:TMR-4823 +Converter_DCDC:TMR10-1211WI +Converter_DCDC:TMR10-1212WI +Converter_DCDC:TMR10-1213WI +Converter_DCDC:TMR10-1215WI +Converter_DCDC:TMR10-1222WI +Converter_DCDC:TMR10-1223WI Converter_DCDC:TMR10-2410WIR +Converter_DCDC:TMR10-2411WI Converter_DCDC:TMR10-2411WIR +Converter_DCDC:TMR10-2412WI Converter_DCDC:TMR10-2412WIR +Converter_DCDC:TMR10-2413WI Converter_DCDC:TMR10-2413WIR +Converter_DCDC:TMR10-2415WI Converter_DCDC:TMR10-2415WIR Converter_DCDC:TMR10-2421WIR +Converter_DCDC:TMR10-2422WI Converter_DCDC:TMR10-2422WIR +Converter_DCDC:TMR10-2423WI Converter_DCDC:TMR10-2423WIR Converter_DCDC:TMR10-4810WIR +Converter_DCDC:TMR10-4811WI Converter_DCDC:TMR10-4811WIR +Converter_DCDC:TMR10-4812WI Converter_DCDC:TMR10-4812WIR +Converter_DCDC:TMR10-4813WI Converter_DCDC:TMR10-4813WIR +Converter_DCDC:TMR10-4815WI Converter_DCDC:TMR10-4815WIR Converter_DCDC:TMR10-4821WIR +Converter_DCDC:TMR10-4822WI Converter_DCDC:TMR10-4822WIR +Converter_DCDC:TMR10-4823WI Converter_DCDC:TMR10-4823WIR Converter_DCDC:TMR10-7210WIR Converter_DCDC:TMR10-7211WIR @@ -6124,6 +6143,7 @@ Device:SparkGap Device:Speaker Device:Speaker_Crystal Device:Speaker_Ultrasound +Device:Thermal_Jumper Device:Thermistor Device:Thermistor_NTC Device:Thermistor_NTC_3Wire @@ -14593,6 +14613,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 +15137,7 @@ Memory_Flash:AM29F400Bx-xxEx Memory_Flash:AM29F400Bx-xxSx Memory_Flash:AM29PDL128G Memory_Flash:AT25DF041x-UxN-x +Memory_Flash:AT25SF041B-SSHD-X Memory_Flash:AT25SF081-SSHD-X Memory_Flash:AT25SF081-SSHF-X Memory_Flash:AT25SF081-XMHD-X @@ -15670,6 +15693,7 @@ Power_Management:LM5069MM-1 Power_Management:LM5069MM-2 Power_Management:LM66100DCK Power_Management:LM74700 +Power_Management:LM74701-Q1 Power_Management:LMG3410 Power_Management:LMG5200 Power_Management:LT1641-1 @@ -15771,6 +15795,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 +16505,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 +16707,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 +16867,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 @@ -19261,6 +19299,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 @@ -20989,6 +21028,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 +22519,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/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..80fcc43c 100644 --- a/src/Entity/Parts/Category.php +++ b/src/Entity/Parts/Category.php @@ -68,23 +68,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: '/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")' - ) - ], - 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"])] #[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] diff --git a/src/Entity/Parts/Footprint.php b/src/Entity/Parts/Footprint.php index 3d8be686..2027a310 100644 --- a/src/Entity/Parts/Footprint.php +++ b/src/Entity/Parts/Footprint.php @@ -67,23 +67,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: '/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")' - ) - ], - 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"])] #[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] 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..76526c31 100644 --- a/src/Entity/Parts/Manufacturer.php +++ b/src/Entity/Parts/Manufacturer.php @@ -66,23 +66,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: '/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")' - ) - ], - 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"])] #[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] diff --git a/src/Entity/Parts/MeasurementUnit.php b/src/Entity/Parts/MeasurementUnit.php index 6dd0b9f2..e775b65b 100644 --- a/src/Entity/Parts/MeasurementUnit.php +++ b/src/Entity/Parts/MeasurementUnit.php @@ -71,23 +71,16 @@ 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")' - ) - ], - 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"])] #[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] 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..9571da6e 100644 --- a/src/Entity/Parts/StorageLocation.php +++ b/src/Entity/Parts/StorageLocation.php @@ -67,23 +67,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: '/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")' - ) - ], - 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"])] #[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] diff --git a/src/Entity/Parts/Supplier.php b/src/Entity/Parts/Supplier.php index 2c004e9e..75cf62d1 100644 --- a/src/Entity/Parts/Supplier.php +++ b/src/Entity/Parts/Supplier.php @@ -71,21 +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: '/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) - ], - normalizationContext: ['groups' => ['supplier:read', 'company: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/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..34e77051 100644 --- a/src/Entity/ProjectSystem/Project.php +++ b/src/Entity/ProjectSystem/Project.php @@ -66,23 +66,16 @@ 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")' - ) - ], - 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"])] #[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])] @@ -117,7 +110,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..c016d741 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'])] 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..e59dc85f 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,17 +46,22 @@ 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 = []; @@ -69,20 +74,35 @@ class ProviderSelectType extends AbstractType }); //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?->getProviderKey()); } 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/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/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..1b90091f 100644 --- a/src/Services/Attachments/AttachmentSubmitHandler.php +++ b/src/Services/Attachments/AttachmentSubmitHandler.php @@ -543,8 +543,10 @@ class AttachmentSubmitHandler return $attachment; } + $guessed_mime_type = $this->mimeTypes->guessMimeType($path); + //Check if the file is an SVG - if ($attachment->getExtension() === "svg") { + if ($guessed_mime_type === "image/svg+xml" || $attachment->getExtension() === "svg") { $this->SVGSanitizer->sanitizeFile($path); } 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/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/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/Providers/AIWebProvider.php b/src/Services/InfoProviderSystem/Providers/AIWebProvider.php index 6539e69b..be91041e 100644 --- a/src/Services/InfoProviderSystem/Providers/AIWebProvider.php +++ b/src/Services/InfoProviderSystem/Providers/AIWebProvider.php @@ -282,6 +282,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/Settings/AISettings/AISettings.php b/src/Settings/AISettings/AISettings.php index 732eb597..659577b6 100644 --- a/src/Settings/AISettings/AISettings.php +++ b/src/Settings/AISettings/AISettings.php @@ -35,9 +35,14 @@ class AISettings { use SettingsTrait; + public const TIMEOUT_LIMIT = 600; + #[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/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/symfony.lock b/symfony.lock index f8f88675..94af6e6a 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": { 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/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/_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 @@