Compare commits

...

76 commits

Author SHA1 Message Date
Jan Böhmer
523b07ef2f
Bump version from 2.13.0 to 2.13.1
Some checks failed
Build assets artifact / Build assets artifact (push) Has been cancelled
Docker Image Build / build (linux/amd64, amd64, ubuntu-latest) (push) Has been cancelled
Docker Image Build / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/amd64, amd64, ubuntu-latest) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Has been cancelled
Static analysis / Static analysis (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, sqlite) (push) Has been cancelled
Docker Image Build / merge (push) Has been cancelled
Docker Image Build (FrankenPHP) / merge (push) Has been cancelled
Just an empty release to get the actions run correctly on tagging.
Fixes issue #1430
2026-06-29 17:01:04 +02:00
Jan Böhmer
ffe2f8004b
New Crowdin updates (#1419)
* New translations frontend.en.xlf (English)

[ci skip]

* New translations messages.en.xlf (English)

[ci skip]

* New translations frontend.en.xlf (English)

[ci skip]

* New translations messages.en.xlf (English)

[ci skip]

* New translations messages.en.xlf (English)

[ci skip]

* New translations messages.en.xlf (Chinese Simplified)

[ci skip]

* New translations validators.en.xlf (Chinese Simplified)

[ci skip]

* New translations security.en.xlf (Chinese Simplified)

[ci skip]

* New translations frontend.en.xlf (Chinese Simplified)

[ci skip]

* New translations messages.en.xlf (English)

[ci skip]

* New translations validators.en.xlf (English)

[ci skip]

* New translations messages.en.xlf (German)

[ci skip]

* New translations validators.en.xlf (German)

[ci skip]

* New translations frontend.en.xlf (German)

[ci skip]
2026-06-29 00:12:20 +02:00
Jan Böhmer
7244803937 Fixed phpstan error 2026-06-29 00:08:44 +02:00
Jan Böhmer
214646caa6 Bumped version to 2.13.0 2026-06-28 23:58:38 +02:00
Jan Böhmer
ffcfdb793f Validate info provider references modified via part edit form 2026-06-28 23:56:55 +02:00
Jan Böhmer
e03eda84c5 Allow to edit info provider reference
Fixes   issue #1394
2026-06-28 23:47:49 +02:00
Jan Böhmer
4d3e2e28a5 Updated dependencies
Some checks are pending
Build assets artifact / Build assets artifact (push) Waiting to run
Docker Image Build / build (linux/amd64, amd64, ubuntu-latest) (push) Waiting to run
Docker Image Build / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build / merge (push) Blocked by required conditions
Docker Image Build (FrankenPHP) / build (linux/amd64, amd64, ubuntu-latest) (push) Waiting to run
Docker Image Build (FrankenPHP) / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build (FrankenPHP) / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build (FrankenPHP) / merge (push) Blocked by required conditions
Static analysis / Static analysis (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, sqlite) (push) Waiting to run
2026-06-28 19:31:20 +02:00
dependabot[bot]
8ed14ca708
Bump actions/checkout from 6 to 7 (#1421)
Some checks failed
Build assets artifact / Build assets artifact (push) Has been cancelled
Docker Image Build / build (linux/amd64, amd64, ubuntu-latest) (push) Has been cancelled
Docker Image Build / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/amd64, amd64, ubuntu-latest) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Has been cancelled
Static analysis / Static analysis (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, sqlite) (push) Has been cancelled
Docker Image Build / merge (push) Has been cancelled
Docker Image Build (FrankenPHP) / merge (push) Has been cancelled
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-25 20:44:44 +02:00
Jan Böhmer
34257332ff
Update KiCad symbols and footprints lists (#1420)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-25 20:42:27 +02:00
0x915
2503e63bb2
Perform a full translation of Simplified Chinese. (#1426)
* Perform a full translation of Simplified Chinese.

Based on the latest *.en.xlf file.

Translated automatically using DeepSinkV4 based on the source code, 
then manually reviewed and corrected line by line.

* Normalized formatting

---------

Co-authored-by: Jan Böhmer <mail@jan-boehmer.de>
2026-06-25 20:41:55 +02:00
Jan Böhmer
0eba7121aa Merged subchildren operations on APIPlatform into one APIResource, as havinng multiple is deprecated 2026-06-25 20:16:44 +02:00
Jan Böhmer
188444b30f Fixed deprecations
Some checks are pending
Build assets artifact / Build assets artifact (push) Waiting to run
Docker Image Build / build (linux/amd64, amd64, ubuntu-latest) (push) Waiting to run
Docker Image Build / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build / merge (push) Blocked by required conditions
Docker Image Build (FrankenPHP) / build (linux/amd64, amd64, ubuntu-latest) (push) Waiting to run
Docker Image Build (FrankenPHP) / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build (FrankenPHP) / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build (FrankenPHP) / merge (push) Blocked by required conditions
Static analysis / Static analysis (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, sqlite) (push) Waiting to run
2026-06-25 12:44:06 +02:00
Jan Böhmer
cd87b59c15 Fixed small deprecations 2026-06-25 12:37:22 +02:00
Jan Böhmer
b3895c1e91 Fixed depprecation of fluent config builders 2026-06-25 12:33:52 +02:00
Jan Böhmer
23e22b19e2 Removed usesless easy_log_handler file 2026-06-25 12:27:37 +02:00
Jan Böhmer
9c919a9be9 Moved from phpunit annotation to attribute 2026-06-25 12:12:34 +02:00
Jan Böhmer
eb7da91c44 Fixed phpstan issue 2026-06-25 12:09:06 +02:00
Jan Böhmer
b8fc5d4ace Warn if ProjectBuild passed lot if null
Fixed caused deprecations for it
2026-06-25 12:07:54 +02:00
Jan Böhmer
fe0809230b Fixed sqlite deprecation on PHP8.5 2026-06-25 11:59:37 +02:00
Jan Böhmer
9d4dabbd20 Removed useless setAccessible() calls
They are noop since 8.1 and we only support 8.2+
2026-06-25 10:54:13 +02:00
Jan Böhmer
5e18ae2874 Fixed problem with changing last_stocktake date, when editing part
Some checks are pending
Build assets artifact / Build assets artifact (push) Waiting to run
Docker Image Build / build (linux/amd64, amd64, ubuntu-latest) (push) Waiting to run
Docker Image Build / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build / merge (push) Blocked by required conditions
Docker Image Build (FrankenPHP) / build (linux/amd64, amd64, ubuntu-latest) (push) Waiting to run
Docker Image Build (FrankenPHP) / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build (FrankenPHP) / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build (FrankenPHP) / merge (push) Blocked by required conditions
Static analysis / Static analysis (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, sqlite) (push) Waiting to run
The database has UTC values, we need to set that in the form. A form extension was added to ensure that this does not happen again. Also an issue was fixed that the seconds were cut off. This fixes issue #1390
2026-06-25 00:08:57 +02:00
Jan Böhmer
3e725dd2ec Increased timeout for local AI inferences, and made AI timeout configurable per provider
Some checks failed
Build assets artifact / Build assets artifact (push) Has been cancelled
Docker Image Build / build (linux/amd64, amd64, ubuntu-latest) (push) Has been cancelled
Docker Image Build / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/amd64, amd64, ubuntu-latest) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Has been cancelled
Static analysis / Static analysis (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, sqlite) (push) Has been cancelled
Docker Image Build / merge (push) Has been cancelled
Docker Image Build (FrankenPHP) / merge (push) Has been cancelled
Fixes issue #1396
2026-06-22 22:06:30 +02:00
Jan Böhmer
ec80115d0a Renamed AI response schema from errornous "clock" to "part_detail" 2026-06-21 23:10:44 +02:00
Jan Böhmer
b60887c71d Added ollama as AI provider
Some checks are pending
Build assets artifact / Build assets artifact (push) Waiting to run
Docker Image Build / build (linux/amd64, amd64, ubuntu-latest) (push) Waiting to run
Docker Image Build / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build / merge (push) Blocked by required conditions
Docker Image Build (FrankenPHP) / build (linux/amd64, amd64, ubuntu-latest) (push) Waiting to run
Docker Image Build (FrankenPHP) / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build (FrankenPHP) / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build (FrankenPHP) / merge (push) Blocked by required conditions
Static analysis / Static analysis (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, sqlite) (push) Waiting to run
Unlike the LMStudio one it also features an API key and proper model auto suggestion
2026-06-21 20:41:24 +02:00
Jan Böhmer
9f686c88fe Updated symfony ai bundle to 0.10.0 2026-06-21 16:15:36 +02:00
Jan Böhmer
36244ec63f Added brite bootswatch theme
Some checks are pending
Build assets artifact / Build assets artifact (push) Waiting to run
Docker Image Build / build (linux/amd64, amd64, ubuntu-latest) (push) Waiting to run
Docker Image Build / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build / merge (push) Blocked by required conditions
Docker Image Build (FrankenPHP) / build (linux/amd64, amd64, ubuntu-latest) (push) Waiting to run
Docker Image Build (FrankenPHP) / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build (FrankenPHP) / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build (FrankenPHP) / merge (push) Blocked by required conditions
Static analysis / Static analysis (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, sqlite) (push) Waiting to run
2026-06-21 16:10:38 +02:00
Jan Böhmer
f45e3a9ef8 Upgraded compression-webpack-plugin 2026-06-21 15:54:09 +02:00
Jan Böhmer
a46f1713fe Use the correct yarn urls 2026-06-21 15:52:52 +02:00
Jan Böhmer
22f23d9c82 Upgraded jquery to 4.0.0 2026-06-21 15:51:22 +02:00
Jan Böhmer
b4cf5b57fa Translate Swal buttons 2026-06-21 15:10:39 +02:00
Jan Böhmer
3491559e9f Migrated register_events from jquery to native JS 2026-06-21 14:52:30 +02:00
Jan Böhmer
b83fc73e18 Remove jquery command from error_handler.js 2026-06-21 14:46:15 +02:00
Jan Böhmer
8c88df4ecf Use upstream version of dataTables.select as the fix was merged 2026-06-21 14:41:56 +02:00
Jan Böhmer
176d5ad2b6 Improved page load error dialog
We now show a more user friendly message
2026-06-21 14:37:40 +02:00
Jan Böhmer
99e56c4b1d Moved alerts and dialogs from unsupported bootbox to Sweetalert2 library 2026-06-21 14:21:01 +02:00
Jan Böhmer
a489380f49 Merge remote-tracking branch 'origin/master' 2026-06-21 12:49:06 +02:00
Jan Böhmer
9127bcf25e Added it and pl translations for password estimator, use lvenshtein distance and block partdb word 2026-06-21 12:49:00 +02:00
Jan Böhmer
c3af73daae Use dictonaries for german and english words for password estimator 2026-06-21 12:37:31 +02:00
Jan Böhmer
7e90f6d707 Updated password strenght estimator to latest version and show crack time estimate as tooltip 2026-06-21 12:33:30 +02:00
Jan Böhmer
a793bc32c7
Update KiCad symbols and footprints lists (#1411)
Some checks are pending
Build assets artifact / Build assets artifact (push) Waiting to run
Docker Image Build / build (linux/amd64, amd64, ubuntu-latest) (push) Waiting to run
Docker Image Build / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build / merge (push) Blocked by required conditions
Docker Image Build (FrankenPHP) / build (linux/amd64, amd64, ubuntu-latest) (push) Waiting to run
Docker Image Build (FrankenPHP) / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build (FrankenPHP) / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build (FrankenPHP) / merge (push) Blocked by required conditions
Static analysis / Static analysis (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, sqlite) (push) Waiting to run
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-20 23:59:22 +02:00
Jan Böhmer
e642dbe060 Bumped to version 2.12.3 2026-06-20 23:51:51 +02:00
Jan Böhmer
8ba3139617 Updated dependencies 2026-06-20 23:49:22 +02:00
Jan Böhmer
b62f47ba05 Set CSP policy for static assets for security hardeninng 2026-06-20 23:42:01 +02:00
Jan Böhmer
0cd83f0322 Set strict CSP policies when serving files from the attachment endpoints 2026-06-20 23:02:55 +02:00
Jan Böhmer
02726fdf69 Sanatize SVG files, even when they try to hide themselves with a different extension 2026-06-17 22:38:41 +02:00
dependabot[bot]
98df91d785
Bump codecov/codecov-action from 6 to 7 (#1407)
Some checks failed
Build assets artifact / Build assets artifact (push) Has been cancelled
Docker Image Build / build (linux/amd64, amd64, ubuntu-latest) (push) Has been cancelled
Docker Image Build / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/amd64, amd64, ubuntu-latest) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Has been cancelled
Static analysis / Static analysis (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, sqlite) (push) Has been cancelled
Docker Image Build / merge (push) Has been cancelled
Docker Image Build (FrankenPHP) / merge (push) Has been cancelled
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 6 to 7.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-14 23:22:04 +02:00
Jan Böhmer
bffb2c1d70 Bumped versionnn to 2.12.2 2026-06-14 23:21:36 +02:00
Jan Böhmer
d44ce85d89 Merge branch 'fix_deprecations' 2026-06-14 23:20:08 +02:00
Jan Böhmer
12f4a3606e Remove deprecation logging by default and added env to reenable it again
The files can easily get quite large and probably also not good for performance. This fixes issue #1405
2026-06-14 23:19:59 +02:00
d-buchmann
192c5fcaa3
Update hide_sidebar_controller.js (#1404)
With this fix, the sidebar state is reapplied correctly on page reload.
2026-06-14 22:52:08 +02:00
d-buchmann
63d507b2f3
Update StorageLocation.php (#1403)
fixes #1398
2026-06-14 22:48:58 +02:00
Jan Böhmer
ef7e6d6f3b Fixed MYSQL_ATTR_INIT_COMMAND deprecation 2026-06-14 22:44:17 +02:00
Jan Böhmer
350e5a0245 Fixed deprecationn in TreeController 2026-06-14 22:40:23 +02:00
Jan Böhmer
704f7e7645
Update KiCad symbols and footprints lists (#1402)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-14 22:32:31 +02:00
Jan Böhmer
589f420b79 Updated dependencies 2026-06-14 22:19:43 +02:00
Jan Böhmer
dfbdac7688 Avoid using render in datatables, as it require escaping
This commit also fixes an XSS vulnerability in IPN project bom
2026-06-14 22:16:00 +02:00
Jan Böhmer
11b41ee66a Hardened against potential XSS injection in table columns 2026-06-14 12:08:25 +02:00
Jan Böhmer
c9dd27712c Fixed stored XSS vulnerability in BOM Validation Service 2026-06-14 11:55:16 +02:00
Jan Böhmer
8421636b1c Use HTML sanatizer to harden HTML rendering on log_details page
Should be more safe than use |raw directly and for these smalls things performance hit is zero.
2026-06-10 23:43:07 +02:00
Jan Böhmer
b357ee196c Avoid usage of raw filter in javascript to minimize risk 2026-06-10 23:37:57 +02:00
Jan Böhmer
0c5f8dc9fd Updated dependencies 2026-06-10 23:37:22 +02:00
Jan Böhmer
57a0dfdbdb Fixed phpstan issue
Some checks failed
Build assets artifact / Build assets artifact (push) Has been cancelled
Docker Image Build / build (linux/amd64, amd64, ubuntu-latest) (push) Has been cancelled
Docker Image Build / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/amd64, amd64, ubuntu-latest) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Has been cancelled
Static analysis / Static analysis (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, sqlite) (push) Has been cancelled
Docker Image Build / merge (push) Has been cancelled
Docker Image Build (FrankenPHP) / merge (push) Has been cancelled
2026-06-07 22:49:41 +02:00
Jan Böhmer
61dcc99597
New Crowdin updates (#1383)
* New translations messages.en.xlf (German)

[ci skip]

* New translations messages.en.xlf (German)

[ci skip]

* New translations messages.en.xlf (German)

[ci skip]

* New translations messages.en.xlf (English)

[ci skip]

* New translations messages.en.xlf (German)

[ci skip]

* New translations messages.en.xlf (English)

[ci skip]
2026-06-07 22:47:22 +02:00
Jan Böhmer
cf8826a906 Bumped version to 2.12.1 2026-06-07 22:43:33 +02:00
Jan Böhmer
4b00541dd6 Improved APP_SECRET warning message 2026-06-07 22:43:06 +02:00
Jan Böhmer
93ab410857 Added documentation about changing the APP_SECRET env on installation 2026-06-07 22:37:47 +02:00
Jan Böhmer
cb28afcdf5 Moved APP_SECRET value to the top of .env 2026-06-07 22:28:29 +02:00
Jan Böhmer
f888e10827 Show a warning if using the default APP_SECRET value 2026-06-07 22:26:45 +02:00
Jan Böhmer
c229208bd5 Rename phar files on upload
Some checks are pending
Build assets artifact / Build assets artifact (push) Waiting to run
Docker Image Build / build (linux/amd64, amd64, ubuntu-latest) (push) Waiting to run
Docker Image Build / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build / merge (push) Blocked by required conditions
Docker Image Build (FrankenPHP) / build (linux/amd64, amd64, ubuntu-latest) (push) Waiting to run
Docker Image Build (FrankenPHP) / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build (FrankenPHP) / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build (FrankenPHP) / merge (push) Blocked by required conditions
Static analysis / Static analysis (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, sqlite) (push) Waiting to run
2026-06-07 20:45:43 +02:00
Jan Böhmer
6e5d1c967f Block access to all php and phar files that are uploaded into the media folder 2026-06-07 20:40:15 +02:00
tonghuaroot
c2ec0ee12b Escape user-controlled element/collection names in CollectionElementDeleted log rendering
LogEntryExtraFormatter::getInternalFormat() escapes user-controlled
strings with htmlspecialchars() in every branch except the
CollectionElementDeleted one, which interpolates getOldName() and
getCollectionName() into the returned HTML unescaped. That string is
rendered as raw HTML by LogEntryExtraColumn in the activity-log and
element-history DataTables, so a name set on a deleted sub-element
(parameter/attachment/lot/orderdetail) by a low-privileged editor is
parsed as live markup in a log viewer's browser (stored XSS / HTML
injection).

Wrap both values in htmlspecialchars(), matching the sibling branches
(e.g. ElementDeletedLogEntry at the old_name line).
2026-06-07 18:04:40 +02:00
Jan Böhmer
6ae4381551 Fixed phpstan issues 2026-06-07 14:11:53 +02:00
Jan Böhmer
e018e1d821 Merge remote-tracking branch 'origin/master' 2026-06-07 14:08:48 +02:00
Jan Böhmer
4b6a3ba72b Updated dependencies 2026-06-07 14:08:46 +02:00
Jan Böhmer
c547500031
Update KiCad symbols and footprints lists (#1395)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-07 14:08:00 +02:00
Jan Böhmer
4b119490ca Updated depdencies
Some checks failed
Build assets artifact / Build assets artifact (push) Has been cancelled
Docker Image Build / build (linux/amd64, amd64, ubuntu-latest) (push) Has been cancelled
Docker Image Build / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/amd64, amd64, ubuntu-latest) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Has been cancelled
Static analysis / Static analysis (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, sqlite) (push) Has been cancelled
Docker Image Build / merge (push) Has been cancelled
Docker Image Build (FrankenPHP) / merge (push) Has been cancelled
2026-06-03 22:49:22 +02:00
137 changed files with 10594 additions and 6565 deletions

View file

@ -51,5 +51,18 @@
# Disable Topics tracking if not enabled explicitly: https://github.com/jkarlin/topics # Disable Topics tracking if not enabled explicitly: https://github.com/jkarlin/topics
header ?Permissions-Policy "browsing-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
php_server php_server
} }

View file

@ -15,6 +15,14 @@
AllowOverride All AllowOverride All
</Directory> </Directory>
# Prevent PHP execution in the media upload directory (server-level, not .htaccess,
# because public/media is a Docker volume and .htaccess there may not be present)
<Directory /var/www/html/public/media>
<FilesMatch "(?i)\.(php[3-8]?|phar|phtml|pht|phps)$">
Require all denied
</FilesMatch>
</Directory>
# Available loglevels: trace8, ..., trace1, debug, info, notice, warn, # Available loglevels: trace8, ..., trace1, debug, info, notice, warn,
# error, crit, alert, emerg. # error, crit, alert, emerg.
# It is also possible to configure the loglevel for particular # It is also possible to configure the loglevel for particular

15
.env
View file

@ -1,6 +1,10 @@
#### Part-DB Configuration #### Part-DB Configuration
# See https://docs.part-db.de/configuration.html for documentation of available options # See https://docs.part-db.de/configuration.html for documentation of available options
# Change this to a random value to secure your installation! You can generate a random string with "openssl rand -hex 16"
# Share that value with nobody and keep it secret
APP_SECRET=a03498528f5a5fc089273ec9ae5b2849
################################################################################### ###################################################################################
# Database settings # Database settings
################################################################################### ###################################################################################
@ -145,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_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)$' #TRUSTED_HOSTS='^(localhost|example\.com)$'
###################################################################################
# Logging settings
###################################################################################
# The minimum level a deprecation notice must have to be written to the var/log/<env>_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 ### ###> symfony/lock ###
# Choose one of the stores below # Choose one of the stores below
@ -158,7 +172,6 @@ CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
###> symfony/framework-bundle ### ###> symfony/framework-bundle ###
APP_ENV=prod APP_ENV=prod
APP_SECRET=a03498528f5a5fc089273ec9ae5b2849
APP_SHARE_DIR=var/share APP_SHARE_DIR=var/share
###< symfony/framework-bundle ### ###< symfony/framework-bundle ###

View file

@ -27,7 +27,7 @@ jobs:
APP_ENV: prod APP_ENV: prod
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Setup PHP - name: Setup PHP
uses: shivammathur/setup-php@v2 uses: shivammathur/setup-php@v2

View file

@ -32,7 +32,7 @@ jobs:
steps: steps:
- -
name: Checkout name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v7
- -
name: Docker meta name: Docker meta
id: docker_meta id: docker_meta

View file

@ -32,7 +32,7 @@ jobs:
steps: steps:
- -
name: Checkout name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v7
- -
name: Docker meta name: Docker meta
id: docker_meta id: docker_meta

View file

@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Setup PHP - name: Setup PHP
uses: shivammathur/setup-php@v2 uses: shivammathur/setup-php@v2

View file

@ -46,7 +46,7 @@ jobs:
if: matrix.db-type == 'postgres' if: matrix.db-type == 'postgres'
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v7
- name: Setup PHP - name: Setup PHP
uses: shivammathur/setup-php@v2 uses: shivammathur/setup-php@v2
@ -129,7 +129,7 @@ jobs:
run: ./bin/phpunit --coverage-clover=coverage.xml run: ./bin/phpunit --coverage-clover=coverage.xml
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v6 uses: codecov/codecov-action@v7
with: with:
env_vars: PHP_VERSION,DB_TYPE env_vars: PHP_VERSION,DB_TYPE
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}

View file

@ -193,7 +193,7 @@ RUN a2dissite 000-default.conf && \
a2enmod proxy_fcgi setenvif && \ a2enmod proxy_fcgi setenvif && \
a2enconf php${PHP_VERSION}-fpm && \ a2enconf php${PHP_VERSION}-fpm && \
a2enconf docker-php && \ a2enconf docker-php && \
a2enmod rewrite a2enmod rewrite headers
# Install composer and yarn dependencies for Part-DB # Install composer and yarn dependencies for Part-DB
USER www-data USER www-data

View file

@ -1 +1 @@
2.12.0 2.13.1

View file

@ -19,8 +19,7 @@
import {Controller} from "@hotwired/stimulus"; import {Controller} from "@hotwired/stimulus";
import {visit} from "@hotwired/turbo"; import {visit} from "@hotwired/turbo";
import * as bootbox from "bootbox"; import {ConfirmSwal} from "../../helpers/swal";
import "../../css/components/bootbox_extensions.css";
import "../../css/components/dirty_form.css"; import "../../css/components/dirty_form.css";
/** /**
@ -207,11 +206,10 @@ export default class extends Controller {
} }
_confirmNavigation(onConfirm) { _confirmNavigation(onConfirm) {
bootbox.confirm({ ConfirmSwal.fire({
title: this.confirmTitleValue, titleText: this.confirmTitleValue,
message: this.confirmMessageValue, text: this.confirmMessageValue,
callback: (result) => { if (result) onConfirm(); } }).then(({isConfirmed}) => { if (isConfirmed) onConfirm(); });
});
} }
_handleLinkClick(event) { _handleLinkClick(event) {

View file

@ -51,7 +51,7 @@ export default class extends Controller {
//Make the state persistent over reloads //Make the state persistent over reloads
if(localStorage.getItem(STORAGE_KEY) === 'true') { if(localStorage.getItem(STORAGE_KEY) === 'true') {
sidebarHide(); this.hideSidebar();
} }
} }

View file

@ -19,8 +19,7 @@
import {Controller} from "@hotwired/stimulus"; import {Controller} from "@hotwired/stimulus";
import * as bootbox from "bootbox"; import {AlertSwal, ConfirmSwal} from "../../helpers/swal";
import "../../css/components/bootbox_extensions.css";
import accept from "attr-accept"; import accept from "attr-accept";
export default class extends Controller { export default class extends Controller {
@ -62,7 +61,7 @@ export default class extends Controller {
if(!prototype) { if(!prototype) {
console.warn("Prototype is not set, we cannot create a new element. This is most likely due to missing permissions."); 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; return;
} }
@ -226,8 +225,10 @@ export default class extends Controller {
} }
if(this.deleteMessageValue) { if(this.deleteMessageValue) {
bootbox.confirm(this.deleteMessageValue, (result) => { ConfirmSwal.fire({
if (result) { text: this.deleteMessageValue,
}).then(({isConfirmed}) => {
if (isConfirmed) {
del(); del();
} }
}); });

View file

@ -38,9 +38,7 @@ import 'datatables.net-colreorder-bs5';
import 'datatables.net-responsive-bs5'; import 'datatables.net-responsive-bs5';
import '../../../js/lib/datatables'; import '../../../js/lib/datatables';
//import 'datatables.net-select-bs5'; import 'datatables.net-select-bs5';
//Use the local version containing the fix for the select extension
import '../../../js/lib/dataTables.select.mjs';
const EVENT_DT_LOADED = 'dt:loaded'; const EVENT_DT_LOADED = 'dt:loaded';

View file

@ -20,7 +20,7 @@
import DatatablesController from "./datatables_controller.js"; import DatatablesController from "./datatables_controller.js";
import TomSelect from "tom-select"; import TomSelect from "tom-select";
import * as bootbox from "bootbox"; import {ConfirmSwal} from "../../../helpers/swal";
/** /**
* This is the datatables controller for parts lists * 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 bubbles: true, //This line is important, otherwise Turbo will not receive the event
}); });
const confirm = bootbox.confirm({ ConfirmSwal.fire({
message: message, title: title, callback: function (result) { titleText: title,
//If the dialog was confirmed, then submit the form. text: message,
if (result) { icon: "warning"
that._confirmed = true; }).then(({isConfirmed}) => {
form.dispatchEvent(that._our_event); //If the dialog was confirmed, then submit the form.
} else { if (isConfirmed) {
that._confirmed = false; that._confirmed = true;
} form.dispatchEvent(that._our_event);
} else {
that._confirmed = false;
} }
}); });
} }

View file

@ -19,8 +19,7 @@
import {Controller} from "@hotwired/stimulus"; import {Controller} from "@hotwired/stimulus";
import * as bootbox from "bootbox"; import {ConfirmSwal} from "../../helpers/swal";
import "../../css/components/bootbox_extensions.css";
export default class extends Controller export default class extends Controller
{ {
@ -48,31 +47,32 @@ export default class extends Controller
const submitter = event.submitter; const submitter = event.submitter;
const that = this; const that = this;
const confirm = bootbox.confirm({ ConfirmSwal.fire({
message: message, title: title, callback: function (result) { titleText: title,
//If the dialog was confirmed, then submit the form. html: message, //Message contains a <br> tag and no user injectable HTML
if (result) { }).then(({isConfirmed}) => {
//Set a flag to prevent the dialog from popping up again and allowing turbo to submit the form //If the dialog was confirmed, then submit the form.
that._confirmed = true; 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 //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 //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'); const submit_btn = document.createElement('button');
submit_btn.type = 'submit'; submit_btn.type = 'submit';
submit_btn.style.display = 'none'; submit_btn.style.display = 'none';
//If the clicked button has a value, set it on the submit button //If the clicked button has a value, set it on the submit button
if (submitter.value) { if (submitter.value) {
submit_btn.value = 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 (submitter.name) {
submit_btn.name = submitter.name;
}
form.appendChild(submit_btn);
submit_btn.click();
} else {
that._confirmed = false;
} }
}); });
} }

View file

@ -19,8 +19,7 @@
import {Controller} from "@hotwired/stimulus"; import {Controller} from "@hotwired/stimulus";
import * as bootbox from "bootbox"; import {ConfirmSwal} from "../../helpers/swal";
import "../../css/components/bootbox_extensions.css";
export default class extends Controller export default class extends Controller
{ {
@ -53,19 +52,18 @@ export default class extends Controller
const that = this; const that = this;
bootbox.confirm({ ConfirmSwal.fire({
title: this.titleValue, titleText: this.titleValue,
message: this.messageValue, text: this.messageValue,
callback: (result) => { }).then(({isConfirmed}) => {
if (result) { if (isConfirmed) {
//Set a flag to prevent the dialog from popping up again and allowing turbo to submit the form //Set a flag to prevent the dialog from popping up again and allowing turbo to submit the form
that._confirmed = true; that._confirmed = true;
//Click the link //Click the link
that.element.click(); that.element.click();
} else { } else {
that._confirmed = false; that._confirmed = false;
}
} }
}); });
} }

View file

@ -19,12 +19,14 @@
import {Controller} from "@hotwired/stimulus"; 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 zxcvbnCommonPackage from '@zxcvbn-ts/language-common';
import * as zxcvbnEnPackage from '@zxcvbn-ts/language-en'; import * as zxcvbnEnPackage from '@zxcvbn-ts/language-en';
import * as zxcvbnDePackage from '@zxcvbn-ts/language-de'; import * as zxcvbnDePackage from '@zxcvbn-ts/language-de';
import * as zxcvbnFrPackage from '@zxcvbn-ts/language-fr'; import * as zxcvbnFrPackage from '@zxcvbn-ts/language-fr';
import * as zxcvbnJaPackage from '@zxcvbn-ts/language-ja'; 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'; import {trans} from '../../translator.js';
/* stimulusFetch: 'lazy' */ /* stimulusFetch: 'lazy' */
@ -34,6 +36,8 @@ export default class extends Controller {
static targets = ["badge", "warning"] static targets = ["badge", "warning"]
_zxcvbnFactory;
_getTranslations() { _getTranslations() {
//Get the current locale //Get the current locale
const locale = document.documentElement.lang; const locale = document.documentElement.lang;
@ -43,6 +47,10 @@ export default class extends Controller {
return zxcvbnFrPackage.translations; return zxcvbnFrPackage.translations;
} else if (locale.includes('ja')) { } else if (locale.includes('ja')) {
return zxcvbnJaPackage.translations; return zxcvbnJaPackage.translations;
} else if (locale.includes('it')) {
return zxcvbnItPackage.translations;
} else if (locale.includes('pl')) {
return zxcvbnPlPackage.translations;
} }
//Fallback to english //Fallback to english
@ -56,34 +64,39 @@ export default class extends Controller {
//Configure zxcvbn //Configure zxcvbn
const options = { const options = {
graphs: zxcvbnCommonPackage.adjacencyGraphs, graphs: zxcvbnCommonPackage.adjacencyGraphs,
useLevenshtein: true,
dictionary: { dictionary: {
...zxcvbnCommonPackage.dictionary, ...zxcvbnCommonPackage.dictionary,
// We could use the english dictionary here too, but it is very big. So we just use the common words // 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(), translations: this._getTranslations(),
}; };
zxcvbnOptions.setOptions(options);
this._zxcvbnFactory = new ZxcvbnFactory(options);
//Add event listener to the password input field //Add event listener to the password input field
this._passwordInput.addEventListener('input', this._onPasswordInput.bind(this)); this._passwordInput.addEventListener('input', this._onPasswordInput.bind(this));
} }
_onPasswordInput() { async _onPasswordInput() {
//Retrieve the password //Retrieve the password
const password = this._passwordInput.value; const password = this._passwordInput.value;
//Estimate the password strength //Estimate the password strength
const result = zxcvbn(password); const result = await this._zxcvbnFactory.checkAsync(password);
//Update the badge //Update the badge
this.badgeTarget.parentElement.classList.remove("d-none"); 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; this.warningTarget.innerHTML = result.feedback.warning;
} }
_setBadgeToLevel(level) { _setBadgeToLevel(level, time = null) {
let text, classes; let text, classes;
switch (level) { switch (level) {
@ -118,5 +131,11 @@ export default class extends Controller {
//Re-add the classes //Re-add the classes
this.badgeTarget.classList.add("badge"); this.badgeTarget.classList.add("badge");
this.badgeTarget.classList.add(...classes.split(" ")); 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");
}
} }
} }

View file

@ -18,7 +18,7 @@
*/ */
import {Controller} from "@hotwired/stimulus"; import {Controller} from "@hotwired/stimulus";
import * as bootbox from "bootbox"; import {AlertSwal} from "../../helpers/swal";
export default class extends Controller { export default class extends Controller {
@ -35,12 +35,12 @@ export default class extends Controller {
const part_distance = document.getElementById('reel_part_distance').value; const part_distance = document.getElementById('reel_part_distance').value;
if (dia_inner == "" || dia_outer == "" || tape_thickness == "") { if (dia_inner == "" || dia_outer == "" || tape_thickness == "") {
bootbox.alert(this.errorMissingValuesValue); AlertSwal.fire({title: this.errorMissingValuesValue});
return; return;
} }
if (dia_outer**dia_outer < dia_inner**dia_inner) { if (dia_outer**dia_outer < dia_inner**dia_inner) {
bootbox.alert(this.errorOuterGreaterInnerValue); AlertSwal.fire({title: this.errorOuterGreaterInnerValue});
return; return;
} }
@ -61,12 +61,12 @@ export default class extends Controller {
return; 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'; 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();
} }
} }

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
/**
* Respect the dark mode of Bootstrap 5 set via data-bs-theme="dark" on the <html> 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;
}

44
assets/helpers/swal.js Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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,};

View file

@ -30,21 +30,21 @@ import '../css/app/images.css';
// start the Stimulus application // start the Stimulus application
import '../stimulus_bootstrap'; import '../stimulus_bootstrap';
// Need jQuery? Install it with "yarn add jquery", then uncomment to require it. import $ from 'jquery';
const $ = require('jquery');
//Only include javascript //Only include javascript
import '@fortawesome/fontawesome-free/css/all.css' import '@fortawesome/fontawesome-free/css/all.css'
require('bootstrap'); import 'bootstrap';
import "./error_handler"; import "./error_handler";
import "./tab_remember"; import "./tab_remember";
import "./register_events"; import "./register_events";
import "./tristate_checkboxes"; import "./tristate_checkboxes";
//Define jquery globally // Expose jQuery globally so legacy plugins and Bootstrap's jQuery integration
global.$ = global.jQuery = require("jquery"); // can find it on window at runtime.
global.$ = global.jQuery = $;
//Use the local WASM file for the ZXing library //Use the local WASM file for the ZXing library
import { import {

View file

@ -17,7 +17,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
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. * 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) _showAlert(statusText, statusCode, location, responseHTML)
{ {
const httpStatusToText = { 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', '400': 'Bad Request',
'401': 'Unauthorized', '401': 'Unauthorized',
'402': 'Payment Required', '402': 'Payment Required',
@ -83,49 +68,67 @@ class ErrorHandlerHelper {
'505': 'HTTP Version Not Supported', '505': 'HTTP Version Not Supported',
}; };
//If the statusText is empty, we use the status code as text const userFriendlyMessages = {
if (!statusText) { '400': 'The request was invalid or malformed.',
statusText = httpStatusToText[statusCode]; '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.',
//Create error text '408': 'The request timed out. Please check your connection and try again.',
const title = statusText + ' (Status ' + statusCode + ')'; '409': 'There was a conflict with the current state of the resource.',
'429': 'Too many requests sent. Please wait a moment and try again.',
let trimString = function (string, length) { '500': 'An internal server error occurred. This is not your fault.',
return string.length > length ? '502': 'The server received an invalid response from an upstream service.',
string.substring(0, length) + '...' : '503': 'The service is temporarily unavailable. Please try again later.',
string; '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( const title = `${statusText} <small class="text-muted fs-6">(HTTP ${statusCode})</small>`;
{ const friendlyMsg = userFriendlyMessages[String(statusCode)]
size: 'large', ?? 'An unexpected error occurred. Please try again or contact the administrator.';
message: function() {
let url = location;
let msg = `Error calling <a href="${url}">${short_location}</a>.<br>`;
msg += '<b>Try to reload the page or contact the administrator if this error persists.</b>';
msg += '<br><br><a class=\"btn btn-outline-secondary mb-2\" data-bs-toggle=\"collapse\" href=\"#iframe_div\" >' + 'View details' + "</a>"; const short_location = location.length > 80
msg += "<div class=\" collapse\" id='iframe_div'><iframe height='512' width='100%' id='error-iframe'></iframe></div>"; ? location.substring(0, 80) + '…'
: location;
return msg; const msg = `
}, <p class="mb-3">${friendlyMsg}</p>
title: title, <p class="text-muted small mb-3">If this error keeps happening, please contact your administrator.</p>
callback: function () { <button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#swal-error-details" aria-expanded="false">
//Remove blur <i class="fas fa-code me-1"></i>Technical details
$('#content').removeClass('loading-content'); </button>
} <div class="collapse mt-2" id="swal-error-details">
<iframe height="400" width="100%" id="error-iframe" style="border:1px solid var(--bs-border-color);border-radius:var(--bs-border-radius);"></iframe>
</div>`;
}); const footer = `<span class="text-muted small">Error while loading: <a href="${location}" class="text-muted text-decoration-none" style="opacity:0.7;">${short_location}</a></span>`;
alert.init(function (){ Swal.fire({
var dstFrame = document.getElementById('error-iframe'); icon: 'error',
//@ts-ignore title: title,
var dstDoc = dstFrame.contentDocument || dstFrame.contentWindow.document; html: msg,
dstDoc.write(responseHTML) footer: footer,
dstDoc.close(); width: '90%',
confirmButtonText: '<i class="fas fa-rotate-right me-1"></i>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();
}
}); });
} }

File diff suppressed because it is too large Load diff

View file

@ -19,9 +19,8 @@
'use strict'; 'use strict';
import {Dropdown} from "bootstrap"; import {Dropdown, Modal, Tooltip} from "bootstrap";
import ClipboardJS from "clipboard"; import ClipboardJS from "clipboard";
import {Modal} from "bootstrap";
class RegisterEventHelper { class RegisterEventHelper {
constructor() { constructor() {
@ -40,8 +39,6 @@ class RegisterEventHelper {
}); });
this.registerModalDropRemovalOnFormSubmit(); this.registerModalDropRemovalOnFormSubmit();
} }
registerModalDropRemovalOnFormSubmit() { registerModalDropRemovalOnFormSubmit() {
@ -83,11 +80,17 @@ class RegisterEventHelper {
registerTooltips() { registerTooltips() {
const handler = () => { 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.) //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]') 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]';
//@ts-ignore document.querySelectorAll(tooltipSelector).forEach(el => {
.tooltip("hide").tooltip({container: "body", placement: "auto", boundary: 'window'}); const existing = Tooltip.getInstance(el);
if (existing) {
existing.dispose();
}
new Tooltip(el, {container: 'body', placement: 'auto', boundary: 'window'});
});
}; };
this.registerLoadHandler(handler); this.registerLoadHandler(handler);
@ -95,241 +98,239 @@ class RegisterEventHelper {
} }
registerSpecialCharInput() { registerSpecialCharInput() {
this.registerLoadHandler(() => { const keydownHandler = function(event) {
//@ts-ignore let use_special_char = event.altKey;
$("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) );
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;
})
} }
} }

20
assets/themes/brite.js Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.
*/
import "bootswatch/dist/brite/bootstrap.css";

View file

@ -57,9 +57,10 @@
"scheb/2fa-trusted-device": "^v7.11.0", "scheb/2fa-trusted-device": "^v7.11.0",
"shivas/versioning-bundle": "^4.0", "shivas/versioning-bundle": "^4.0",
"spatie/db-dumper": "^3.3.1", "spatie/db-dumper": "^3.3.1",
"symfony/ai-bundle": "^0.9.0", "symfony/ai-bundle": "^0.10.0",
"symfony/ai-lm-studio-platform": "^0.9.0", "symfony/ai-lm-studio-platform": "^v0.10.0",
"symfony/ai-open-router-platform": "^0.9.0", "symfony/ai-ollama-platform": "^0.10.0",
"symfony/ai-open-router-platform": "^0.10.0",
"symfony/apache-pack": "^1.0", "symfony/apache-pack": "^1.0",
"symfony/asset": "7.4.*", "symfony/asset": "7.4.*",
"symfony/console": "7.4.*", "symfony/console": "7.4.*",
@ -70,6 +71,7 @@
"symfony/flex": "^v2.3.1", "symfony/flex": "^v2.3.1",
"symfony/form": "7.4.*", "symfony/form": "7.4.*",
"symfony/framework-bundle": "7.4.*", "symfony/framework-bundle": "7.4.*",
"symfony/html-sanitizer": "7.4.*",
"symfony/http-client": "7.4.*", "symfony/http-client": "7.4.*",
"symfony/http-kernel": "7.4.*", "symfony/http-kernel": "7.4.*",
"symfony/mailer": "7.4.*", "symfony/mailer": "7.4.*",

1853
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -2,3 +2,4 @@ ai:
platform: platform:
lmstudio: lmstudio:
host_url: '%env(string:settings:ai_lmstudio:hostURL)%' host_url: '%env(string:settings:ai_lmstudio:hostURL)%'
http_client: 'app.http_client.ai_lmstudio'

View file

@ -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'

View file

@ -2,3 +2,4 @@ ai:
platform: platform:
openrouter: openrouter:
api_key: '%env(string:settings:ai_openrouter:apiKey)%' api_key: '%env(string:settings:ai_openrouter:apiKey)%'
http_client: 'app.http_client.ai_openrouter'

View file

@ -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

View file

@ -20,16 +20,16 @@
declare(strict_types=1); 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. * 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.
//On PHP 8.4+ we can use native lazy objects, which are much more efficient than proxies. if (PHP_VERSION_ID >= 80400) {
if (PHP_VERSION_ID >= 80400) { return ['doctrine' => ['orm' => ['enable_native_lazy_objects' => true]]];
$doctrine->orm()->enableNativeLazyObjects(true); }
}
}; return [];

View file

@ -51,6 +51,7 @@ when@prod:
type: stream type: stream
channels: [deprecation] channels: [deprecation]
path: "%kernel.logs_dir%/%kernel.environment%_deprecations.log" path: "%kernel.logs_dir%/%kernel.environment%_deprecations.log"
level: "%env(DEPRECATION_LOG_LEVEL)%"
when@docker: when@docker:
monolog: monolog:
@ -75,3 +76,4 @@ when@docker:
type: stream type: stream
channels: [deprecation] channels: [deprecation]
path: "%kernel.logs_dir%/%kernel.environment%_deprecations.log" path: "%kernel.logs_dir%/%kernel.environment%_deprecations.log"
level: "%env(DEPRECATION_LOG_LEVEL)%"

View file

@ -53,6 +53,7 @@ parameters:
# Themes commented here by default, are not really usable, because of display problems. Enable them at your own risk! # Themes commented here by default, are not really usable, because of display problems. Enable them at your own risk!
partdb.available_themes: partdb.available_themes:
- bootstrap - bootstrap
- brite
- cerulean - cerulean
- cosmo - cosmo
- cyborg - cyborg

View file

@ -121,7 +121,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* } * }
* @psalm-type ServicesConfig = array{ * @psalm-type ServicesConfig = array{
* _defaults?: DefaultsType, * _defaults?: DefaultsType,
* _instanceof?: InstanceofType, * _instanceof?: array<class-string, InstanceofType>,
* ...<string, DefinitionType|AliasType|PrototypeType|StackType|ArgumentsType|null> * ...<string, DefinitionType|AliasType|PrototypeType|StackType|ArgumentsType|null>
* } * }
* @psalm-type ExtensionType = array<string, mixed> * @psalm-type ExtensionType = array<string, mixed>
@ -653,7 +653,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* time_based_uuid_node?: scalar|Param|null, * time_based_uuid_node?: scalar|Param|null,
* }, * },
* html_sanitizer?: bool|array{ // HtmlSanitizer configuration * html_sanitizer?: bool|array{ // HtmlSanitizer configuration
* enabled?: bool|Param, // Default: false * enabled?: bool|Param, // Default: true
* sanitizers?: array<string, array{ // Default: [] * sanitizers?: array<string, array{ // Default: []
* allow_safe_elements?: bool|Param, // Allows "safe" elements and attributes. // Default: false * allow_safe_elements?: bool|Param, // Allows "safe" elements and attributes. // Default: false
* allow_static_elements?: bool|Param, // Allows all static elements and attributes from the W3C Sanitizer API standard. // Default: false * allow_static_elements?: bool|Param, // Allows all static elements and attributes from the W3C Sanitizer API standard. // Default: false
@ -718,7 +718,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* servicename?: scalar|Param|null, // Overrules dbname parameter if given and used as SERVICE_NAME or SID connection parameter for Oracle depending on the service parameter. * servicename?: scalar|Param|null, // Overrules dbname parameter if given and used as SERVICE_NAME or SID connection parameter for Oracle depending on the service parameter.
* sessionMode?: scalar|Param|null, // The session mode to use for the oci8 driver * sessionMode?: scalar|Param|null, // The session mode to use for the oci8 driver
* server?: scalar|Param|null, // The name of a running database server to connect to for SQL Anywhere. * server?: scalar|Param|null, // The name of a running database server to connect to for SQL Anywhere.
* default_dbname?: scalar|Param|null, // Override the default database (postgres) to connect to for PostgreSQL connexion. * default_dbname?: scalar|Param|null, // Override the default database (postgres) to connect to for PostgreSQL connection.
* sslmode?: scalar|Param|null, // Determines whether or with what priority a SSL TCP/IP connection will be negotiated with the server for PostgreSQL. * sslmode?: scalar|Param|null, // Determines whether or with what priority a SSL TCP/IP connection will be negotiated with the server for PostgreSQL.
* sslrootcert?: scalar|Param|null, // The name of a file containing SSL certificate authority (CA) certificate(s). If the file exists, the server's certificate will be verified to be signed by one of these authorities. * sslrootcert?: scalar|Param|null, // The name of a file containing SSL certificate authority (CA) certificate(s). If the file exists, the server's certificate will be verified to be signed by one of these authorities.
* sslcert?: scalar|Param|null, // The path to the SSL client certificate file for PostgreSQL. * sslcert?: scalar|Param|null, // The path to the SSL client certificate file for PostgreSQL.
@ -769,7 +769,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* servicename?: scalar|Param|null, // Overrules dbname parameter if given and used as SERVICE_NAME or SID connection parameter for Oracle depending on the service parameter. * servicename?: scalar|Param|null, // Overrules dbname parameter if given and used as SERVICE_NAME or SID connection parameter for Oracle depending on the service parameter.
* sessionMode?: scalar|Param|null, // The session mode to use for the oci8 driver * sessionMode?: scalar|Param|null, // The session mode to use for the oci8 driver
* server?: scalar|Param|null, // The name of a running database server to connect to for SQL Anywhere. * server?: scalar|Param|null, // The name of a running database server to connect to for SQL Anywhere.
* default_dbname?: scalar|Param|null, // Override the default database (postgres) to connect to for PostgreSQL connexion. * default_dbname?: scalar|Param|null, // Override the default database (postgres) to connect to for PostgreSQL connection.
* sslmode?: scalar|Param|null, // Determines whether or with what priority a SSL TCP/IP connection will be negotiated with the server for PostgreSQL. * sslmode?: scalar|Param|null, // Determines whether or with what priority a SSL TCP/IP connection will be negotiated with the server for PostgreSQL.
* sslrootcert?: scalar|Param|null, // The name of a file containing SSL certificate authority (CA) certificate(s). If the file exists, the server's certificate will be verified to be signed by one of these authorities. * sslrootcert?: scalar|Param|null, // The name of a file containing SSL certificate authority (CA) certificate(s). If the file exists, the server's certificate will be verified to be signed by one of these authorities.
* sslcert?: scalar|Param|null, // The path to the SSL client certificate file for PostgreSQL. * sslcert?: scalar|Param|null, // The path to the SSL client certificate file for PostgreSQL.
@ -801,7 +801,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* servicename?: scalar|Param|null, // Overrules dbname parameter if given and used as SERVICE_NAME or SID connection parameter for Oracle depending on the service parameter. * servicename?: scalar|Param|null, // Overrules dbname parameter if given and used as SERVICE_NAME or SID connection parameter for Oracle depending on the service parameter.
* sessionMode?: scalar|Param|null, // The session mode to use for the oci8 driver * sessionMode?: scalar|Param|null, // The session mode to use for the oci8 driver
* server?: scalar|Param|null, // The name of a running database server to connect to for SQL Anywhere. * server?: scalar|Param|null, // The name of a running database server to connect to for SQL Anywhere.
* default_dbname?: scalar|Param|null, // Override the default database (postgres) to connect to for PostgreSQL connexion. * default_dbname?: scalar|Param|null, // Override the default database (postgres) to connect to for PostgreSQL connection.
* sslmode?: scalar|Param|null, // Determines whether or with what priority a SSL TCP/IP connection will be negotiated with the server for PostgreSQL. * sslmode?: scalar|Param|null, // Determines whether or with what priority a SSL TCP/IP connection will be negotiated with the server for PostgreSQL.
* sslrootcert?: scalar|Param|null, // The name of a file containing SSL certificate authority (CA) certificate(s). If the file exists, the server's certificate will be verified to be signed by one of these authorities. * sslrootcert?: scalar|Param|null, // The name of a file containing SSL certificate authority (CA) certificate(s). If the file exists, the server's certificate will be verified to be signed by one of these authorities.
* sslcert?: scalar|Param|null, // The path to the SSL client certificate file for PostgreSQL. * sslcert?: scalar|Param|null, // The path to the SSL client certificate file for PostgreSQL.
@ -2436,6 +2436,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* }, * },
* jsonapi?: array{ * jsonapi?: array{
* use_iri_as_id?: bool|Param, // Set to false to use entity identifiers instead of IRIs as the "id" field in JSON:API responses. // Default: true * use_iri_as_id?: bool|Param, // Set to false to use entity identifiers instead of IRIs as the "id" field in JSON:API responses. // Default: true
* allow_client_generated_id?: bool|Param, // Allow client-generated IDs on JSON:API POST per https://jsonapi.org/format/#crud-creating-client-ids. Off by default to prevent id spoofing on public endpoints. // Default: false
* }, * },
* eager_loading?: bool|array{ * eager_loading?: bool|array{
* enabled?: bool|Param, // Default: true * enabled?: bool|Param, // Default: true
@ -2873,8 +2874,8 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* enable_translation?: bool|Param, // Enable translation for the system prompt // Default: false * enable_translation?: bool|Param, // Enable translation for the system prompt // Default: false
* translation_domain?: string|Param, // The translation domain for the system prompt // Default: null * translation_domain?: string|Param, // The translation domain for the system prompt // Default: null
* }, * },
* tools?: bool|array{ * tools?: bool|array{ // Tools are opt-in: set to true to inject all services tagged with "ai.tool", or configure an explicit list of tools. When the option is omitted (or set to null or false), no tools are registered.
* enabled?: bool|Param, // Default: true * enabled?: bool|Param, // Default: false
* services?: list<string|array{ // Default: [] * services?: list<string|array{ // Default: []
* service?: string|Param, * service?: string|Param,
* agent?: string|Param, * agent?: string|Param,
@ -2885,6 +2886,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* }, * },
* keep_tool_messages?: bool|Param, // Keep tool messages in the conversation history // Default: false * keep_tool_messages?: bool|Param, // Keep tool messages in the conversation history // Default: false
* include_sources?: bool|Param, // Include sources exposed by tools as part of the tool result metadata // Default: false * include_sources?: bool|Param, // Include sources exposed by tools as part of the tool result metadata // Default: false
* max_tool_calls?: scalar|Param|null, // Maximum number of tool calls per agent call, null to disable // Default: 50
* fault_tolerant_toolbox?: bool|Param, // Continue the agent run even if a tool call fails // Default: true * fault_tolerant_toolbox?: bool|Param, // Continue the agent run even if a tool call fails // Default: true
* speech?: bool|array{ // Speech (TTS/STT) decorator configuration * speech?: bool|array{ // Speech (TTS/STT) decorator configuration
* enabled?: bool|Param, // Default: true * enabled?: bool|Param, // Default: true

View file

@ -52,6 +52,28 @@ services:
alias: 'doctrine.migrations.dependency_factory' alias: 'doctrine.migrations.dependency_factory'
####################################################################################################################
# AI provider HTTP clients (with configurable timeouts)
####################################################################################################################
app.http_client.ai_ollama:
class: Symfony\Contracts\HttpClient\HttpClientInterface
factory: ['@http_client', 'withOptions']
arguments:
- { timeout: '%env(int:settings:ai_ollama:timeout)%' }
app.http_client.ai_lmstudio:
class: Symfony\Contracts\HttpClient\HttpClientInterface
factory: ['@http_client', 'withOptions']
arguments:
- { timeout: '%env(int:settings:ai_lmstudio:timeout)%' }
app.http_client.ai_openrouter:
class: Symfony\Contracts\HttpClient\HttpClientInterface
factory: ['@http_client', 'withOptions']
arguments:
- { timeout: '%env(int:settings:ai_openrouter:timeout)%' }
#################################################################################################################### ####################################################################################################################
# Email # Email
#################################################################################################################### ####################################################################################################################

View file

@ -114,10 +114,21 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept
* `datastructure_create`: Creation of a new data structure (e.g. category, manufacturer, ...) * `datastructure_create`: Creation of a new data structure (e.g. category, manufacturer, ...)
* `CHECK_FOR_UPDATES` (default `1`): Set this to 0 if you do not want Part-DB to connect to GitHub to check for new * `CHECK_FOR_UPDATES` (default `1`): Set this to 0 if you do not want Part-DB to connect to GitHub to check for new
versions, or if your server cannot connect to the internet. versions, or if your server cannot connect to the internet.
* `APP_SECRET` (env only): This variable is a configuration parameter used for various security-related purposes, * `APP_SECRET` (env only): A secret key used by Symfony for cryptographic operations — signing cookies, generating
particularly for securing and protecting various aspects of your application. It's a secret key that is used for CSRF tokens, and other security-sensitive tasks. **You must change this from the default value before exposing
cryptographic operations and security measures (session management, CSRF protection, etc..). Therefore this Part-DB to any network.** The default value shipped with Part-DB is publicly known; leaving it in place would allow
value should be handled as confidential data and not shared publicly. an attacker to forge signed cookies and bypass CSRF protection.
Generate a secure value and add it to `.env.local`:
```bash
echo "APP_SECRET=$(openssl rand -hex 32)" >> .env.local
```
For Docker, pass it in the `environment` section of your `docker-compose.yaml`:
```yaml
environment:
- APP_SECRET=<output of: openssl rand -hex 32>
```
Part-DB displays a warning on the homepage (visible to administrators only) as long as the default value is in use.
* `SHOW_PART_IMAGE_OVERLAY`: Set to 0 to disable the part image overlay, which appears if you hover over an image in the * `SHOW_PART_IMAGE_OVERLAY`: Set to 0 to disable the part image overlay, which appears if you hover over an image in the
part image gallery part image gallery
* `IPN_SUGGEST_REGEX`: A global regular expression, that part IPNs have to fulfill. Enforce your own format for your users. * `IPN_SUGGEST_REGEX`: A global regular expression, that part IPNs have to fulfill. Enforce your own format for your users.
@ -268,9 +279,13 @@ See the [information providers]({% link usage/information_provider_system.md %})
* `BANNER`: You can configure the text that should be shown as the banner on the homepage. Useful especially for docker * `BANNER`: You can configure the text that should be shown as the banner on the homepage. Useful especially for docker
containers. In all other applications you can just change the `config/banner.md` file. containers. In all other applications you can just change the `config/banner.md` file.
* `DISABLE_YEAR2038_BUG_CHECK` (env only): If set to `1`, the year 2038 bug check is disabled on 32-bit systems, and dates after * `DISABLE_YEAR2038_BUG_CHECK` (env only): If set to `1`, the year 2038 bug check is disabled on 32-bit systems, and dates after
2038 are no longer forbidden. However this will lead to 500 error messages when rendering dates after 2038 as all current 2038 are no longer forbidden. However, this will lead to 500 error messages when rendering dates after 2038 as all current
32-bit PHP versions can not format these dates correctly. This setting is for the case that future PHP versions will 32-bit PHP versions can not format these dates correctly. This setting is for the case that future PHP versions will
handle this correctly on 32-bit systems. 64-bit systems are not affected by this bug, and the check is always disabled. handle this correctly on 32-bit systems. 64-bit systems are not affected by this bug, and the check is always disabled.
* `DEPRECATION_LOG_LEVEL` (default `emergency`) (env only): In the `prod` and `docker` environments, PHP/Symfony
deprecation notices are written to their own `var/log/<env>_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 ## Banner

View file

@ -47,6 +47,9 @@ services:
- DATABASE_URL=sqlite:///%kernel.project_dir%/var/db/app.db - DATABASE_URL=sqlite:///%kernel.project_dir%/var/db/app.db
# In docker env logs will be redirected to stderr # In docker env logs will be redirected to stderr
- APP_ENV=docker - APP_ENV=docker
# Secret key used to sign cookies and CSRF tokens. MUST be changed to a unique random value before going live!
# Generate one with: openssl rand -hex 32
- APP_SECRET=CHANGE_ME
# Uncomment this, if you want to use the automatic database migration feature. With this you have you do not have to # Uncomment this, if you want to use the automatic database migration feature. With this you have you do not have to
# run the doctrine:migrations:migrate commands on installation or upgrade. A database backup is written to the uploads/ # run the doctrine:migrations:migrate commands on installation or upgrade. A database backup is written to the uploads/
@ -89,7 +92,11 @@ services:
4. Customize the settings by changing the environment variables (or adding new ones). See [Configuration]({% link 4. Customize the settings by changing the environment variables (or adding new ones). See [Configuration]({% link
configuration.md %}) for more information. configuration.md %}) for more information.
5. Inside the folder, run 5. Make sure to change the `APP_SECRET` variable to a unique random value (32 characters). You can generate one with the following command:
```bash
openssl rand -hex 32
```
6. Inside the folder, run
```bash ```bash
docker-compose up -d docker-compose up -d
@ -100,7 +107,7 @@ services:
> Otherwise Part-DB console might use the wrong configuration to execute commands. > Otherwise Part-DB console might use the wrong configuration to execute commands.
6. Create the initial database with 7. Create the initial database with
```bash ```bash
docker exec --user=www-data partdb php bin/console doctrine:migrations:migrate docker exec --user=www-data partdb php bin/console doctrine:migrations:migrate
@ -108,7 +115,7 @@ docker exec --user=www-data partdb php bin/console doctrine:migrations:migrate
and watch for the password output and watch for the password output
6. Part-DB is available under `http://localhost:8080` and you can log in with the username `admin` and the password shown 8. Part-DB is available under `http://localhost:8080` and you can log in with the username `admin` and the password shown
before before
The docker image uses a SQLite database and all data (database, uploads, and other media) is put into folders relative to The docker image uses a SQLite database and all data (database, uploads, and other media) is put into folders relative to
@ -121,6 +128,7 @@ If you want to use MySQL as a database, you can use the following docker-compose
{: .warning } {: .warning }
> You have to replace the values for MYSQL_ROOT_PASSWORD and MYSQL_PASSWORD with your own passwords!! > You have to replace the values for MYSQL_ROOT_PASSWORD and MYSQL_PASSWORD with your own passwords!!
> You have to change MYSQL_PASSWORD in the database section and for the DATABASE_URL in the partdb section. > You have to change MYSQL_PASSWORD in the database section and for the DATABASE_URL in the partdb section.
> Generate a random string for APP_SECRET.
```yaml ```yaml
version: '3.3' version: '3.3'
@ -142,14 +150,19 @@ services:
environment: environment:
# Replace SECRET_USER_PASSWORD with the value of MYSQL_PASSWORD from below # Replace SECRET_USER_PASSWORD with the value of MYSQL_PASSWORD from below
- DATABASE_URL=mysql://partdb:SECRET_USER_PASSWORD@database:3306/partdb - DATABASE_URL=mysql://partdb:SECRET_USER_PASSWORD@database:3306/partdb
# Secret key used to sign cookies. MUST be changed to a unique random value before going live!
# Generate one with: openssl rand -hex 32
- APP_SECRET=CHANGE_ME
# In docker env logs will be redirected to stderr # In docker env logs will be redirected to stderr
- APP_ENV=docker - APP_ENV=docker
# Uncomment this, if you want to use the automatic database migration feature. With this you do not have to # Uncomment this, if you want to use the automatic database migration feature. With this you do not have to
# run the doctrine:migrations:migrate commands on installation or upgrade. A database backup is written to the uploads/ # run the doctrine:migrations:migrate commands on installation or upgrade. A database backup is written to the uploads/
# folder (under .automigration-backup), so you can restore it, if the migration fails. # folder (under .automigration-backup), so you can restore it, if the migration fails.
# This feature is currently experimental, so use it at your own risk! # This feature is currently experimental, so use it at your own risk!
# - DB_AUTOMIGRATE=true # - DB_AUTOMIGRATE=true
# You can configure Part-DB using the webUI or environment variables # You can configure Part-DB using the webUI or environment variables
# However you can add any other environment configuration you want here # However you can add any other environment configuration you want here

View file

@ -136,6 +136,15 @@ cp .env .env.local
In your `.env.local` you can configure Part-DB according to your wishes and overwrite web interface settings. In your `.env.local` you can configure Part-DB according to your wishes and overwrite web interface settings.
A full list of configuration options can be found [here](../configuration.md). A full list of configuration options can be found [here](../configuration.md).
{: .important }
> **Change `APP_SECRET` before going live.** The default value shipped with Part-DB is publicly known and must not be
> used in production — it would allow an attacker to forge signed cookies and bypass CSRF protection.
> Generate a new value and add it to your `.env.local`:
> ```bash
> echo "APP_SECRET=$(openssl rand -hex 32)" >> .env.local
> ```
> or edit the file with a text editor and add a new value for `APP_SECRET` (you can generate a random value with `openssl rand -hex 32`).
Please check that the configured base currency matches your mainly used currency, as Please check that the configured base currency matches your mainly used currency, as
this can not be changed after creating price information. this can not be changed after creating price information.
@ -223,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`): Configure apache to show pretty URL paths for Part-DB (`/label/dialog` instead of `/index.php/label/dialog`):
```bash ```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 If you want to access Part-DB via the IP-Address of the server, instead of the domain name, you have to remove the

View file

@ -36,6 +36,10 @@ server {
root /var/www/partdb/public; root /var/www/partdb/public;
location / { 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; try_files $uri /index.php$is_args$args;
} }
@ -53,9 +57,16 @@ server {
return 404; return 404;
} }
# Set Content-Security-Policy for svg files, to block embedded javascript in there # Prevent PHP execution in the media upload directory
location ~* ^/media/.*\.(php[3-8]?|phar|phtml|pht|phps)$ {
return 403;
}
# 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$ { 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; error_log /var/log/nginx/parts.error.log;

View file

@ -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. [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. 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.

View file

@ -13,7 +13,7 @@
"bootstrap": "^5.1.3", "bootstrap": "^5.1.3",
"core-js": "^3.38.0", "core-js": "^3.38.0",
"intl-messageformat": "^10.5.11", "intl-messageformat": "^10.5.11",
"jquery": "^3.5.1", "jquery": "^4.0.0",
"popper.js": "^1.14.7", "popper.js": "^1.14.7",
"regenerator-runtime": "^0.14.1", "regenerator-runtime": "^0.14.1",
"webpack": "^5.74.0", "webpack": "^5.74.0",
@ -38,20 +38,21 @@
"@algolia/autocomplete-theme-classic": "^1.17.0", "@algolia/autocomplete-theme-classic": "^1.17.0",
"@jbtronics/bs-treeview": "^1.0.1", "@jbtronics/bs-treeview": "^1.0.1",
"@part-db/html5-qrcode": "^4.0.0", "@part-db/html5-qrcode": "^4.0.0",
"@zxcvbn-ts/core": "^3.0.2", "@zxcvbn-ts/core": "^4.1.2",
"@zxcvbn-ts/language-common": "^3.0.3", "@zxcvbn-ts/language-common": "^4.1.2",
"@zxcvbn-ts/language-de": "^3.0.1", "@zxcvbn-ts/language-de": "^4.1.1",
"@zxcvbn-ts/language-en": "^3.0.1", "@zxcvbn-ts/language-en": "^4.1.1",
"@zxcvbn-ts/language-fr": "^3.0.1", "@zxcvbn-ts/language-fr": "^4.1.1",
"@zxcvbn-ts/language-ja": "^3.0.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", "attr-accept": "^2.2.5",
"barcode-detector": "^3.0.5", "barcode-detector": "^3.0.5",
"bootbox": "^6.0.0",
"bootswatch": "^5.1.3", "bootswatch": "^5.1.3",
"bs-custom-file-input": "^1.3.4", "bs-custom-file-input": "^1.3.4",
"ckeditor5": "^48.0.0", "ckeditor5": "^48.0.0",
"clipboard": "^2.0.4", "clipboard": "^2.0.4",
"compression-webpack-plugin": "^11.1.0", "compression-webpack-plugin": "^12.0.0",
"datatables.net": "^2.0.0", "datatables.net": "^2.0.0",
"datatables.net-bs5": "^2.0.0", "datatables.net-bs5": "^2.0.0",
"datatables.net-buttons-bs5": "^3.0.0", "datatables.net-buttons-bs5": "^3.0.0",
@ -69,11 +70,12 @@
"marked-mangle": "^1.0.1", "marked-mangle": "^1.0.1",
"pdfmake": "^0.3.7", "pdfmake": "^0.3.7",
"stimulus-use": "^0.52.0", "stimulus-use": "^0.52.0",
"sweetalert2": "^11.26.25",
"tom-select": "^2.1.0", "tom-select": "^2.1.0",
"ts-loader": "^9.2.6", "ts-loader": "^9.2.6",
"typescript": "^6.0.2" "typescript": "^6.0.2"
}, },
"resolutions": { "resolutions": {
"jquery": "^3.5.1" "jquery": "^4.0.0"
} }
} }

View file

@ -119,9 +119,14 @@ DirectoryIndex index.php
</IfModule> </IfModule>
</IfModule> </IfModule>
# Set Content-Security-Policy for svg files (and compressed variants), to block embedded javascript in there
<IfModule mod_headers.c> <IfModule mod_headers.c>
# Set a strict CSP for all static assets not handled by PHP.
# PHP responses already carry their own CSP via NelmioSecurityBundle, so setifempty leaves those untouched.
Header always setifempty Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; sandbox;"
Header always setifempty X-Content-Type-Options "nosniff"
# SVG files get a slightly different CSP because they can embed resources and must not be framed.
<FilesMatch "\.(svg|svg\.gz|svg\.br)$"> <FilesMatch "\.(svg|svg\.gz|svg\.br)$">
Header set Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none';" Header always set Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'; sandbox;"
</FilesMatch> </FilesMatch>
</IfModule> </IfModule>

View file

@ -1,4 +1,4 @@
# Generated on Mon May 25 06:41:46 UTC 2026 # Generated on Mon Jun 22 07:31:48 UTC 2026
# This file contains all footprints available in the offical KiCAD library # This file contains all footprints available in the offical KiCAD library
Audio_Module:Reverb_BTDR-1H Audio_Module:Reverb_BTDR-1H
Audio_Module:Reverb_BTDR-1V 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_NID30_THT
Converter_DCDC:Converter_DCDC_MeanWell_NID60_THT Converter_DCDC:Converter_DCDC_MeanWell_NID60_THT
Converter_DCDC:Converter_DCDC_MeanWell_NSD10_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_CRE1xxxxxx3C_THT
Converter_DCDC:Converter_DCDC_Murata_CRE1xxxxxxDC_THT Converter_DCDC:Converter_DCDC_Murata_CRE1xxxxxxDC_THT
Converter_DCDC:Converter_DCDC_Murata_CRE1xxxxxxSC_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_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
Package_DFN_QFN:WDFN-8-1EP_8x6mm_P1.27mm_EP6x4.8mm_ThermalVias 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: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
Package_DFN_QFN:WQFN-14-1EP_2.5x2.5mm_P0.5mm_EP1.45x1.45mm_ThermalVias 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_DirectFET:DirectFET_ST
Package_LCC:Analog_LCC-8_5x5mm_P1.27mm Package_LCC:Analog_LCC-8_5x5mm_P1.27mm
Package_LCC:MO047AD_PLCC-52_19.1x19.1mm_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_9.0x9.0mm_P1.27mm
Package_LCC:PLCC-20_SMD-Socket Package_LCC:PLCC-20_SMD-Socket
Package_LCC:PLCC-20_THT-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_2x2mm_P0.65mm_EP1x1.6mm_ThermalVias
Package_SON:WSON-6-1EP_3x3mm_P0.95mm Package_SON:WSON-6-1EP_3x3mm_P0.95mm
Package_SON:WSON-6_1.5x1.5mm_P0.5mm 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
Package_SON:WSON-8-1EP_3x2.5mm_P0.5mm_EP1.2x1.5mm_PullBack_ThermalVias Package_SON:WSON-8-1EP_3x2.5mm_P0.5mm_EP1.2x1.5mm_PullBack_ThermalVias
Package_SON:WSON-8-1EP_3x3mm_P0.5mm_EP1.2x2mm Package_SON:WSON-8-1EP_3x3mm_P0.5mm_EP1.2x2mm

View file

@ -1,4 +1,4 @@
# Generated on Mon May 25 06:42:27 UTC 2026 # Generated on Mon Jun 22 07:32:26 UTC 2026
# This file contains all symbols available in the offical KiCAD library # This file contains all symbols available in the offical KiCAD library
4xxx:14528 4xxx:14528
4xxx:14529 4xxx:14529
@ -2255,11 +2255,11 @@ Battery_Management:BQ76200PW
Battery_Management:BQ76920PW Battery_Management:BQ76920PW
Battery_Management:BQ76930DBT Battery_Management:BQ76930DBT
Battery_Management:BQ76940DBT Battery_Management:BQ76940DBT
Battery_Management:BQ7695201PFBR Battery_Management:BQ7695201PFB
Battery_Management:BQ7695202PFBR Battery_Management:BQ7695202PFB
Battery_Management:BQ7695203PFBR Battery_Management:BQ7695203PFB
Battery_Management:BQ7695204PFBR Battery_Management:BQ7695204PFB
Battery_Management:BQ76952PFBR Battery_Management:BQ76952PFB
Battery_Management:BQ78350DBT Battery_Management:BQ78350DBT
Battery_Management:BQ78350DBT-R1 Battery_Management:BQ78350DBT-R1
Battery_Management:CN3063 Battery_Management:CN3063
@ -4954,6 +4954,7 @@ Converter_DCDC:RPMH15-1.5
Converter_DCDC:RPMH24-1.5 Converter_DCDC:RPMH24-1.5
Converter_DCDC:RPMH3.3-1.5 Converter_DCDC:RPMH3.3-1.5
Converter_DCDC:RPMH5.0-1.5 Converter_DCDC:RPMH5.0-1.5
Converter_DCDC:SMU02L-24N
Converter_DCDC:TBA1-0310 Converter_DCDC:TBA1-0310
Converter_DCDC:TBA1-0311 Converter_DCDC:TBA1-0311
Converter_DCDC:TBA1-0510 Converter_DCDC:TBA1-0510
@ -5300,21 +5301,39 @@ Converter_DCDC:TMR-4812
Converter_DCDC:TMR-4821 Converter_DCDC:TMR-4821
Converter_DCDC:TMR-4822 Converter_DCDC:TMR-4822
Converter_DCDC:TMR-4823 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-2410WIR
Converter_DCDC:TMR10-2411WI
Converter_DCDC:TMR10-2411WIR Converter_DCDC:TMR10-2411WIR
Converter_DCDC:TMR10-2412WI
Converter_DCDC:TMR10-2412WIR Converter_DCDC:TMR10-2412WIR
Converter_DCDC:TMR10-2413WI
Converter_DCDC:TMR10-2413WIR Converter_DCDC:TMR10-2413WIR
Converter_DCDC:TMR10-2415WI
Converter_DCDC:TMR10-2415WIR Converter_DCDC:TMR10-2415WIR
Converter_DCDC:TMR10-2421WIR Converter_DCDC:TMR10-2421WIR
Converter_DCDC:TMR10-2422WI
Converter_DCDC:TMR10-2422WIR Converter_DCDC:TMR10-2422WIR
Converter_DCDC:TMR10-2423WI
Converter_DCDC:TMR10-2423WIR Converter_DCDC:TMR10-2423WIR
Converter_DCDC:TMR10-4810WIR Converter_DCDC:TMR10-4810WIR
Converter_DCDC:TMR10-4811WI
Converter_DCDC:TMR10-4811WIR Converter_DCDC:TMR10-4811WIR
Converter_DCDC:TMR10-4812WI
Converter_DCDC:TMR10-4812WIR Converter_DCDC:TMR10-4812WIR
Converter_DCDC:TMR10-4813WI
Converter_DCDC:TMR10-4813WIR Converter_DCDC:TMR10-4813WIR
Converter_DCDC:TMR10-4815WI
Converter_DCDC:TMR10-4815WIR Converter_DCDC:TMR10-4815WIR
Converter_DCDC:TMR10-4821WIR Converter_DCDC:TMR10-4821WIR
Converter_DCDC:TMR10-4822WI
Converter_DCDC:TMR10-4822WIR Converter_DCDC:TMR10-4822WIR
Converter_DCDC:TMR10-4823WI
Converter_DCDC:TMR10-4823WIR Converter_DCDC:TMR10-4823WIR
Converter_DCDC:TMR10-7210WIR Converter_DCDC:TMR10-7210WIR
Converter_DCDC:TMR10-7211WIR Converter_DCDC:TMR10-7211WIR
@ -6124,6 +6143,7 @@ Device:SparkGap
Device:Speaker Device:Speaker
Device:Speaker_Crystal Device:Speaker_Crystal
Device:Speaker_Ultrasound Device:Speaker_Ultrasound
Device:Thermal_Jumper
Device:Thermistor Device:Thermistor
Device:Thermistor_NTC Device:Thermistor_NTC
Device:Thermistor_NTC_3Wire Device:Thermistor_NTC_3Wire
@ -14593,6 +14613,8 @@ MCU_Texas:LM4F111C4QR
MCU_Texas:LM4F111E5QR MCU_Texas:LM4F111E5QR
MCU_Texas:LM4F111H5QR MCU_Texas:LM4F111H5QR
MCU_Texas:MSP432E401Y MCU_Texas:MSP432E401Y
MCU_Texas:MSPM0C110xSDDF
MCU_Texas:MSPM0C110xSDSG
MCU_Texas:TM4C1230C3PM MCU_Texas:TM4C1230C3PM
MCU_Texas:TM4C1230D5PM MCU_Texas:TM4C1230D5PM
MCU_Texas:TM4C1230E6PM MCU_Texas:TM4C1230E6PM
@ -15115,6 +15137,7 @@ Memory_Flash:AM29F400Bx-xxEx
Memory_Flash:AM29F400Bx-xxSx Memory_Flash:AM29F400Bx-xxSx
Memory_Flash:AM29PDL128G Memory_Flash:AM29PDL128G
Memory_Flash:AT25DF041x-UxN-x Memory_Flash:AT25DF041x-UxN-x
Memory_Flash:AT25SF041B-SSHD-X
Memory_Flash:AT25SF081-SSHD-X Memory_Flash:AT25SF081-SSHD-X
Memory_Flash:AT25SF081-SSHF-X Memory_Flash:AT25SF081-SSHF-X
Memory_Flash:AT25SF081-XMHD-X Memory_Flash:AT25SF081-XMHD-X
@ -15670,6 +15693,7 @@ Power_Management:LM5069MM-1
Power_Management:LM5069MM-2 Power_Management:LM5069MM-2
Power_Management:LM66100DCK Power_Management:LM66100DCK
Power_Management:LM74700 Power_Management:LM74700
Power_Management:LM74701-Q1
Power_Management:LMG3410 Power_Management:LMG3410
Power_Management:LMG5200 Power_Management:LMG5200
Power_Management:LT1641-1 Power_Management:LT1641-1
@ -15771,6 +15795,7 @@ Power_Management:TPS22810DBV
Power_Management:TPS22810DRV Power_Management:TPS22810DRV
Power_Management:TPS22917DBV Power_Management:TPS22917DBV
Power_Management:TPS22917LDBV Power_Management:TPS22917LDBV
Power_Management:TPS22919DCK
Power_Management:TPS22929D Power_Management:TPS22929D
Power_Management:TPS22993 Power_Management:TPS22993
Power_Management:TPS2412D Power_Management:TPS2412D
@ -16037,6 +16062,7 @@ Power_Supervisor:TPS3831
Power_Supervisor:TPS3839DBZ Power_Supervisor:TPS3839DBZ
Power_Supervisor:TPS3839DQN Power_Supervisor:TPS3839DQN
RF:0900PC15J0013 RF:0900PC15J0013
RF:AD8302xRU
RF:ADC-10-1R RF:ADC-10-1R
RF:ADCH-80 RF:ADCH-80
RF:ADCH-80A RF:ADCH-80A
@ -16479,6 +16505,7 @@ RF_Module:DWM3000
RF_Module:E18-MS1-PCB RF_Module:E18-MS1-PCB
RF_Module:E73-2G4M04S-52810 RF_Module:E73-2G4M04S-52810
RF_Module:E73-2G4M04S-52832 RF_Module:E73-2G4M04S-52832
RF_Module:ESP-01
RF_Module:ESP-07 RF_Module:ESP-07
RF_Module:ESP-12E RF_Module:ESP-12E
RF_Module:ESP-12F RF_Module:ESP-12F
@ -16680,6 +16707,19 @@ Reference_Voltage:LM4040LP-4.1
Reference_Voltage:LM4040LP-5 Reference_Voltage:LM4040LP-5
Reference_Voltage:LM4040LP-8.2 Reference_Voltage:LM4040LP-8.2
Reference_Voltage:LM4041LP-ADJ 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:LM4125AIM5-2.5
Reference_Voltage:LM4125IM5-2.0 Reference_Voltage:LM4125IM5-2.0
Reference_Voltage:LM4125IM5-2.5 Reference_Voltage:LM4125IM5-2.5
@ -16827,7 +16867,6 @@ Reference_Voltage:MCP1501-25xCH
Reference_Voltage:MCP1501-25xRW Reference_Voltage:MCP1501-25xRW
Reference_Voltage:MCP1501-25xSN Reference_Voltage:MCP1501-25xSN
Reference_Voltage:MCP1501-30xCH Reference_Voltage:MCP1501-30xCH
Reference_Voltage:MCP1501-30xRW
Reference_Voltage:MCP1501-30xSN Reference_Voltage:MCP1501-30xSN
Reference_Voltage:MCP1501-33xCH Reference_Voltage:MCP1501-33xCH
Reference_Voltage:MCP1501-33xRW Reference_Voltage:MCP1501-33xRW
@ -19260,6 +19299,7 @@ Regulator_Switching:LT1373HVCN8
Regulator_Switching:LT1373HVCS8 Regulator_Switching:LT1373HVCS8
Regulator_Switching:LT1377CN8 Regulator_Switching:LT1377CN8
Regulator_Switching:LT1377CS8 Regulator_Switching:LT1377CS8
Regulator_Switching:LT1931
Regulator_Switching:LT1945 Regulator_Switching:LT1945
Regulator_Switching:LT3430 Regulator_Switching:LT3430
Regulator_Switching:LT3430-1 Regulator_Switching:LT3430-1
@ -20988,6 +21028,7 @@ Sensor_Temperature:MCP9501
Sensor_Temperature:MCP9502 Sensor_Temperature:MCP9502
Sensor_Temperature:MCP9503 Sensor_Temperature:MCP9503
Sensor_Temperature:MCP9504 Sensor_Temperature:MCP9504
Sensor_Temperature:MCP96xx01x-x-MX
Sensor_Temperature:MCP9700Ax-ELT Sensor_Temperature:MCP9700Ax-ELT
Sensor_Temperature:MCP9700Ax-ETT Sensor_Temperature:MCP9700Ax-ETT
Sensor_Temperature:MCP9700Ax-HLT Sensor_Temperature:MCP9700Ax-HLT
@ -22478,6 +22519,7 @@ Transistor_FET_Other:Q_NMOS_Depletion_GDS
Transistor_FET_Other:Q_NMOS_Depletion_GSD Transistor_FET_Other:Q_NMOS_Depletion_GSD
Transistor_FET_Other:Q_NMOS_Depletion_SDG Transistor_FET_Other:Q_NMOS_Depletion_SDG
Transistor_FET_Other:Q_NMOS_Depletion_SGD Transistor_FET_Other:Q_NMOS_Depletion_SGD
Transistor_FET_Other:SP010N70T8
Transistor_FET_Other:VNB35N07xx-E Transistor_FET_Other:VNB35N07xx-E
Transistor_FET_Other:VNP10N07 Transistor_FET_Other:VNP10N07
Transistor_FET_Other:VNP35N07xx-E Transistor_FET_Other:VNP35N07xx-E

View file

@ -1,3 +1,4 @@
# Ignore everything except this .gitignore # Ignore everything except this .gitignore
* *
!.gitignore !.gitignore
!.htaccess

10
public/media/.htaccess Normal file
View file

@ -0,0 +1,10 @@
# Deny access to PHP and PHP-like files to prevent remote code execution
<FilesMatch "(?i)\.(php[3-8]?|phar|phtml|pht|phps)$">
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
Order deny,allow
Deny from all
</IfModule>
</FilesMatch>

View file

@ -22,6 +22,7 @@ declare(strict_types=1);
*/ */
namespace App\Command; namespace App\Command;
use App\Services\System\AppSecretChecker;
use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
@ -33,7 +34,9 @@ use Symfony\Component\DependencyInjection\ParameterBag\ContainerBagInterface;
#[AsCommand('partdb:check-requirements', 'Checks if the requirements Part-DB needs or recommends are fulfilled.')] #[AsCommand('partdb:check-requirements', 'Checks if the requirements Part-DB needs or recommends are fulfilled.')]
class CheckRequirementsCommand extends Command class CheckRequirementsCommand extends Command
{ {
public function __construct(protected ContainerBagInterface $params) public function __construct(protected ContainerBagInterface $params,
private readonly AppSecretChecker $appSecretChecker
)
{ {
parent::__construct(); parent::__construct();
} }
@ -121,6 +124,16 @@ class CheckRequirementsCommand extends Command
$io->success('Debug mode disabled.'); $io->success('Debug mode disabled.');
} }
//Check if APP_SECRET has been changed from the default
if ($io->isVerbose()) {
$io->comment('Checking APP_SECRET...');
}
if ($this->appSecretChecker->isInsecureSecret()) {
$io->warning('APP_SECRET is set to the default value shipped with Part-DB. This is a security risk! Generate a new secret (e.g. using "openssl rand -hex 32") and set it as APP_SECRET in your .env.local file.');
} elseif (!$only_issues) {
$io->success('APP_SECRET has been changed from the default value.');
}
} }
protected function checkPHPExtensions(SymfonyStyle $io, bool $only_issues = false): void protected function checkPHPExtensions(SymfonyStyle $io, bool $only_issues = false): void

View file

@ -93,6 +93,8 @@ class AttachmentFileController extends AbstractController
//Set header content disposition, so that the file will be downloaded //Set header content disposition, so that the file will be downloaded
$response->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $attachment->getFilename()); $response->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $attachment->getFilename());
$this->setAttachmentCSPHeaders($response);
return $response; return $response;
} }
@ -112,6 +114,16 @@ class AttachmentFileController extends AbstractController
//Set header content disposition, so that the file will be downloaded //Set header content disposition, so that the file will be downloaded
$response->setContentDisposition(ResponseHeaderBag::DISPOSITION_INLINE, $attachment->getFilename()); $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; return $response;
} }

View file

@ -24,6 +24,7 @@ namespace App\Controller;
use App\DataTables\LogDataTable; use App\DataTables\LogDataTable;
use App\Entity\Parts\Part; use App\Entity\Parts\Part;
use App\Services\System\AppSecretChecker;
use App\Services\System\BannerHelper; use App\Services\System\BannerHelper;
use App\Services\System\GitVersionInfoProvider; use App\Services\System\GitVersionInfoProvider;
use App\Services\System\UpdateAvailableFacade; use App\Services\System\UpdateAvailableFacade;
@ -36,8 +37,11 @@ use Symfony\Component\Routing\Attribute\Route;
class HomepageController extends AbstractController class HomepageController extends AbstractController
{ {
public function __construct(private readonly DataTableFactory $dataTable, private readonly BannerHelper $bannerHelper) public function __construct(
{ private readonly DataTableFactory $dataTable,
private readonly BannerHelper $bannerHelper,
private readonly AppSecretChecker $appSecretChecker,
) {
} }
@ -84,6 +88,8 @@ class HomepageController extends AbstractController
'new_version_available' => $updateAvailableManager->isUpdateAvailable(), 'new_version_available' => $updateAvailableManager->isUpdateAvailable(),
'new_version' => $updateAvailableManager->getLatestVersionString(), 'new_version' => $updateAvailableManager->getLatestVersionString(),
'new_version_url' => $updateAvailableManager->getLatestVersionUrl(), 'new_version_url' => $updateAvailableManager->getLatestVersionUrl(),
'insecure_app_secret' => $this->appSecretChecker->isInsecureSecret(),
'suggested_app_secret' => $this->appSecretChecker->isInsecureSecret() ? $this->appSecretChecker->generateSecret() : null,
]); ]);
} }
} }

View file

@ -90,7 +90,7 @@ class PartImportExportController extends AbstractController
goto ret; goto ret;
} }
if (!isset($errors) || $errors) { if (!isset($errors) || $errors) { //@phpstan-ignore-line
$this->addFlash('error', 'parts.import.flash.error'); $this->addFlash('error', 'parts.import.flash.error');
} else { } else {
$this->addFlash('success', 'parts.import.flash.success'); $this->addFlash('success', 'parts.import.flash.success');

View file

@ -22,6 +22,7 @@ declare(strict_types=1);
namespace App\Controller; namespace App\Controller;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use App\Entity\ProjectSystem\Project; use App\Entity\ProjectSystem\Project;
use App\Entity\Parts\Category; use App\Entity\Parts\Category;
@ -55,7 +56,7 @@ class TreeController extends AbstractController
#[Route(path: '/category/{id}', name: 'tree_category')] #[Route(path: '/category/{id}', name: 'tree_category')]
#[Route(path: '/categories', name: 'tree_category_root')] #[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')) { if ($this->isGranted('@parts.read') && $this->isGranted('@categories.read')) {
$tree = $this->treeGenerator->getTreeView(Category::class, $category, 'list_parts_root'); $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: '/footprint/{id}', name: 'tree_footprint')]
#[Route(path: '/footprints', name: 'tree_footprint_root')] #[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')) { if ($this->isGranted('@parts.read') && $this->isGranted('@footprints.read')) {
$tree = $this->treeGenerator->getTreeView(Footprint::class, $footprint, 'list_parts_root'); $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: '/location/{id}', name: 'tree_location')]
#[Route(path: '/locations', name: 'tree_location_root')] #[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')) { if ($this->isGranted('@parts.read') && $this->isGranted('@storelocations.read')) {
$tree = $this->treeGenerator->getTreeView(StorageLocation::class, $location, 'list_parts_root'); $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: '/manufacturer/{id}', name: 'tree_manufacturer')]
#[Route(path: '/manufacturers', name: 'tree_manufacturer_root')] #[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')) { if ($this->isGranted('@parts.read') && $this->isGranted('@manufacturers.read')) {
$tree = $this->treeGenerator->getTreeView(Manufacturer::class, $manufacturer, 'list_parts_root'); $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: '/supplier/{id}', name: 'tree_supplier')]
#[Route(path: '/suppliers', name: 'tree_supplier_root')] #[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')) { if ($this->isGranted('@parts.read') && $this->isGranted('@suppliers.read')) {
$tree = $this->treeGenerator->getTreeView(Supplier::class, $supplier, 'list_parts_root'); $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: '/device/{id}', name: 'tree_device')]
#[Route(path: '/devices', name: 'tree_device_root')] #[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')) { if ($this->isGranted('@projects.read')) {
$tree = $this->treeGenerator->getTreeView(Project::class, $device, 'devices'); $tree = $this->treeGenerator->getTreeView(Project::class, $device, 'devices');

View file

@ -22,6 +22,7 @@ declare(strict_types=1);
namespace App\DataTables; namespace App\DataTables;
use App\DataTables\Column\HTMLColumn;
use App\DataTables\Column\LocaleDateTimeColumn; use App\DataTables\Column\LocaleDateTimeColumn;
use App\DataTables\Column\PrettyBoolColumn; use App\DataTables\Column\PrettyBoolColumn;
use App\DataTables\Column\RowClassColumn; use App\DataTables\Column\RowClassColumn;
@ -40,14 +41,19 @@ use Omines\DataTablesBundle\DataTable;
use Omines\DataTablesBundle\DataTableTypeInterface; use Omines\DataTablesBundle\DataTableTypeInterface;
use Symfony\Contracts\Translation\TranslatorInterface; 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 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, [ $dataTable->add('dont_matter', RowClassColumn::class, [
'render' => function ($value, Attachment $context): string { 'render' => function ($value, Attachment $context): string {
//Mark attachments yellow which have an internal file linked that doesn't exist //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' => '', 'label' => '',
'className' => 'no-colvis', 'className' => 'no-colvis',
'render' => function ($value, Attachment $context): string { 'data' => function (Attachment $context): string {
if ($context->isPicture() if ($context->isPicture()
&& $this->attachmentHelper->isInternalFileExisting($context)) { && $this->attachmentHelper->isInternalFileExisting($context)) {
@ -95,65 +101,65 @@ final class AttachmentDataTable implements DataTableTypeInterface
'orderField' => 'NATSORT(attachment.name)', 'orderField' => 'NATSORT(attachment.name)',
]); ]);
$dataTable->add('attachment_type', TextColumn::class, [ $dataTable->add('attachment_type', HTMLColumn::class, [
'label' => 'attachment.table.type', 'label' => 'attachment.table.type',
'field' => 'attachment_type.name', 'field' => 'attachment_type.name',
'orderField' => 'NATSORT(attachment_type.name)', 'orderField' => 'NATSORT(attachment_type.name)',
'render' => fn($value, Attachment $context): string => sprintf( 'data' => fn(Attachment $context, $value): string => sprintf(
'<a href="%s">%s</a>', '<a href="%s">%s</a>',
$this->entityURLGenerator->editURL($context->getAttachmentType()), $this->entityURLGenerator->editURL($context->getAttachmentType()),
htmlspecialchars((string) $value) htmlspecialchars((string) $value)
), ),
]); ]);
$dataTable->add('element', TextColumn::class, [ $dataTable->add('element', HTMLColumn::class, [
'label' => 'attachment.table.element', 'label' => 'attachment.table.element',
//'propertyPath' => 'element.name', //'propertyPath' => 'element.name',
'render' => fn($value, Attachment $context): string => sprintf( 'data' => fn(Attachment $context): string => sprintf(
'<a href="%s">%s</a>', '<a href="%s">%s</a>',
$this->entityURLGenerator->infoURL($context->getElement()), $this->entityURLGenerator->infoURL($context->getElement()),
$this->elementTypeNameGenerator->getTypeNameCombination($context->getElement(), true) $this->elementTypeNameGenerator->getTypeNameCombination($context->getElement(), true)
), ),
]); ]);
$dataTable->add('internal_link', TextColumn::class, [ $dataTable->add('internal_link', HTMLColumn::class, [
'label' => 'attachment.table.internal_file', 'label' => 'attachment.table.internal_file',
'propertyPath' => 'filename', 'propertyPath' => 'filename',
'orderField' => 'NATSORT(attachment.original_filename)', 'orderField' => 'NATSORT(attachment.original_filename)',
'render' => function ($value, Attachment $context) { 'data' => function (Attachment $context, $value) {
if ($this->attachmentHelper->isInternalFileExisting($context)) { if ($this->attachmentHelper->isInternalFileExisting($context)) {
return sprintf( return sprintf(
'<a href="%s" target="_blank" data-no-ajax>%s</a>', '<a href="%s" target="_blank" data-no-ajax>%s</a>',
$this->entityURLGenerator->viewURL($context), $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', 'label' => 'attachment.table.external_link',
'propertyPath' => 'host', 'propertyPath' => 'host',
'orderField' => 'attachment.external_path', 'orderField' => 'attachment.external_path',
'render' => function ($value, Attachment $context) { 'data' => function (Attachment $context, $value) {
if ($context->hasExternal()) { if ($context->hasExternal()) {
return sprintf( return sprintf(
'<a href="%s" class="link-external" title="%s" target="_blank" rel="noopener">%s</a>', '<a href="%s" class="link-external" title="%s" target="_blank" rel="noopener">%s</a>',
htmlspecialchars((string) $context->getExternalPath()), htmlspecialchars((string) $context->getExternalPath()),
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'), 'label' => $this->translator->trans('attachment.table.filesize'),
'render' => function ($value, Attachment $context) { 'data' => function (Attachment $context) {
if (!$context->hasInternal()) { if (!$context->hasInternal()) {
return sprintf( return sprintf(
'<span class="badge bg-primary"> '<span class="badge bg-primary">
@ -168,7 +174,7 @@ final class AttachmentDataTable implements DataTableTypeInterface
'<span class="badge bg-secondary"> '<span class="badge bg-secondary">
<i class="fas fa-hdd fa-fw"></i> %s <i class="fas fa-hdd fa-fw"></i> %s
</span>', </span>',
$this->attachmentHelper->getHumanFileSize($context) htmlspecialchars($this->attachmentHelper->getHumanFileSize($context))
); );
} }

View file

@ -78,7 +78,7 @@ class EntityColumn extends AbstractColumn
); );
} }
return sprintf('<i>%s</i>', $value); return sprintf('<i>%s</i>', htmlspecialchars($value));
} }
return ''; return '';

View file

@ -1,7 +1,11 @@
<?php
declare(strict_types=1);
/* /*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). * 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) * Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU Affero General Public License as published
@ -16,31 +20,18 @@
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
namespace App\DataTables\Column;
.modal-body > .bootbox-close-button { use Omines\DataTablesBundle\Column\TextColumn;
position: absolute;
top: 0;
right: 0;
padding: 0.5rem 0.75rem;
z-index: 1;
}
.modal .bootbox-close-button {
font-weight: 100;
}
button.bootbox-close-button { /**
padding: 0; * A TextColumn whose value is always treated as raw HTML and therefore never passed through htmlspecialchars().
background-color: transparent; * The value returned by the 'data' option must already contain properly escaped/sanitized HTML, as it is output as-is.
border: 0; */
-webkit-appearance: none; class HTMLColumn extends TextColumn
} {
public function isRaw(): bool
.bootbox-close-button { {
/* float: right; */ return true;
font-size: 1.40625rem; }
font-weight: 600;
line-height: 1;
color: #000;
text-shadow: none;
opacity: .5;
} }

View file

@ -87,9 +87,9 @@ class IconLinkColumn extends AbstractColumn
return sprintf( return sprintf(
'<a class="btn btn-primary btn-sm %s" href="%s" title="%s"><i class="%s"></i></a>', '<a class="btn btn-primary btn-sm %s" href="%s" title="%s"><i class="%s"></i></a>',
$disabled ? 'disabled' : '', $disabled ? 'disabled' : '',
$href, htmlspecialchars($href),
$title, htmlspecialchars($title ?? ''),
$icon htmlspecialchars($icon ?? '')
); );
} }

View file

@ -22,9 +22,9 @@ declare(strict_types=1);
*/ */
namespace App\DataTables; namespace App\DataTables;
use App\DataTables\Column\HTMLColumn;
use App\DataTables\Column\RowClassColumn; use App\DataTables\Column\RowClassColumn;
use Omines\DataTablesBundle\Adapter\ArrayAdapter; use Omines\DataTablesBundle\Adapter\ArrayAdapter;
use Omines\DataTablesBundle\Column\TextColumn;
use Omines\DataTablesBundle\DataTable; use Omines\DataTablesBundle\DataTable;
use Omines\DataTablesBundle\DataTableFactory; use Omines\DataTablesBundle\DataTableFactory;
use Omines\DataTablesBundle\DataTableTypeInterface; use Omines\DataTablesBundle\DataTableTypeInterface;
@ -32,7 +32,7 @@ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
class ErrorDataTable implements DataTableTypeInterface final readonly class ErrorDataTable implements DataTableTypeInterface
{ {
public function configureOptions(OptionsResolver $optionsResolver): void public function configureOptions(OptionsResolver $optionsResolver): void
{ {
@ -49,6 +49,11 @@ class ErrorDataTable implements DataTableTypeInterface
public function configure(DataTable $dataTable, array $options): void 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(); $optionsResolver = new OptionsResolver();
$this->configureOptions($optionsResolver); $this->configureOptions($optionsResolver);
$options = $optionsResolver->resolve($options); $options = $optionsResolver->resolve($options);
@ -58,9 +63,9 @@ class ErrorDataTable implements DataTableTypeInterface
'render' => fn($value, $context): string => 'table-warning', 'render' => fn($value, $context): string => 'table-warning',
]) ])
->add('error', TextColumn::class, [ ->add('error', HTMLColumn::class, [
'label' => 'error_table.error', 'label' => 'error_table.error',
'render' => fn($value, $context): string => '<i class="fa-solid fa-triangle-exclamation fa-fw"></i> ' . $value, 'data' => fn($context, $value): string => '<i class="fa-solid fa-triangle-exclamation fa-fw"></i> ' . htmlspecialchars((string) $value),
]) ])
; ;

View file

@ -62,7 +62,7 @@ class PartDataTableHelper
} }
if ($context->getBuiltProject() instanceof Project) { if ($context->getBuiltProject() instanceof Project) {
$icon = sprintf('<i class="fa-solid fa-box-archive fa-fw me-1" title="%s"></i>', $icon = sprintf('<i class="fa-solid fa-box-archive fa-fw me-1" title="%s"></i>',
$this->translator->trans('part.info.projectBuildPart.hint').': '.$context->getBuiltProject()->getName()); $this->translator->trans('part.info.projectBuildPart.hint').': '.htmlspecialchars($context->getBuiltProject()->getName()));
} }

View file

@ -25,6 +25,7 @@ namespace App\DataTables;
use App\DataTables\Column\EnumColumn; use App\DataTables\Column\EnumColumn;
use App\Entity\LogSystem\LogTargetType; use App\Entity\LogSystem\LogTargetType;
use Symfony\Bundle\SecurityBundle\Security; use Symfony\Bundle\SecurityBundle\Security;
use App\DataTables\Column\HTMLColumn;
use App\DataTables\Column\IconLinkColumn; use App\DataTables\Column\IconLinkColumn;
use App\DataTables\Column\LocaleDateTimeColumn; use App\DataTables\Column\LocaleDateTimeColumn;
use App\DataTables\Column\LogEntryExtraColumn; use App\DataTables\Column\LogEntryExtraColumn;
@ -59,7 +60,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
class LogDataTable implements DataTableTypeInterface final readonly class LogDataTable implements DataTableTypeInterface
{ {
protected LogEntryRepository $logRepo; protected LogEntryRepository $logRepo;
@ -95,6 +96,11 @@ class LogDataTable implements DataTableTypeInterface
public function configure(DataTable $dataTable, array $options): void 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(); $resolver = new OptionsResolver();
$this->configureOptions($resolver); $this->configureOptions($resolver);
$options = $resolver->resolve($options); $options = $resolver->resolve($options);
@ -104,10 +110,10 @@ class LogDataTable implements DataTableTypeInterface
'render' => fn($value, AbstractLogEntry $context) => $this->logLevelHelper->logLevelToTableColorClass($context->getLevelString()), 'render' => fn($value, AbstractLogEntry $context) => $this->logLevelHelper->logLevelToTableColorClass($context->getLevelString()),
]); ]);
$dataTable->add('symbol', TextColumn::class, [ $dataTable->add('symbol', HTMLColumn::class, [
'label' => '', 'label' => '',
'className' => 'no-colvis', 'className' => 'no-colvis',
'render' => fn($value, AbstractLogEntry $context): string => sprintf( 'data' => fn(AbstractLogEntry $context): string => sprintf(
'<i class="fas fa-fw %s" title="%s"></i>', '<i class="fas fa-fw %s" title="%s"></i>',
$this->logLevelHelper->logLevelToIconClass($context->getLevelString()), $this->logLevelHelper->logLevelToIconClass($context->getLevelString()),
$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', 'label' => 'log.type',
'propertyPath' => 'type', 'propertyPath' => 'type',
'render' => function (string $value, AbstractLogEntry $context) { 'data' => function (AbstractLogEntry $context, string $value) {
$text = $this->translator->trans('log.type.'.$value); $text = $this->translator->trans('log.type.'.$value);
if ($context instanceof PartStockChangedLogEntry) { if ($context instanceof PartStockChangedLogEntry) {
@ -149,20 +155,20 @@ class LogDataTable implements DataTableTypeInterface
'label' => 'log.level', 'label' => 'log.level',
'visible' => 'system_log' === $options['mode'], 'visible' => 'system_log' === $options['mode'],
'propertyPath' => 'levelString', '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', 'label' => 'log.user',
'orderField' => 'NATSORT(user.name)', 'orderField' => 'NATSORT(user.name)',
'render' => function ($value, AbstractLogEntry $context): string { 'data' => function (AbstractLogEntry $context): string {
$user = $context->getUser(); $user = $context->getUser();
//If user was deleted, show the info from the username field //If user was deleted, show the info from the username field
if (!$user instanceof User) { if (!$user instanceof User) {
if ($context->isCLIEntry()) { if ($context->isCLIEntry()) {
return sprintf('%s [%s]', return sprintf('%s [%s]',
htmlentities((string) $context->getCLIUsername()), htmlspecialchars((string) $context->getCLIUsername()),
$this->translator->trans('log.cli_user') $this->translator->trans('log.cli_user')
); );
} }
@ -170,7 +176,7 @@ class LogDataTable implements DataTableTypeInterface
//Else we just deal with a deleted user //Else we just deal with a deleted user
return sprintf( return sprintf(
'@%s [%s]', '@%s [%s]',
htmlentities($context->getUsername()), htmlspecialchars($context->getUsername()),
$this->translator->trans('log.target_deleted'), $this->translator->trans('log.target_deleted'),
); );
} }
@ -182,7 +188,7 @@ class LogDataTable implements DataTableTypeInterface
$img_url, $img_url,
$this->userAvatarHelper->getAvatarMdURL($user), $this->userAvatarHelper->getAvatarMdURL($user),
$this->urlGenerator->generate('user_info', ['id' => $user->getID()]), $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) { 'render' => function (LogTargetType $value, AbstractLogEntry $context) {
$class = $value->toClass(); $class = $value->toClass();
if (null !== $class) { if (null !== $class) {
return $this->elementTypeNameGenerator->getLocalizedTypeLabel($class); return $this->elementTypeNameGenerator->typeLabel($class);
} }
return ''; return '';
@ -216,9 +222,9 @@ class LogDataTable implements DataTableTypeInterface
'icon' => 'fas fa-fw fa-eye', 'icon' => 'fas fa-fw fa-eye',
'href' => function ($value, AbstractLogEntry $context) { 'href' => function ($value, AbstractLogEntry $context) {
if ( if (
$context instanceof CollectionElementDeleted ||
($context instanceof TimeTravelInterface ($context instanceof TimeTravelInterface
&& $context->hasOldDataInformation()) && $context->hasOldDataInformation())
|| $context instanceof CollectionElementDeleted
) { ) {
try { try {
$target = $this->logRepo->getTargetElement($context); $target = $this->logRepo->getTargetElement($context);

View file

@ -25,6 +25,7 @@ namespace App\DataTables;
use App\DataTables\Adapters\TwoStepORMAdapter; use App\DataTables\Adapters\TwoStepORMAdapter;
use App\DataTables\Column\EntityColumn; use App\DataTables\Column\EntityColumn;
use App\DataTables\Column\EnumColumn; use App\DataTables\Column\EnumColumn;
use App\DataTables\Column\HTMLColumn;
use App\DataTables\Column\IconLinkColumn; use App\DataTables\Column\IconLinkColumn;
use App\DataTables\Column\LocaleDateTimeColumn; use App\DataTables\Column\LocaleDateTimeColumn;
use App\DataTables\Column\MarkdownColumn; use App\DataTables\Column\MarkdownColumn;
@ -58,7 +59,7 @@ use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface; 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"]]; 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! * 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 $this->csh
//Color the table rows depending on the review and favorite status //Color the table rows depending on the review and favorite status
->add('row_color', RowClassColumn::class, [ ->add('row_color', RowClassColumn::class, [
@ -109,23 +115,23 @@ final class PartsDataTable implements DataTableTypeInterface
}, },
], visibility_configurable: false) ], visibility_configurable: false)
->add('select', SelectColumn::class, visibility_configurable: false) ->add('select', SelectColumn::class, visibility_configurable: false)
->add('picture', TextColumn::class, [ ->add('picture', HTMLColumn::class, [
'label' => '', 'label' => '',
'className' => 'no-colvis', 'className' => 'no-colvis',
'render' => fn($value, Part $context) => $this->partDataTableHelper->renderPicture($context), 'data' => fn(Part $context) => $this->partDataTableHelper->renderPicture($context),
], visibility_configurable: false) ], visibility_configurable: false)
->add('name', TextColumn::class, [ ->add('name', HTMLColumn::class, [
'label' => $this->translator->trans('part.table.name'), '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)' 'orderField' => 'NATSORT(part.name)'
]) ])
->add('si_value', TextColumn::class, [ ->add('si_value', TextColumn::class, [
'label' => $this->translator->trans('part.table.si_value'), 'label' => $this->translator->trans('part.table.si_value'),
'render' => function ($value, Part $context): string { 'data' => function (Part $context): string {
$siValue = SiValueSort::sqliteSiValue($context->getName()); $siValue = SiValueSort::sqliteSiValue($context->getName());
if ($siValue !== null) { if ($siValue !== null) {
//Output it as scientific number with a big E //Output it as scientific number with a big E
return htmlspecialchars(sprintf('%G', $siValue)); return sprintf('%G', $siValue);
} }
return ''; return '';
}, },
@ -156,38 +162,38 @@ final class PartsDataTable implements DataTableTypeInterface
'label' => $this->translator->trans('part.table.manufacturer'), 'label' => $this->translator->trans('part.table.manufacturer'),
'orderField' => 'NATSORT(_manufacturer.name)' 'orderField' => 'NATSORT(_manufacturer.name)'
]) ])
->add('storelocation', TextColumn::class, [ ->add('storelocation', HTMLColumn::class, [
'label' => $this->translator->trans('part.table.storeLocations'), '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 //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))', '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') ], alias: 'storage_location')
->add('amount', TextColumn::class, [ ->add('amount', HTMLColumn::class, [
'label' => $this->translator->trans('part.table.amount'), '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' 'orderField' => 'amountSum'
]) ])
->add('minamount', TextColumn::class, [ ->add('minamount', TextColumn::class, [
'label' => $this->translator->trans('part.table.minamount'), '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, $value,
$context->getPartUnit() $context->getPartUnit()
)), ),
]) ])
->add('partUnit', TextColumn::class, [ ->add('partUnit', TextColumn::class, [
'label' => $this->translator->trans('part.table.partUnit'), 'label' => $this->translator->trans('part.table.partUnit'),
'orderField' => 'NATSORT(_partUnit.name)', 'orderField' => 'NATSORT(_partUnit.name)',
'render' => function ($value, Part $context): string { 'data' => function (Part $context): string {
$partUnit = $context->getPartUnit(); $partUnit = $context->getPartUnit();
if ($partUnit === null) { if ($partUnit === null) {
return ''; return '';
} }
$tmp = htmlspecialchars($partUnit->getName()); $tmp = $partUnit->getName();
if ($partUnit->getUnit()) { if ($partUnit->getUnit()) {
$tmp .= ' (' . htmlspecialchars($partUnit->getUnit()) . ')'; $tmp .= ' (' . $partUnit->getUnit() . ')';
} }
return $tmp; return $tmp;
} }
@ -195,14 +201,14 @@ final class PartsDataTable implements DataTableTypeInterface
->add('partCustomState', TextColumn::class, [ ->add('partCustomState', TextColumn::class, [
'label' => $this->translator->trans('part.table.partCustomState'), 'label' => $this->translator->trans('part.table.partCustomState'),
'orderField' => 'NATSORT(_partCustomState.name)', 'orderField' => 'NATSORT(_partCustomState.name)',
'render' => function($value, Part $context): string { 'data' => function(Part $context): string {
$partCustomState = $context->getPartCustomState(); $partCustomState = $context->getPartCustomState();
if ($partCustomState === null) { if ($partCustomState === null) {
return ''; return '';
} }
return htmlspecialchars($partCustomState->getName()); return $partCustomState->getName();
} }
]) ])
->add('addedDate', LocaleDateTimeColumn::class, [ ->add('addedDate', LocaleDateTimeColumn::class, [
@ -248,25 +254,25 @@ final class PartsDataTable implements DataTableTypeInterface
]) ])
->add('eda_reference', TextColumn::class, [ ->add('eda_reference', TextColumn::class, [
'label' => $this->translator->trans('part.table.eda_reference'), '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)' 'orderField' => 'NATSORT(part.eda_info.reference_prefix)'
]) ])
->add('eda_value', TextColumn::class, [ ->add('eda_value', TextColumn::class, [
'label' => $this->translator->trans('part.table.eda_value'), '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)' 'orderField' => 'NATSORT(part.eda_info.value)'
]) ])
->add('eda_status', TextColumn::class, [ ->add('eda_status', HTMLColumn::class, [
'label' => $this->translator->trans('part.table.eda_status'), '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', '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 //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)) { 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'), 'label' => $this->translator->trans('project.labelp'),
'render' => function ($value, Part $context): string { 'data' => function (Part $context): string {
//Only show the first 5 projects names //Only show the first 5 projects names
$projects = $context->getProjects(); $projects = $context->getProjects();
$tmp = ""; $tmp = "";
@ -286,7 +292,7 @@ final class PartsDataTable implements DataTableTypeInterface
} }
return $tmp; return $tmp;
} },
]); ]);
} }

View file

@ -25,6 +25,7 @@ namespace App\DataTables;
use App\DataTables\Adapters\TwoStepORMAdapter; use App\DataTables\Adapters\TwoStepORMAdapter;
use App\DataTables\Column\EntityColumn; use App\DataTables\Column\EntityColumn;
use App\DataTables\Column\EnumColumn; use App\DataTables\Column\EnumColumn;
use App\DataTables\Column\HTMLColumn;
use App\DataTables\Column\LocaleDateTimeColumn; use App\DataTables\Column\LocaleDateTimeColumn;
use App\DataTables\Column\MarkdownColumn; use App\DataTables\Column\MarkdownColumn;
use App\DataTables\Helpers\PartDataTableHelper; use App\DataTables\Helpers\PartDataTableHelper;
@ -48,7 +49,7 @@ use Omines\DataTablesBundle\DataTable;
use Omines\DataTablesBundle\DataTableTypeInterface; use Omines\DataTablesBundle\DataTableTypeInterface;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
class ProjectBomEntriesDataTable implements DataTableTypeInterface final readonly class ProjectBomEntriesDataTable implements DataTableTypeInterface
{ {
public function __construct( public function __construct(
protected EntityURLGenerator $entityURLGenerator, protected EntityURLGenerator $entityURLGenerator,
@ -63,17 +64,22 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
public function configure(DataTable $dataTable, array $options): void 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 $dataTable
//->add('select', SelectColumn::class) //->add('select', SelectColumn::class)
->add('picture', TextColumn::class, [ ->add('picture', HTMLColumn::class, [
'label' => '', 'label' => '',
'className' => 'no-colvis', 'className' => 'no-colvis',
'render' => function ($value, ProjectBOMEntry $context) { 'data' => function (ProjectBOMEntry $context) {
if(!$context->getPart() instanceof Part) { if(!$context->getPart() instanceof Part) {
return ''; return '';
} }
return $this->partDataTableHelper->renderPicture($context->getPart()); return $this->partDataTableHelper->renderPicture($context->getPart());
} },
]) ])
->add('id', TextColumn::class, [ ->add('id', TextColumn::class, [
@ -85,27 +91,27 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
'label' => $this->translator->trans('project.bom.quantity'), 'label' => $this->translator->trans('project.bom.quantity'),
'className' => 'text-center', 'className' => 'text-center',
'orderField' => 'bom_entry.quantity', '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 we have a non-part entry, only show the rounded quantity
if (!$context->getPart() instanceof Part) { if (!$context->getPart() instanceof Part) {
return round($context->getQuantity()); return round($context->getQuantity());
} }
//Otherwise use the unit of the part to format the quantity //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, [ ->add('partId', TextColumn::class, [
'label' => $this->translator->trans('project.bom.part_id'), 'label' => $this->translator->trans('project.bom.part_id'),
'visible' => true, 'visible' => true,
'orderField' => 'part.id', 'orderField' => 'part.id',
'render' => function ($value, ProjectBOMEntry $context) { 'data' => function (ProjectBOMEntry $context) {
return $context->getPart() instanceof Part ? (string) $context->getPart()->getId() : ''; return $context->getPart() instanceof Part ? (string) $context->getPart()->getId() : '';
}, },
]) ])
->add('name', TextColumn::class, [ ->add('name', HTMLColumn::class, [
'label' => $this->translator->trans('part.table.name'), 'label' => $this->translator->trans('part.table.name'),
'orderField' => 'NATSORT(part.name)', 'orderField' => 'NATSORT(part.name)',
'render' => function ($value, ProjectBOMEntry $context) { 'data' => function (ProjectBOMEntry $context) {
if(!$context->getPart() instanceof Part) { if(!$context->getPart() instanceof Part) {
return htmlspecialchars((string) $context->getName()); return htmlspecialchars((string) $context->getName());
} }
@ -123,11 +129,7 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
'label' => $this->translator->trans('part.table.ipn'), 'label' => $this->translator->trans('part.table.ipn'),
'orderField' => 'NATSORT(part.ipn)', 'orderField' => 'NATSORT(part.ipn)',
'visible' => false, 'visible' => false,
'render' => function ($value, ProjectBOMEntry $context) { 'data' => fn (ProjectBOMEntry $context) => $context->getPart()?->getIpn()
if($context->getPart() instanceof Part) {
return $context->getPart()->getIpn();
}
}
]) ])
->add('description', MarkdownColumn::class, [ ->add('description', MarkdownColumn::class, [
'label' => $this->translator->trans('part.table.description'), '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', 'label' => 'project.bom.mountnames',
'render' => function ($value, ProjectBOMEntry $context) { 'data' => function (ProjectBOMEntry $context) {
$html = ''; $html = '';
foreach (explode(',', $context->getMountnames()) as $mountname) { 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', 'label' => 'project.bom.instockAmount',
'visible' => false, 'visible' => false,
'render' => function ($value, ProjectBOMEntry $context) { 'data' => function (ProjectBOMEntry $context) {
if ($context->getPart() !== null) { if ($context->getPart() !== null) {
return $this->partDataTableHelper->renderAmount($context->getPart()); return $this->partDataTableHelper->renderAmount($context->getPart());
} }
return ''; return '';
} },
]) ])
->add('storelocation', TextColumn::class, [ ->add('storelocation', HTMLColumn::class, [
'label' => $this->translator->trans('part.table.storeLocations'), '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 //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))', 'orderField' => 'NATSORT(MIN(_storelocations.name))',
'visible' => false, 'visible' => false,
'render' => function ($value, ProjectBOMEntry $context) { 'data' => function (ProjectBOMEntry $context) {
if ($context->getPart() !== null) { if ($context->getPart() !== null) {
return $this->partDataTableHelper->renderStorageLocations($context->getPart()); return $this->partDataTableHelper->renderStorageLocations($context->getPart());
} }
return ''; return '';
} },
]) ])
->add('price', TextColumn::class, [ ->add('price', TextColumn::class, [
'label' => 'project.bom.price', 'label' => 'project.bom.price',
'visible' => false, 'visible' => false,
'render' => function ($value, ProjectBOMEntry $context) { 'data' => function (ProjectBOMEntry $context) {
$price = $this->projectBuildHelper->getEntryUnitPrice($context); $price = $this->projectBuildHelper->getEntryUnitPrice($context);
return $this->moneyFormatter->format($price->toScale(2, RoundingMode::Up)->toFloat(), null, 2, true); 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, [ ->add('ext_price', TextColumn::class, [
'label' => 'project.bom.ext_price', 'label' => 'project.bom.ext_price',
'visible' => false, 'visible' => false,
'render' => function ($value, ProjectBOMEntry $context) { 'data' => function (ProjectBOMEntry $context) {
$price = $this->projectBuildHelper->getEntryUnitPrice($context); $price = $this->projectBuildHelper->getEntryUnitPrice($context);
return $this->moneyFormatter->format( return $this->moneyFormatter->format(
$price->multipliedBy(BigDecimal::fromFloatShortest($context->getQuantity())) $price->multipliedBy(BigDecimal::fromFloatShortest($context->getQuantity()))

View file

@ -27,6 +27,7 @@ use App\Doctrine\Functions\SiValueSort;
use App\Exceptions\InvalidRegexException; use App\Exceptions\InvalidRegexException;
use Doctrine\DBAL\Driver\Connection; use Doctrine\DBAL\Driver\Connection;
use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware; use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware;
use Pdo\Sqlite;
/** /**
* This middleware is used to add the regexp operator to the SQLite platform. * 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') { if ($params['driver'] === 'pdo_sqlite') {
$native_connection = $connection->getNativeConnection(); $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) { 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 //Use the new PDO::createFunction and PDO::createCollation methods if available (PHP 8.4+)
$native_connection->sqliteCreateCollation('NATURAL_CMP', strnatcmp(...)); 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 //Create a new collation for natural sorting
$native_connection->sqliteCreateFunction('SI_VALUE', SiValueSort::sqliteSiValue(...), 1, \PDO::SQLITE_DETERMINISTIC); $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);
}
} }
} }

View file

@ -35,8 +35,10 @@ class SetSQLModeMiddlewareDriver extends AbstractDriverMiddleware
{ {
//Only set this on MySQL connections, as other databases don't support this parameter //Only set this on MySQL connections, as other databases don't support this parameter
if($params['driver'] === 'pdo_mysql') { if($params['driver'] === 'pdo_mysql') {
//1002 is \PDO::MYSQL_ATTR_INIT_COMMAND constant value //PDO::MYSQL_ATTR_INIT_COMMAND is deprecated since PHP 8.5 in favor of Pdo\Mysql::ATTR_INIT_COMMAND,
$params['driverOptions'][\PDO::MYSQL_ATTR_INIT_COMMAND] = 'SET SESSION sql_mode=(SELECT REPLACE(@@sql_mode, \'ONLY_FULL_GROUP_BY\', \'\'))'; //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); return parent::connect($params);

View file

@ -65,21 +65,16 @@ use Symfony\Component\Validator\Constraints as Assert;
new Post(securityPostDenormalize: 'is_granted("create", object)'), new Post(securityPostDenormalize: 'is_granted("create", object)'),
new Patch(security: 'is_granted("edit", object)'), new Patch(security: 'is_granted("edit", object)'),
new Delete(security: 'is_granted("delete", 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'], 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'], 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(PropertyFilter::class)]
#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])] #[ApiFilter(LikeFilter::class, properties: ["name", "comment"])]
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] #[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]

View file

@ -68,23 +68,16 @@ use Symfony\Component\Validator\Constraints as Assert;
new Post(securityPostDenormalize: 'is_granted("create", object)'), new Post(securityPostDenormalize: 'is_granted("create", object)'),
new Patch(security: 'is_granted("edit", object)'), new Patch(security: 'is_granted("edit", object)'),
new Delete(security: 'is_granted("delete", 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'], 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'], 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(PropertyFilter::class)]
#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])] #[ApiFilter(LikeFilter::class, properties: ["name", "comment"])]
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] #[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]

View file

@ -67,23 +67,16 @@ use Symfony\Component\Validator\Constraints as Assert;
new Post(securityPostDenormalize: 'is_granted("create", object)'), new Post(securityPostDenormalize: 'is_granted("create", object)'),
new Patch(security: 'is_granted("edit", object)'), new Patch(security: 'is_granted("edit", object)'),
new Delete(security: 'is_granted("delete", 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'], 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'], 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(PropertyFilter::class)]
#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])] #[ApiFilter(LikeFilter::class, properties: ["name", "comment"])]
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] #[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]

View file

@ -28,9 +28,11 @@ use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping\Column; use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Embeddable; use Doctrine\ORM\Mapping\Embeddable;
use Symfony\Component\Serializer\Annotation\Groups; 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 * @see \App\Tests\Entity\Parts\InfoProviderReferenceTest
*/ */
#[Embeddable] #[Embeddable]
@ -157,4 +159,44 @@ class InfoProviderReference
$ref->last_updated = new \DateTimeImmutable(); $ref->last_updated = new \DateTimeImmutable();
return $ref; 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();
}
}
} }

View file

@ -66,23 +66,16 @@ use Symfony\Component\Validator\Constraints as Assert;
new Post(securityPostDenormalize: 'is_granted("create", object)'), new Post(securityPostDenormalize: 'is_granted("create", object)'),
new Patch(security: 'is_granted("edit", object)'), new Patch(security: 'is_granted("edit", object)'),
new Delete(security: 'is_granted("delete", 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'], 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'], 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(PropertyFilter::class)]
#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])] #[ApiFilter(LikeFilter::class, properties: ["name", "comment"])]
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] #[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]

View file

@ -71,23 +71,16 @@ use Symfony\Component\Validator\Constraints\Length;
new Post(securityPostDenormalize: 'is_granted("create", object)'), new Post(securityPostDenormalize: 'is_granted("create", object)'),
new Patch(security: 'is_granted("edit", object)'), new Patch(security: 'is_granted("edit", object)'),
new Delete(security: 'is_granted("delete", 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'], 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'], 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(PropertyFilter::class)]
#[ApiFilter(LikeFilter::class, properties: ["name", "comment", "unit"])] #[ApiFilter(LikeFilter::class, properties: ["name", "comment", "unit"])]
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] #[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]

View file

@ -75,6 +75,7 @@ trait AdvancedPropertyTrait
*/ */
#[ORM\Embedded(class: InfoProviderReference::class, columnPrefix: 'provider_reference_')] #[ORM\Embedded(class: InfoProviderReference::class, columnPrefix: 'provider_reference_')]
#[Groups(['full', 'part:read'])] #[Groups(['full', 'part:read'])]
#[Assert\Valid()]
protected InfoProviderReference $providerReference; protected InfoProviderReference $providerReference;
/** /**

View file

@ -67,23 +67,16 @@ use Symfony\Component\Validator\Constraints as Assert;
new Post(securityPostDenormalize: 'is_granted("create", object)'), new Post(securityPostDenormalize: 'is_granted("create", object)'),
new Patch(security: 'is_granted("edit", object)'), new Patch(security: 'is_granted("edit", object)'),
new Delete(security: 'is_granted("delete", 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'], 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'], 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(PropertyFilter::class)]
#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])] #[ApiFilter(LikeFilter::class, properties: ["name", "comment"])]
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] #[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]

View file

@ -71,21 +71,16 @@ use Symfony\Component\Validator\Constraints as Assert;
new Post(securityPostDenormalize: 'is_granted("create", object)'), new Post(securityPostDenormalize: 'is_granted("create", object)'),
new Patch(security: 'is_granted("edit", object)'), new Patch(security: 'is_granted("edit", object)'),
new Delete(security: 'is_granted("delete", 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'], 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'], 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(PropertyFilter::class)]
#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])] #[ApiFilter(LikeFilter::class, properties: ["name", "comment"])]
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] #[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]

View file

@ -71,23 +71,16 @@ use Symfony\Component\Validator\Constraints as Assert;
new Post(securityPostDenormalize: 'is_granted("create", object)'), new Post(securityPostDenormalize: 'is_granted("create", object)'),
new Patch(security: 'is_granted("edit", object)'), new Patch(security: 'is_granted("edit", object)'),
new Delete(security: 'is_granted("delete", 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'], 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'], 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(PropertyFilter::class)]
#[ApiFilter(LikeFilter::class, properties: ["name", "comment", "iso_code"])] #[ApiFilter(LikeFilter::class, properties: ["name", "comment", "iso_code"])]
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] #[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]

View file

@ -71,23 +71,17 @@ use Symfony\Component\Validator\Constraints\Length;
new Post(securityPostDenormalize: 'is_granted("create", object)'), new Post(securityPostDenormalize: 'is_granted("create", object)'),
new Patch(security: 'is_granted("edit", object)'), new Patch(security: 'is_granted("edit", object)'),
new Delete(security: 'is_granted("delete", 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'], 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'], 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(PropertyFilter::class)] #[ApiFilter(PropertyFilter::class)]
#[ApiFilter(LikeFilter::class, properties: ["supplierpartnr", "supplier_product_url"])] #[ApiFilter(LikeFilter::class, properties: ["supplierpartnr", "supplier_product_url"])]

View file

@ -66,23 +66,16 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
new Post(securityPostDenormalize: 'is_granted("create", object)'), new Post(securityPostDenormalize: 'is_granted("create", object)'),
new Patch(security: 'is_granted("edit", object)'), new Patch(security: 'is_granted("edit", object)'),
new Delete(security: 'is_granted("delete", 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'], 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'], 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(PropertyFilter::class)]
#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])] #[ApiFilter(LikeFilter::class, properties: ["name", "comment"])]
#[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])] #[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 * @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'])] #[Groups(['extended', 'full', 'project:read', 'project:write', 'import'])]
#[ORM\Column(type: Types::STRING, length: 64, nullable: true)] #[ORM\Column(type: Types::STRING, length: 64, nullable: true)]
protected ?string $status = null; protected ?string $status = null;

View file

@ -63,23 +63,16 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
new Post(uriTemplate: '/project_bom_entries.{_format}', securityPostDenormalize: 'is_granted("create", object)',), 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 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 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'], normalizationContext: ['groups' => ['bom_entry:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
denormalizationContext: ['groups' => ['bom_entry:write', 'api:basic:write'], 'openapi_definition_name' => 'Write'], 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(PropertyFilter::class)]
#[ApiFilter(LikeFilter::class, properties: ["name", "comment", 'mountnames'])] #[ApiFilter(LikeFilter::class, properties: ["name", "comment", 'mountnames'])]
#[ApiFilter(RangeFilter::class, properties: ['quantity'])] #[ApiFilter(RangeFilter::class, properties: ['quantity'])]

View file

@ -0,0 +1,79 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2025 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\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
));
}
});
}
}

View file

@ -0,0 +1,113 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\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);
}
}

View file

@ -31,12 +31,12 @@ use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Translation\StaticMessage; use Symfony\Component\Translation\StaticMessage;
use Symfony\Component\Translation\TranslatableMessage;
class ProviderSelectType extends AbstractType class ProviderSelectType extends AbstractType
{ {
public function __construct(private readonly ProviderRegistry $providerRegistry) public function __construct(private readonly ProviderRegistry $providerRegistry)
{ {
} }
public function getParent(): string public function getParent(): string
@ -46,17 +46,22 @@ class ProviderSelectType extends AbstractType
public function configureOptions(OptionsResolver $resolver): void public function configureOptions(OptionsResolver $resolver): void
{ {
$providers = $this->providerRegistry->getActiveProviders();
$resolver->setDefault('input', 'object'); $resolver->setDefault('input', 'object');
$resolver->setAllowedTypes('input', 'string'); $resolver->setAllowedTypes('input', 'string');
//Either the form returns the provider objects or their keys //Either the form returns the provider objects or their keys
$resolver->setAllowedValues('input', ['object', 'string']); $resolver->setAllowedValues('input', ['object', 'string']);
$resolver->setDefault('multiple', true); $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']) { if ('object' === $options['input']) {
return $this->providerRegistry->getActiveProviders(); return $providers;
} }
$tmp = []; $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 //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']) { 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) { $resolver->setDefault('choice_value', function (Options $options) {
if ('object' === $options['input']) { 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; 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');
};
});
} }
} }

View file

@ -33,6 +33,7 @@ use App\Entity\Parts\Part;
use App\Entity\Parts\PartCustomState; use App\Entity\Parts\PartCustomState;
use App\Entity\PriceInformations\Orderdetail; use App\Entity\PriceInformations\Orderdetail;
use App\Form\AttachmentFormType; use App\Form\AttachmentFormType;
use App\Form\InfoProviderSystem\InfoProviderReferenceType;
use App\Form\ParameterType; use App\Form\ParameterType;
use App\Form\Part\EDA\EDAPartInfoType; use App\Form\Part\EDA\EDAPartInfoType;
use App\Form\Type\MasterPictureAttachmentType; use App\Form\Type\MasterPictureAttachmentType;
@ -225,6 +226,10 @@ class PartBaseType extends AbstractType
'empty_data' => null, 'empty_data' => null,
'label' => 'part.gtin', 'label' => 'part.gtin',
]) ])
->add('providerReference', InfoProviderReferenceType::class, [
'label' => false,
'required' => false,
])
; ;
//Comment section //Comment section

View file

@ -115,8 +115,10 @@ class PartLotType extends AbstractType
$builder->add('last_stocktake_at', DateTimeType::class, [ $builder->add('last_stocktake_at', DateTimeType::class, [
'label' => 'part_lot.edit.last_stocktake_at', 'label' => 'part_lot.edit.last_stocktake_at',
'widget' => 'single_text', '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'), 'disabled' => !$this->security->isGranted('@parts_stock.stocktake'),
'required' => false, 'required' => false,
'with_seconds' => true,
]); ]);
} }

View file

@ -38,7 +38,7 @@ class AttachmentTypeType extends AbstractType
return StructuralEntityType::class; return StructuralEntityType::class;
} }
public function configureOptions(OptionsResolver $resolver) public function configureOptions(OptionsResolver $resolver): void
{ {
$resolver->define('attachment_filter_class')->allowedTypes('null', 'string')->default(null); $resolver->define('attachment_filter_class')->allowedTypes('null', 'string')->default(null);

View file

@ -84,8 +84,10 @@ final class ProjectBuildRequest
$remaining_amount = $this->getNeededAmountForBOMEntry($bom_entry); $remaining_amount = $this->getNeededAmountForBOMEntry($bom_entry);
foreach($this->getPartLotsForBOMEntry($bom_entry) as $lot) { foreach($this->getPartLotsForBOMEntry($bom_entry) as $lot) {
//If the lot has instock use it for the build //If the lot has instock use it for the build
$this->withdraw_amounts[$lot->getID()] = min($remaining_amount, $lot->getAmount()); $id = $lot->getID() ?? throw new \RuntimeException("Part lot needs to have an ID!");
$remaining_amount -= max(0, $this->withdraw_amounts[$lot->getID()]);
$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; $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)) { if (! array_key_exists($lot_id, $this->withdraw_amounts)) {
throw new \InvalidArgumentException('The given lot is not in the withdraw amounts array!'); throw new \InvalidArgumentException('The given lot is not in the withdraw amounts array!');
} }
@ -192,10 +198,12 @@ final class ProjectBuildRequest
{ {
if ($lot instanceof PartLot) { if ($lot instanceof PartLot) {
$lot_id = $lot->getID(); $lot_id = $lot->getID();
} elseif (is_int($lot)) {
$lot_id = $lot;
} else { } 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; $this->withdraw_amounts[$lot_id] = $amount;
@ -296,7 +304,7 @@ final class ProjectBuildRequest
* @param bool $dont_check_quantity * @param bool $dont_check_quantity
* @return $this * @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; $this->dont_check_quantity = $dont_check_quantity;
return $this; return $this;

View file

@ -158,7 +158,6 @@ class DBElementRepository extends EntityRepository
{ {
$reflection = new ReflectionClass($element::class); $reflection = new ReflectionClass($element::class);
$property = $reflection->getProperty($field); $property = $reflection->getProperty($field);
$property->setAccessible(true);
$property->setValue($element, $new_value); $property->setValue($element, $new_value);
} }
} }

View file

@ -24,6 +24,7 @@ declare(strict_types=1);
namespace App\Services\AI; namespace App\Services\AI;
use App\Settings\AISettings\LMStudioSettings; use App\Settings\AISettings\LMStudioSettings;
use App\Settings\AISettings\OllamaSettings;
use App\Settings\AISettings\OpenRouterSettings; use App\Settings\AISettings\OpenRouterSettings;
use Symfony\Contracts\Translation\TranslatableInterface; use Symfony\Contracts\Translation\TranslatableInterface;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
@ -32,6 +33,7 @@ enum AIPlatforms: string implements TranslatableInterface
{ {
case OPENROUTER = 'openrouter'; case OPENROUTER = 'openrouter';
case LMSTUDIO = 'lmstudio'; 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 * 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) { return match ($this) {
self::LMSTUDIO => LMStudioSettings::class, self::LMSTUDIO => LMStudioSettings::class,
self::OPENROUTER => OpenRouterSettings::class, self::OPENROUTER => OpenRouterSettings::class,
self::OLLAMA => OllamaSettings::class,
}; };
} }

View file

@ -69,7 +69,7 @@ class AttachmentSubmitHandler
protected const BLACKLISTED_EXTENSIONS = ['php', 'phtml', 'php3', 'ph3', 'php4', 'ph4', 'php5', 'ph5', 'phtm', 'sh', protected const BLACKLISTED_EXTENSIONS = ['php', 'phtml', 'php3', 'ph3', 'php4', 'ph4', 'php5', 'ph5', 'phtm', 'sh',
'asp', 'cgi', 'py', 'pl', 'exe', 'aspx', 'js', 'mjs', 'jsp', 'css', 'jar', 'html', 'htm', 'shtm', 'shtml', 'htaccess', 'asp', 'cgi', 'py', 'pl', 'exe', 'aspx', 'js', 'mjs', 'jsp', 'css', 'jar', 'html', 'htm', 'shtm', 'shtml', 'htaccess',
'htpasswd', '']; 'htpasswd', 'phar', 'phps', ''];
public function __construct( public function __construct(
protected AttachmentPathResolver $pathResolver, protected AttachmentPathResolver $pathResolver,
@ -543,8 +543,10 @@ class AttachmentSubmitHandler
return $attachment; return $attachment;
} }
$guessed_mime_type = $this->mimeTypes->guessMimeType($path);
//Check if the file is an SVG //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); $this->SVGSanitizer->sanitizeFile($path);
} }

View file

@ -29,13 +29,13 @@ use Symfony\Contracts\Translation\TranslatorInterface;
/** /**
* Service for validating BOM import data with comprehensive validation rules * 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( public function __construct(
private readonly EntityManagerInterface $entityManager, private EntityManagerInterface $entityManager,
private readonly TranslatorInterface $translator private TranslatorInterface $translator
) { ) {
} }

View file

@ -233,7 +233,6 @@ trait PKImportHelperTrait
$reflectionClass = new \ReflectionClass($entity); $reflectionClass = new \ReflectionClass($entity);
$property = $reflectionClass->getProperty('addedDate'); $property = $reflectionClass->getProperty('addedDate');
$property->setAccessible(true);
$property->setValue($entity, $date); $property->setValue($entity, $date);
} }

View file

@ -42,7 +42,7 @@ final class DTOJsonSchemaConverter
public function getJSONSchema(): array public function getJSONSchema(): array
{ {
return [ return [
'name' => 'clock', 'name' => 'part_detail',
'strict' => true, 'strict' => true,
'schema' => [ 'schema' => [
'type' => 'object', 'type' => 'object',

View file

@ -282,6 +282,10 @@ final class AIWebProvider implements InfoProviderInterface
try { try {
$aiPlatform = $this->AIPlatformRegistry->getPlatform($this->settings->platform ?? throw new \RuntimeException('No AI platform selected') ); $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' //'openai/gpt-5-mini'
$result = $aiPlatform->invoke($this->settings->model ?? throw new \RuntimeException('No model selected'), $input, [ $result = $aiPlatform->invoke($this->settings->model ?? throw new \RuntimeException('No model selected'), $input, [
'response_format' => [ 'response_format' => [

View file

@ -169,8 +169,8 @@ class LogEntryExtraFormatter
$array['log.collection_deleted.deleted'] = sprintf( $array['log.collection_deleted.deleted'] = sprintf(
'%s: %s (%s)', '%s: %s (%s)',
$this->elementTypeNameGenerator->getLocalizedTypeLabel($context->getDeletedElementClass()), $this->elementTypeNameGenerator->getLocalizedTypeLabel($context->getDeletedElementClass()),
$context->getOldName() ?? (string) $context->getDeletedElementID(), htmlspecialchars($context->getOldName() ?? (string) $context->getDeletedElementID()),
$context->getCollectionName() htmlspecialchars($context->getCollectionName())
); );
} }

View file

@ -0,0 +1,63 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2024 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\Services\System;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
/**
* Checks whether APP_SECRET has been changed from the default value shipped with Part-DB.
*/
final readonly class AppSecretChecker
{
/** Known default/example secrets that must not be used in production. */
public const INSECURE_SECRETS = [
'a03498528f5a5fc089273ec9ae5b2849', // default in .env
'318b5d659e07a0b3f96d9b3a83b254ca', // default in .env.dev
'CHANGE_ME' //example secret used in documentation and error messages
];
public function __construct(
#[Autowire('%kernel.secret%')]
private string $appSecret,
) {
}
/**
* @return bool True if the app secret is one of the known insecure default secrets, false otherwise.
*/
public function isInsecureSecret(): bool
{
return in_array($this->appSecret, self::INSECURE_SECRETS, true);
}
/**
* Generates a new random app secret that can be used to replace the default insecure one.
* @return string
* @throws \Random\RandomException
*/
public function generateSecret(): string
{
//Symfony docs recommend 32 characters for the app secret, which are 16 random bytes when hex-encoded.
return bin2hex(random_bytes(16));
}
}

View file

@ -35,9 +35,14 @@ class AISettings
{ {
use SettingsTrait; use SettingsTrait;
public const TIMEOUT_LIMIT = 600;
#[EmbeddedSettings] #[EmbeddedSettings]
public ?OpenRouterSettings $openRouter = null; public ?OpenRouterSettings $openRouter = null;
#[EmbeddedSettings] #[EmbeddedSettings]
public ?LMStudioSettings $lmstudio = null; public ?LMStudioSettings $lmstudio = null;
#[EmbeddedSettings]
public ?OllamaSettings $ollama = null;
} }

View file

@ -23,16 +23,17 @@ declare(strict_types=1);
namespace App\Settings\AISettings; namespace App\Settings\AISettings;
use App\Form\Type\APIKeyType;
use App\Services\AI\AIPlatformSettingsInterface; use App\Services\AI\AIPlatformSettingsInterface;
use App\Settings\SettingsIcon; use App\Settings\SettingsIcon;
use Jbtronics\SettingsBundle\Metadata\EnvVarMode; use Jbtronics\SettingsBundle\Metadata\EnvVarMode;
use Jbtronics\SettingsBundle\Settings\Settings; use Jbtronics\SettingsBundle\Settings\Settings;
use Jbtronics\SettingsBundle\Settings\SettingsParameter; use Jbtronics\SettingsBundle\Settings\SettingsParameter;
use Jbtronics\SettingsBundle\Settings\SettingsTrait; use Jbtronics\SettingsBundle\Settings\SettingsTrait;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\Extension\Core\Type\UrlType; use Symfony\Component\Form\Extension\Core\Type\UrlType;
use Symfony\Component\Translation\StaticMessage; use Symfony\Component\Translation\StaticMessage;
use Symfony\Component\Translation\TranslatableMessage as TM; use Symfony\Component\Translation\TranslatableMessage as TM;
use Symfony\Component\Validator\Constraints as Assert;
#[Settings(name: 'ai_lmstudio', label: new TM("settings.ai.lmstudio"))] #[Settings(name: 'ai_lmstudio', label: new TM("settings.ai.lmstudio"))]
#[SettingsIcon("fa-robot")] #[SettingsIcon("fa-robot")]
@ -46,6 +47,14 @@ class LMStudioSettings implements AIPlatformSettingsInterface
envVar: "AI_LMSTUDIO_HOSTURL", envVarMode: EnvVarMode::OVERWRITE)] envVar: "AI_LMSTUDIO_HOSTURL", envVarMode: EnvVarMode::OVERWRITE)]
public ?string $hostURL = null; 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 public function isAIPlatformEnabled(): bool
{ {
return $this->hostURL !== null && $this->hostURL !== ""; return $this->hostURL !== null && $this->hostURL !== "";

View file

@ -0,0 +1,68 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\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 !== "";
}
}

View file

@ -30,7 +30,9 @@ use Jbtronics\SettingsBundle\Metadata\EnvVarMode;
use Jbtronics\SettingsBundle\Settings\Settings; use Jbtronics\SettingsBundle\Settings\Settings;
use Jbtronics\SettingsBundle\Settings\SettingsParameter; use Jbtronics\SettingsBundle\Settings\SettingsParameter;
use Jbtronics\SettingsBundle\Settings\SettingsTrait; use Jbtronics\SettingsBundle\Settings\SettingsTrait;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Translation\TranslatableMessage as TM; 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")] #[Settings(name: 'ai_openrouter', label: new TM("settings.ai.openrouter"), description: "settings.ai.openrouter.help")]
#[SettingsIcon("fa-robot")] #[SettingsIcon("fa-robot")]
@ -43,6 +45,14 @@ class OpenRouterSettings implements AIPlatformSettingsInterface
formOptions: ["help_html" => true], envVar: "AI_OPENROUTER_KEY", envVarMode: EnvVarMode::OVERWRITE)] formOptions: ["help_html" => true], envVar: "AI_OPENROUTER_KEY", envVarMode: EnvVarMode::OVERWRITE)]
public ?string $apiKey = null; 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 public function isAIPlatformEnabled(): bool
{ {
return $this->apiKey !== null && $this->apiKey !== ""; return $this->apiKey !== null && $this->apiKey !== "";

View file

@ -411,6 +411,18 @@
"config/packages/ai_lm_studio_platform.yaml" "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": { "symfony/ai-open-router-platform": {
"version": "0.8", "version": "0.8",
"recipe": { "recipe": {

View file

@ -85,6 +85,16 @@
{% block content %} {% block content %}
{% if insecure_app_secret and is_granted('@system.server_infos') %}
<div class="alert alert-warning" role="alert">
<h5><i class="fa-solid fa-triangle-exclamation fa-fw"></i> {% trans %}system.app_secret.insecure.title{% endtrans %}</h5>
<p class="mb-1">{% trans %}system.app_secret.insecure.message{% endtrans %}</p>
<p class="mb-0">{% trans %}system.app_secret.insecure.suggestion{% endtrans %}
<br><code>APP_SECRET={{ suggested_app_secret }}</code></p>
<small>{% trans %}update_manager.new_version_available.only_administrators_can_see{% endtrans %}</small>
</div>
{% endif %}
{% if is_granted('@system.show_updates') %} {% if is_granted('@system.show_updates') %}
{{ nv.new_version_alert(new_version_available, new_version, new_version_url) }} {{ nv.new_version_alert(new_version_available, new_version, new_version_url) }}
{% endif %} {% endif %}

View file

@ -4,7 +4,7 @@
<p class="m-0"> <p class="m-0">
<b>{% trans %}log.collection_deleted.deleted{% endtrans %}</b>: <b>{% trans %}log.collection_deleted.deleted{% endtrans %}</b>:
{{ entity_type_label(entry.deletedElementClass) }} #{{ entry.deletedElementID }} {{ type_label(entry.deletedElementClass) }} #{{ entry.deletedElementID }}
{% if entry.oldName is not empty %} {% if entry.oldName is not empty %}
({{ entry.oldName }}) ({{ entry.oldName }})
{% endif %} {% endif %}

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