Compare commits

..

114 Commits

Author SHA1 Message Date
Jan Böhmer
d0937218b9 Bumped version to 1.15.2 2025-02-01 23:47:14 +01:00
Jan Böhmer
3247a97217 Removed "All" page length option for log tables, as this easily causes an out of memory situation on the server
You can easily have thousands of log entries, making it impossible to view them all at once.

This fixes #835
2025-02-01 23:44:07 +01:00
Jan Böhmer
edd254ee06 Merge remote-tracking branch 'origin/l10n_master' 2025-02-01 23:14:50 +01:00
Jan Böhmer
42ecb83155 Rename duplicate parameters and attachments when importing from an info provider
This fixes issue #840
2025-02-01 23:14:42 +01:00
Jan Böhmer
56f801c058 New translations messages.en.xlf (English) 2025-02-01 21:30:23 +01:00
Marc
2d3d05e956 Added character symbols for degree and registered in keybindings.md (#845) 2025-02-01 21:02:02 +01:00
Marc
4321e51bf5 Update messages.de.xlf (#841)
Removed "es" in german translation of string tfa_trustedDevices.explanation
2025-02-01 20:58:55 +01:00
Jan Böhmer
be04730906 New Crowdin updates (#837)
* New translations security.en.xlf (Russian)

* New translations validators.en.xlf (Russian)

* New translations messages.en.xlf (Russian)

* New translations messages.en.xlf (English)

* New translations messages.en.xlf (Italian)
2025-02-01 20:58:35 +01:00
Jan Böhmer
aa06e1df04 Fixed exception when trying to export an empty entity list
Fixes issue #836
2025-02-01 20:57:27 +01:00
Jan Böhmer
fd7a0156bc Disable GET_GROSS_PRICES option for TME info provider when using private key. Otherwise we receive an error.
This fixes issue #838
2025-02-01 20:20:33 +01:00
Jan Böhmer
1e19ff24ba Updated dependencies 2025-02-01 20:01:33 +01:00
Jan Böhmer
0f4238291f Bumped version to 1.15.1 2025-01-07 21:56:13 +01:00
Jan Böhmer
03a2a10efd Updated dependencies 2025-01-07 16:51:34 +01:00
Jan Böhmer
04310aa2f8 Removed WebpackAutoPathSubscriber, as it seems it not necessary anymore with encore 5 2025-01-07 16:39:51 +01:00
Jan Böhmer
e8ca11a5cf Do not use a slash prefix for .setPublicPath() in webpack config. Webpack encore 5 seemed to have changed something, so that it does not get overriden anymore
We just get a warning, which we can ignore.
2025-01-07 16:14:21 +01:00
Jan Böhmer
24137b30a5 Added tests for RedirectController that simulates the situation of a reverse proxy and subdirectory 2025-01-07 15:52:28 +01:00
Jan Böhmer
4421917333 Revert "Removed baarcode scanner inbox, as we now have enough performance to apply the decoder to the full image"
This reverts commit de0832bece.
2025-01-06 00:49:16 +01:00
Jan Böhmer
25c8660c2e Bumped version to 1.15.0 2025-01-06 00:31:19 +01:00
Jan Böhmer
190e87390d New Crowdin updates (#804)
* New translations security.en.xlf (Italian)

* New translations messages.en.xlf (German)

* New translations security.en.xlf (German)
2025-01-06 00:29:40 +01:00
Jan Böhmer
c9aefdd862 Start with an empty selection of category when a new part is created, so that the user has to manually select it 2025-01-06 00:29:26 +01:00
Jan Böhmer
3ad088663f Fixed phpstan issue 2025-01-06 00:21:04 +01:00
Jan Böhmer
a29e87e5ac Fixed phpunit tests 2025-01-06 00:19:31 +01:00
Jan Böhmer
de0832bece Removed baarcode scanner inbox, as we now have enough performance to apply the decoder to the full image 2025-01-06 00:08:42 +01:00
Jan Böhmer
614697ba84 Use zxing-wasm polyfill even if native barcode detector API is available as the polyfill is sometimes better than the native on
See discussion #808 for more infos
2025-01-05 23:43:17 +01:00
Jan Böhmer
6bdf3d891a Include the JSON encoded response, when throwing an unknown response format in mouser provider
Should help to debug issue #820
2025-01-05 22:15:12 +01:00
Jan Böhmer
f75704f77c Allow users to save a new label profiles directly from the label generator dialog
This fixes issue #806
2025-01-05 22:00:07 +01:00
Jan Böhmer
9d09543eb9 Added a button to quickly go to all label profiles editor
Related to issue #806
2025-01-05 17:50:15 +01:00
Jan Böhmer
a6116398a8 Only run PartDenormalizer when importing files, otherwise it causes problems with API platform
This fixes issue #800
2025-01-05 17:37:02 +01:00
Jan Böhmer
39763b84d5 Updated dependencies 2025-01-05 15:37:21 +01:00
Jan Böhmer
8502df08fa Added test for autodetection of EIGP114 barcodes 2025-01-04 18:43:50 +01:00
Jan Böhmer
bf2a776403 Do the EIGP114 detection after userdefined barcode detection, so that users can override it on their partLots 2025-01-04 18:41:27 +01:00
Jan Böhmer
052190c69b Do not trim user_barcode for partLots 2025-01-04 18:40:18 +01:00
Jan Böhmer
8826ba6729 Fixed CSP rules to allow the WASM loading for the barcode scanner 2025-01-04 18:23:57 +01:00
Jan Böhmer
39b5240934 We are in development of version 1.15.0 2025-01-04 18:14:15 +01:00
Jan Böhmer
ddc1c286d9 Merge branch 'detector-api-polyfill' 2025-01-04 18:10:36 +01:00
Jan Böhmer
22fba37d28 Pause the camera and scanner, when navigating away from the scanner page, so that the camera feed is closed 2025-01-04 18:10:26 +01:00
Jan Böhmer
0c627a5636 Use forked version of html5-qrcode to avoid including zxing-js which will never be used then 2025-01-04 18:09:56 +01:00
Jan Böhmer
53dcd24216 Improved scanning FPS to 10 2025-01-04 16:57:41 +01:00
Jan Böhmer
4b09a321ad Polyfill BarcodeDetector API for improved performance of Html5QRcodeScanner 2025-01-04 16:49:17 +01:00
Treeed
9e85b70c17 Added capability to scan Digikey barcodes and open the local part part page based on the result (#811)
* added capability to scan digikey barcodes and open the local part page based on the digikey part number or manufacturer part number

* had replaced one too many doublequotes

* Generalized interpretation of format06 barcodes, added ids for mouser

* Renamed vendor_barcode to user_barcode in entities

* Added a own class to parse EIGP114 barcodes

* Added tests to EIGP114Barcode parser

* Refactored code

* Changed BarcodeRedirector to support the new Barcode EIGP114BarcodeScanResult class

* Added possibility to just show all information contained in a barcode

* Dont require trailer for EIGP114 barcodes, as digikey does not seem to put them onto their  barcodes

* Fixed inspection issues

---------

Co-authored-by: jona <a@b.c>
Co-authored-by: Jan Böhmer <mail@jan-boehmer.de>
2025-01-04 01:20:51 +01:00
Jan Böhmer
9c99217dee Fixed test errors 2025-01-03 20:21:17 +01:00
Jan Böhmer
afc1dbdd4b Updated recipe for symfony flex 2025-01-03 17:45:13 +01:00
Jan Böhmer
20f58fc07d Updated symfony cli recipe
This adds an error message if no composer dependencies are installed
2025-01-03 17:42:49 +01:00
Jan Böhmer
e6b78dd213 Updated dependencies 2025-01-03 17:41:37 +01:00
Jan Böhmer
63893ffabe Allow to automatically map categories from info providers to local categories using the "alternative names" system
This partially addresses issues discussed in discussion #808
2024-12-31 18:31:20 +01:00
Jan Böhmer
c9e519d0b5 Show a warning flash, when creating a part from an info provider that has no category autoprovided
This partially fixes the problems described in discussion #808
2024-12-31 18:25:28 +01:00
Jan Böhmer
273bde90f2 Use the providerID as keyword when searching infos for a part with already existing providerreference and preselect the info provider if possible 2024-12-31 18:13:15 +01:00
Treeed
92e4976396 Show when parts from info provider already exist (#810)
* added button to show existing part with same manufacturer and mpn in provider list

* added button to edit existing part in provider list

* added docstring and comments

* replaced unnecessary double quotes

* Introduced a new twig variable localPart to split up the result

* Highlight a row, if the part is already existing

* Made buttons translatable

* Improved styling of the buttons and added a badge to show a hint

* Extracted database queries for part matching into its own service and optimized the query reducing the required queries by factor 2

* Allow to find existing parts via the stored providerReference

This should allow the database to more quickly find entries

* Allow to use part name and manufacturer alternative names for mapping

* Added a button to update a local part from the info provider and moved some buttons into dropdown menu

---------

Co-authored-by: jona <a@b.c>
Co-authored-by: Jan Böhmer <mail@jan-boehmer.de>
2024-12-31 18:03:36 +01:00
Jan Böhmer
e9efbff912 Upgraded typescript version 2024-12-29 20:15:05 +01:00
Jan Böhmer
41089c08f8 Upgraded exports-loader and webpack-cli 2024-12-29 20:08:59 +01:00
Jan Böhmer
9e23e606f8 Updated marked and compression-webpack plugin 2024-12-29 19:59:31 +01:00
Jan Böhmer
b3f0fd368a Updated webpack encore to 5.0.0 2024-12-29 19:50:02 +01:00
Jan Böhmer
12bd5472e2 Fixed sizing of text in label editor input 2024-12-29 19:47:19 +01:00
Jan Böhmer
ef64779759 Updated CKeditor5 to v41 2024-12-29 19:31:04 +01:00
Jan Böhmer
b3d8076ddf Fixed fixture loading process 2024-12-29 19:06:58 +01:00
Jan Böhmer
f775203608 Use new syntax for getReference() in data fixtures 2024-12-29 13:46:22 +01:00
Jan Böhmer
a6083688e4 Upgraded frontend dependencies 2024-12-29 13:43:32 +01:00
Jan Böhmer
50689cd4e6 Updated datatables-bundle 2024-12-29 13:41:30 +01:00
Jan Böhmer
255fcbac1c Removed direct dependency to phpstan-phpdoc-parser, so that it can get removed when updating the webauthn bundle 2024-12-29 13:38:35 +01:00
Jan Böhmer
7f8ffa56e5 Removed (hopefully) unused and unnecessary dependencies 2024-12-29 13:34:10 +01:00
Jan Böhmer
9a2a5f30a3 Updated doctrine fixtures bundle 2024-12-29 13:20:12 +01:00
Jan Böhmer
a9f444cbb4 Added return types to ExponentialNumberTypes for futureproofing the implementation 2024-12-29 13:14:26 +01:00
Jan Böhmer
164efb0551 Fixed phpstan error with NodesListBuilder 2024-12-29 13:14:00 +01:00
Jan Böhmer
a37b8cbb15 Fixed introduced bug in node flattening 2024-12-28 23:19:55 +01:00
Jan Böhmer
946032a101 Fixed phpstan analysis issues and bad code that showed up with phpstan 2.0 2024-12-28 23:08:08 +01:00
Jan Böhmer
a273acbecd Updated PHPstan configuration 2024-12-28 15:12:18 +01:00
Jan Böhmer
0ceee1582e Updated phpstan to 2.0 2024-12-28 14:56:44 +01:00
Jan Böhmer
04a0369d56 Added proxmox LXC installation method to docs 2024-12-25 19:21:30 +01:00
Jan Böhmer
31a288b44d Added recommendation on installation methods 2024-12-25 19:13:28 +01:00
Jan Böhmer
502dc3aa1c Added kubernetes as available installation method. 2024-12-25 19:12:09 +01:00
Jan Böhmer
6874d7ca55 Upgraded dependencies 2024-12-23 20:34:42 +01:00
Jan Böhmer
da8f669aed Test for PHP 8.4 2024-12-23 20:26:31 +01:00
Vedran Vekic
0f92a69b03 Rewrite LCSC datasheet URL (#795) 2024-12-10 14:32:23 +01:00
Bryce Nagaj
8faa3251c4 Update verbiage (#788) 2024-12-10 14:30:47 +01:00
Marc
56fc14003c Update information_provider_system.md (#791)
Fixed minor typo in Octopart variable: PROVIDER_OCOTPART_COUNTRY
2024-12-10 14:30:16 +01:00
Jan Böhmer
b3499e4ea5 Fixed (wrongly) failing test on IPAnonymizer 2024-12-02 11:01:39 +01:00
Jan Böhmer
07b1ff9bf5 Bumped version to 1.14.5 2024-12-02 01:00:35 +01:00
Jan Böhmer
5bbf24c92e New translations security.en.xlf (English) (#786) 2024-12-02 00:22:45 +01:00
Jan Böhmer
eea8b3e679 Merge remote-tracking branch 'origin/master' 2024-12-02 00:22:20 +01:00
Jan Böhmer
e223078af9 Added a custom function to make PostgresSQL searches case insensitive
This is required only for postgres as every other database is case invariant by default. But to achieve a portable way, we implement it via a custom DQL function.

This fixes issue #784
2024-12-02 00:17:54 +01:00
dependabot[bot]
b554d0d851 Bump codecov/codecov-action from 4 to 5 (#774)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4 to 5.
- [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/v4...v5)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  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>
2024-12-01 23:16:37 +01:00
Jan Böhmer
b1ba26e0b9 Allow usernames to contain @-signs as long as it is not at the begininng
This allows to use email addresses as usernames and fixes issue #772
2024-12-01 23:05:27 +01:00
Jan Böhmer
ca8ad760d7 Allow SAML users to access the API
This fixes issue #765.
2024-12-01 22:54:22 +01:00
Jan Böhmer
80129c0a88 Use PHP 8.3 as docker image, and allow for longer error log lines
This fixes issue #781
2024-12-01 22:34:05 +01:00
Jan Böhmer
7530e62dfa Do not format error messages as JSON, this makes log files easier to read and reduces line length which can makes problems on FPM
See issue #781
2024-12-01 22:33:32 +01:00
Jan Böhmer
baf8977578 Correctly handle IP addresses containing RFC 4007 scoping 2024-12-01 19:19:04 +01:00
Jan Böhmer
c7bf843312 Updated dependencies 2024-12-01 18:48:22 +01:00
Jan Böhmer
ce6fee1682 Merge remote-tracking branch 'origin/dependabot/composer/symfony/http-client-6.4.15' 2024-11-18 15:43:25 +01:00
Jan Böhmer
2653fad488 Merge remote-tracking branch 'origin/dependabot/composer/symfony/security-http-6.4.15' 2024-11-18 15:43:16 +01:00
Jan Böhmer
dd54c46a29 Merge remote-tracking branch 'origin/l10n_master' 2024-11-18 15:43:10 +01:00
dependabot[bot]
724a0e21d3 Bump symfony/security-http from 6.4.14 to 6.4.15
Bumps [symfony/security-http](https://github.com/symfony/security-http) from 6.4.14 to 6.4.15.
- [Release notes](https://github.com/symfony/security-http/releases)
- [Changelog](https://github.com/symfony/security-http/blob/7.1/CHANGELOG.md)
- [Commits](https://github.com/symfony/security-http/compare/v6.4.14...v6.4.15)

---
updated-dependencies:
- dependency-name: symfony/security-http
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-13 18:39:42 +00:00
dependabot[bot]
578277d11f Bump symfony/http-client from 6.4.14 to 6.4.15
Bumps [symfony/http-client](https://github.com/symfony/http-client) from 6.4.14 to 6.4.15.
- [Release notes](https://github.com/symfony/http-client/releases)
- [Changelog](https://github.com/symfony/http-client/blob/7.1/CHANGELOG.md)
- [Commits](https://github.com/symfony/http-client/compare/v6.4.14...v6.4.15)

---
updated-dependencies:
- dependency-name: symfony/http-client
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-13 16:49:40 +00:00
Jan Böhmer
22258e3183 New translations validators.en.xlf (Croatian) 2024-11-10 14:30:20 +01:00
Jan Böhmer
0234463b68 Bumped version to 1.14.4 2024-11-08 23:32:34 +01:00
Jan Böhmer
ef412eef92 Fixed tests 2024-11-08 23:32:14 +01:00
Marc
76ebd22eab Fixed Typos and mistranslations in GDPR mode (DSGVO Modus) (#757)
Fixed Typo enviroment

Co-authored-by: Marc Kreidler <kreidler@nedo.com>
2024-11-08 23:22:18 +01:00
Jan Böhmer
5b0ca8e346 Updated dependencies 2024-11-08 23:15:40 +01:00
Jan Böhmer
0b6b10c27b Bumped version to 1.14.3 2024-11-05 00:11:39 +01:00
Jan Böhmer
6225d2c9b3 Implemented an easy to use APIFilter for tags
This makes the process of filters more easily and intuitive. This fixes issue #750
2024-11-05 00:07:25 +01:00
Jan Böhmer
01fc6524a4 Added a aggregate function for storelocation sorting to avoid exceptions on Postgres
Actually this was not good on other DB types too, but they just ignored the problems.

This fixes issue #734
2024-11-04 23:46:45 +01:00
Jan Böhmer
2575e6a160 Improved size and position of back to top button to avoid overlapping with buttons, etc.
This should fix issue #737
2024-11-03 23:58:06 +01:00
Jan Böhmer
484ba5ebd7 If user password set command is run in non-interactive mode, show a warning message if no password is inputted
Related to issue #748
2024-11-03 23:39:04 +01:00
Jan Böhmer
b42d98e9f8 Increase font-weight of the <dl> element in part info page to match its look to the <h5> element 2024-11-03 23:21:58 +01:00
Sam Edwards
65b2f045ac Responsive tweaks (#755)
* Change datatables markup to be responsive with BS5

* Responsive tweaks to single part info
2024-11-03 23:14:52 +01:00
Jan Böhmer
5e76451d46 Try to guess the character encodings and convert it to UTF-8 on importing
This should fix issue #749
2024-11-03 22:27:24 +01:00
Jan Böhmer
a873ad3316 Replace all unicode characters with ASCII chars in FilenameSanatizer to make filenames more sanatized 2024-11-03 22:06:42 +01:00
Jan Böhmer
b1e03f49ee Pass the docker envs for oemsecrets to Part-DB
This fixes issue #742
2024-11-03 21:46:53 +01:00
Jan Böhmer
011e23f8e6 Added polish to language selector 2024-11-03 20:04:43 +01:00
Jan Böhmer
646cd8cf22 Merge remote-tracking branch 'origin/l10n_master' 2024-11-03 20:03:28 +01:00
Jan Böhmer
52ac8a70d5 Updated dependencies 2024-11-03 20:03:24 +01:00
Jan Böhmer
e020334b73 New translations messages.en.xlf (Polish) 2024-10-19 22:40:21 +02:00
Jan Böhmer
7698e83f0b New translations messages.en.xlf (Polish) 2024-10-19 21:40:19 +02:00
Jan Böhmer
dd56f5e0c8 New translations messages.en.xlf (Polish) 2024-10-19 19:30:24 +02:00
Jan Böhmer
92c32eef74 New Crowdin updates (#738)
* New translations validators.en.xlf (Polish)

* New translations messages.en.xlf (Polish)
2024-10-19 19:24:55 +02:00
152 changed files with 20954 additions and 6689 deletions

View File

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

3
.env
View File

@@ -143,7 +143,8 @@ PROVIDER_TME_CURRENCY=EUR
PROVIDER_TME_LANGUAGE=en
# The country to get results for
PROVIDER_TME_COUNTRY=DE
# Set this to 1 to get gross prices (including VAT) instead of net prices
# [DEPRECATED] Set this to 1 to get gross prices (including VAT) instead of net prices
# With private API keys, this option cannot be used anymore is ignored by Part-DB. The VAT inclusion depends on your TME account settings.
PROVIDER_TME_GET_GROSS_PRICES=1
# Octopart / Nexar Provider:

0
.env.dev Normal file
View File

View File

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

View File

@@ -1,5 +1,5 @@
ARG BASE_IMAGE=debian:bookworm-slim
ARG PHP_VERSION=8.2
ARG PHP_VERSION=8.3
FROM ${BASE_IMAGE} AS base
ARG PHP_VERSION
@@ -125,6 +125,7 @@ upload_max_filesize=256M
post_max_size=300M
opcache.preload_user=www-data
opcache.preload=/var/www/html/config/preload.php
log_limit=8096
EOF
COPY ./.docker/symfony.conf /etc/apache2/sites-available/symfony.conf

View File

@@ -1 +1 @@
1.14.2
1.15.2

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
{
"name": "part-db/part-db-server",
"type": "project",
"license": "AGPL-3.0-or-later",
"require": {
@@ -16,7 +17,7 @@
"brick/math": "0.12.1 as 0.11.0",
"composer/ca-bundle": "^1.3",
"composer/package-versions-deprecated": "^1.11.99.5",
"doctrine/data-fixtures": "^1.6.6",
"doctrine/data-fixtures": "^2.0.0",
"doctrine/dbal": "^4.0.0",
"doctrine/doctrine-bundle": "^2.0",
"doctrine/doctrine-migrations-bundle": "^3.0",
@@ -39,12 +40,9 @@
"nelmio/cors-bundle": "^2.3",
"nelmio/security-bundle": "^3.0",
"nyholm/psr7": "^1.1",
"ocramius/proxy-manager": "2.2.*",
"omines/datatables-bundle": "^0.8.0",
"omines/datatables-bundle": "^0.9.1",
"paragonie/sodium_compat": "^1.21",
"part-db/label-fonts": "^1.0",
"phpdocumentor/reflection-docblock": "^5.2",
"phpstan/phpdoc-parser": "^1.23",
"runtime/frankenphp-symfony": "^0.2.0",
"s9e/text-formatter": "^2.1",
"scheb/2fa-backup-code": "^6.8.0",
@@ -69,7 +67,6 @@
"symfony/process": "6.4.*",
"symfony/property-access": "6.4.*",
"symfony/property-info": "6.4.*",
"symfony/proxy-manager-bridge": "6.4.*",
"symfony/rate-limiter": "6.4.*",
"symfony/runtime": "6.4.*",
"symfony/security-bundle": "6.4.*",
@@ -91,21 +88,20 @@
"twig/intl-extra": "^3.8",
"twig/markdown-extra": "^3.8",
"twig/string-extra": "^3.8",
"web-auth/webauthn-symfony-bundle": "^4.0.0",
"webmozart/assert": "^1.4"
"web-auth/webauthn-symfony-bundle": "^4.0.0"
},
"require-dev": {
"dama/doctrine-test-bundle": "^v8.0.0",
"doctrine/doctrine-fixtures-bundle": "^3.2",
"ekino/phpstan-banned-code": "^v1.0.0",
"doctrine/doctrine-fixtures-bundle": "^4.0.0",
"ekino/phpstan-banned-code": "^v3.0.0",
"jbtronics/translation-editor-bundle": "^1.0",
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "^1.4.7",
"phpstan/phpstan-doctrine": "^1.2.11",
"phpstan/phpstan-strict-rules": "^1.5",
"phpstan/phpstan-symfony": "^1.1.7",
"phpstan/phpstan": "^2.0.4",
"phpstan/phpstan-doctrine": "^2.0.1",
"phpstan/phpstan-strict-rules": "^2.0.1",
"phpstan/phpstan-symfony": "^2.0.0",
"phpunit/phpunit": "^9.5",
"rector/rector": "^1.1.1",
"rector/rector": "^2.0.4",
"roave/security-advisories": "dev-latest",
"symfony/browser-kit": "6.4.*",
"symfony/css-selector": "6.4.*",

2495
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -57,6 +57,7 @@ doctrine:
field2: App\Doctrine\Functions\Field2
natsort: App\Doctrine\Functions\Natsort
array_position: App\Doctrine\Functions\ArrayPosition
ilike: App\Doctrine\Functions\ILike
when@test:
doctrine:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -107,7 +107,7 @@ The following env configuration options are available:
default: `EUR`). If an offer is only available in a certain currency,
Part-DB will save the prices in their native currency, and you can use Part-DB currency conversion feature to convert
it to your preferred currency.
* `PROVIDER_OCOTPART_COUNTRY`: The country you want to get prices in if available (optional, 2 letter ISO-code,
* `PROVIDER_OCTOPART_COUNTRY`: The country you want to get prices in if available (optional, 2 letter ISO-code,
default: `DE`). To get the correct prices, you have to set this and the currency setting to the correct value.
* `PROVIDER_OCTOPART_SEARCH_LIMIT`: The maximum number of results to return per search (optional, default: `10`). This
affects how quickly your monthly limit is used up.

View File

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

View File

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

View File

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

View File

@@ -50,7 +50,7 @@ final class LikeFilter extends AbstractFilter
}
$parameterName = $queryNameGenerator->generateParameterName($property); // Generate a unique parameter name to avoid collisions with other filters
$queryBuilder
->andWhere(sprintf('o.%s LIKE :%s', $property, $parameterName))
->andWhere(sprintf('ILIKE(o.%s, :%s) = TRUE', $property, $parameterName))
->setParameter($parameterName, $value);
}

View File

@@ -0,0 +1,102 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2024 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\ApiPlatform\Filter;
use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Metadata\Operation;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\PropertyInfo\Type;
/**
* Due to their nature, tags are stored in a single string, separated by commas, which requires some more complex search logic.
* This filter allows to easily search for tags in a part entity.
*/
final class TagFilter extends AbstractFilter
{
protected function filterProperty(
string $property,
$value,
QueryBuilder $queryBuilder,
QueryNameGeneratorInterface $queryNameGenerator,
string $resourceClass,
?Operation $operation = null,
array $context = []
): void {
// Ignore filter if property is not enabled or mapped
if (
!$this->isPropertyEnabled($property, $resourceClass) ||
!$this->isPropertyMapped($property, $resourceClass)
) {
return;
}
//Escape any %, _ or \ in the tag
$value = addcslashes($value, '%_\\');
$tag_identifier_prefix = $queryNameGenerator->generateParameterName($property);
$expr = $queryBuilder->expr();
$tmp = $expr->orX(
'ILIKE(o.'.$property.', :' . $tag_identifier_prefix . '_1) = TRUE',
'ILIKE(o.'.$property.', :' . $tag_identifier_prefix . '_2) = TRUE',
'ILIKE(o.'.$property.', :' . $tag_identifier_prefix . '_3) = TRUE',
'ILIKE(o.'.$property.', :' . $tag_identifier_prefix . '_4) = TRUE',
);
$queryBuilder->andWhere($tmp);
//Set the parameters for the LIKE expression, in each variation of the tag (so with a comma, at the end, at the beginning, and on both ends, and equaling the tag)
$queryBuilder->setParameter($tag_identifier_prefix . '_1', '%,' . $value . ',%');
$queryBuilder->setParameter($tag_identifier_prefix . '_2', '%,' . $value);
$queryBuilder->setParameter($tag_identifier_prefix . '_3', $value . ',%');
$queryBuilder->setParameter($tag_identifier_prefix . '_4', $value);
}
public function getDescription(string $resourceClass): array
{
if (!$this->properties) {
return [];
}
$description = [];
foreach (array_keys($this->properties) as $property) {
$description[(string)$property] = [
'property' => $property,
'type' => Type::BUILTIN_TYPE_STRING,
'required' => false,
'description' => 'Filter for tags of a part',
'openapi' => [
'example' => '',
'allowReserved' => false,// if true, query parameters will be not percent-encoded
'allowEmptyValue' => true,
'explode' => false, // to be true, the type must be Type::BUILTIN_TYPE_ARRAY, ?product=blue,green will be ?product=blue&product=green
],
];
}
return $description;
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -221,7 +221,6 @@ abstract class BaseAdminController extends AbstractController
}
}
/** @var AbstractPartsContainingRepository $repo */
$repo = $this->entityManager->getRepository($this->entity_class);
return $this->render($this->twig_template, [
@@ -397,7 +396,7 @@ abstract class BaseAdminController extends AbstractController
{
if ($entity instanceof AbstractPartsContainingDBElement) {
/** @var AbstractPartsContainingRepository $repo */
$repo = $this->entityManager->getRepository($this->entity_class);
$repo = $this->entityManager->getRepository($this->entity_class); //@phpstan-ignore-line
if ($repo->getPartsCount($entity) > 0) {
$this->addFlash('error', t('entity.delete.must_not_contain_parts', ['%PATH%' => $entity->getFullPath()]));
@@ -468,6 +467,11 @@ abstract class BaseAdminController extends AbstractController
$this->denyAccessUnlessGranted('read', $entity);
$entities = $em->getRepository($this->entity_class)->findAll();
if (count($entities) === 0) {
$this->addFlash('error', 'entity.export.flash.error.no_entities');
return $this->redirectToRoute($this->route_base.'_new');
}
return $exporter->exportEntityFromRequest($entities, $request);
}

View File

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

View File

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

View File

@@ -229,6 +229,10 @@ class PartController extends AbstractController
$dto = $infoRetriever->getDetails($providerKey, $providerId);
$new_part = $infoRetriever->dtoToPart($dto);
if ($new_part->getCategory() === null || $new_part->getCategory()->getID() === null) {
$this->addFlash('warning', t("part.create_from_info_provider.no_category_yet"));
}
return $this->renderPartForm('new', $request, $new_part, [
'info_provider_dto' => $dto,
]);

View File

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

View File

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

View File

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

View File

@@ -61,10 +61,10 @@ class ToolsController extends AbstractController
'default_theme' => $this->getParameter('partdb.global_theme'),
'enabled_locales' => $this->getParameter('partdb.locale_menu'),
'demo_mode' => $this->getParameter('partdb.demo_mode'),
'gpdr_compliance' => $this->getParameter('partdb.gdpr_compliance'),
'gdpr_compliance' => $this->getParameter('partdb.gdpr_compliance'),
'use_gravatar' => $this->getParameter('partdb.users.use_gravatar'),
'email_password_reset' => $this->getParameter('partdb.users.email_pw_reset'),
'enviroment' => $this->getParameter('kernel.environment'),
'environment' => $this->getParameter('kernel.environment'),
'is_debug' => $this->getParameter('kernel.debug'),
'email_sender' => $this->getParameter('partdb.mail.sender_email'),
'email_sender_name' => $this->getParameter('partdb.mail.sender_name'),

View File

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

View File

@@ -240,7 +240,10 @@ class UserSettingsController extends AbstractController
$page_need_reload = true;
}
/** @var Form $form We need a form implementation for the next calls */
if (!$form instanceof Form) {
throw new RuntimeException('Form is not an instance of Form, so we cannot retrieve the clicked button!');
}
//Remove the avatar attachment from the user if requested
if ($form->getClickedButton() && 'remove_avatar' === $form->getClickedButton()->getName() && $user->getMasterPictureAttachment() instanceof Attachment) {
$em->remove($user->getMasterPictureAttachment());

View File

@@ -41,7 +41,7 @@ class APITokenFixtures extends Fixture implements DependentFixtureInterface
public function load(ObjectManager $manager): void
{
/** @var User $admin_user */
$admin_user = $this->getReference(UserFixtures::ADMIN);
$admin_user = $this->getReference(UserFixtures::ADMIN, User::class);
$read_only_token = new ApiToken();
$read_only_token->setUser($admin_user);

View File

@@ -35,7 +35,7 @@ use Doctrine\Persistence\ObjectManager;
class LogEntryFixtures extends Fixture implements DependentFixtureInterface
{
public function load(ObjectManager $manager)
public function load(ObjectManager $manager): void
{
$this->createCategoryEntries($manager);
$this->createDeletedCategory($manager);

View File

@@ -106,7 +106,7 @@ class PartFixtures extends Fixture implements DependentFixtureInterface
$partLot2->setComment('Test');
$partLot2->setNeedsRefill(true);
$partLot2->setStorageLocation($manager->find(StorageLocation::class, 3));
$partLot2->setVendorBarcode('lot2_vendor_barcode');
$partLot2->setUserBarcode('lot2_vendor_barcode');
$part->addPartLot($partLot2);
$orderdetail = new Orderdetail();

View File

@@ -22,6 +22,7 @@ declare(strict_types=1);
namespace App\DataFixtures;
use App\Entity\UserSystem\Group;
use App\Entity\UserSystem\User;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
@@ -41,7 +42,7 @@ class UserFixtures extends Fixture implements DependentFixtureInterface
{
$anonymous = new User();
$anonymous->setName('anonymous');
$anonymous->setGroup($this->getReference(GroupFixtures::READONLY));
$anonymous->setGroup($this->getReference(GroupFixtures::READONLY, Group::class));
$anonymous->setNeedPwChange(false);
$anonymous->setPassword($this->encoder->hashPassword($anonymous, 'test'));
$manager->persist($anonymous);
@@ -50,7 +51,7 @@ class UserFixtures extends Fixture implements DependentFixtureInterface
$admin->setName('admin');
$admin->setPassword($this->encoder->hashPassword($admin, 'test'));
$admin->setNeedPwChange(false);
$admin->setGroup($this->getReference(GroupFixtures::ADMINS));
$admin->setGroup($this->getReference(GroupFixtures::ADMINS, Group::class));
$manager->persist($admin);
$this->addReference(self::ADMIN, $admin);
@@ -60,7 +61,7 @@ class UserFixtures extends Fixture implements DependentFixtureInterface
$user->setEmail('user@invalid.invalid');
$user->setFirstName('Test')->setLastName('User');
$user->setPassword($this->encoder->hashPassword($user, 'test'));
$user->setGroup($this->getReference(GroupFixtures::USERS));
$user->setGroup($this->getReference(GroupFixtures::USERS, Group::class));
$manager->persist($user);
$noread = new User();

View File

@@ -85,15 +85,18 @@ class TagsConstraint extends AbstractConstraint
*/
protected function getExpressionForTag(QueryBuilder $queryBuilder, string $tag): Orx
{
//Escape any %, _ or \ in the tag
$tag = addcslashes($tag, '%_\\');
$tag_identifier_prefix = uniqid($this->identifier . '_', false);
$expr = $queryBuilder->expr();
$tmp = $expr->orX(
$expr->like($this->property, ':' . $tag_identifier_prefix . '_1'),
$expr->like($this->property, ':' . $tag_identifier_prefix . '_2'),
$expr->like($this->property, ':' . $tag_identifier_prefix . '_3'),
$expr->eq($this->property, ':' . $tag_identifier_prefix . '_4'),
'ILIKE(' . $this->property . ', :' . $tag_identifier_prefix . '_1) = TRUE',
'ILIKE(' . $this->property . ', :' . $tag_identifier_prefix . '_2) = TRUE',
'ILIKE(' . $this->property . ', :' . $tag_identifier_prefix . '_3) = TRUE',
'ILIKE(' . $this->property . ', :' . $tag_identifier_prefix . '_4) = TRUE',
);
//Set the parameters for the LIKE expression, in each variation of the tag (so with a comma, at the end, at the beginning, and on both ends, and equaling the tag)
@@ -130,6 +133,7 @@ class TagsConstraint extends AbstractConstraint
return;
}
//@phpstan-ignore-next-line Keep this check to ensure that everything has the same structure even if we add a new operator
if ($this->operator === 'NONE') {
$queryBuilder->andWhere($queryBuilder->expr()->not($queryBuilder->expr()->orX(...$tagsExpressions)));
return;

View File

@@ -107,7 +107,8 @@ class TextConstraint extends AbstractConstraint
}
if ($like_value !== null) {
$this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier, 'LIKE', $like_value);
$queryBuilder->andWhere(sprintf('ILIKE(%s, :%s) = TRUE', $this->property, $this->identifier));
$queryBuilder->setParameter($this->identifier, $like_value);
return;
}

View File

@@ -21,7 +21,6 @@ declare(strict_types=1);
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\DataTables\Filters;
use Doctrine\ORM\QueryBuilder;
class PartSearchFilter implements FilterInterface
@@ -132,15 +131,15 @@ class PartSearchFilter implements FilterInterface
return sprintf("REGEXP(%s, :search_query) = TRUE", $field);
}
return sprintf("%s LIKE :search_query", $field);
return sprintf("ILIKE(%s, :search_query) = TRUE", $field);
}, $fields_to_search);
//Add Or concatation of the expressions to our query
//Add Or concatenation of the expressions to our query
$queryBuilder->andWhere(
$queryBuilder->expr()->orX(...$expressions)
);
//For regex we pass the query as is, for like we add % to the start and end as wildcards
//For regex, we pass the query as is, for like we add % to the start and end as wildcards
if ($this->regex) {
$queryBuilder->setParameter('search_query', $this->keyword);
} else {

View File

@@ -57,6 +57,8 @@ use Symfony\Contracts\Translation\TranslatorInterface;
final class PartsDataTable implements DataTableTypeInterface
{
const LENGTH_MENU = [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]];
public function __construct(
private readonly EntityURLGenerator $urlGenerator,
private readonly TranslatorInterface $translator,
@@ -137,7 +139,8 @@ final class PartsDataTable implements DataTableTypeInterface
])
->add('storelocation', TextColumn::class, [
'label' => $this->translator->trans('part.table.storeLocations'),
'orderField' => 'NATSORT(_storelocations.name)',
//We need to use a aggregate function to get the first store location, as we have a one-to-many relation
'orderField' => 'NATSORT(MIN(_storelocations.name))',
'render' => fn ($value, Part $context) => $this->partDataTableHelper->renderStorageLocations($context),
], alias: 'storage_location')

View File

@@ -87,16 +87,14 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
if(!$context->getPart() instanceof Part) {
return htmlspecialchars((string) $context->getName());
}
if($context->getPart() instanceof Part) {
$tmp = $this->partDataTableHelper->renderName($context->getPart());
if($context->getName() !== null && $context->getName() !== '') {
$tmp .= '<br><b>'.htmlspecialchars($context->getName()).'</b>';
}
return $tmp;
}
//@phpstan-ignore-next-line
throw new \RuntimeException('This should never happen!');
//Part exists if we reach this point
$tmp = $this->partDataTableHelper->renderName($context->getPart());
if($context->getName() !== null && $context->getName() !== '') {
$tmp .= '<br><b>'.htmlspecialchars($context->getName()).'</b>';
}
return $tmp;
},
])
->add('ipn', TextColumn::class, [

View File

@@ -0,0 +1,71 @@
<?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\Doctrine\Functions;
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\DBAL\Platforms\SQLitePlatform;
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\SqlWalker;
use Doctrine\ORM\Query\TokenType;
/**
* A platform invariant version of the case-insensitive LIKE operation.
* On MySQL and SQLite this is the normal LIKE, but on PostgreSQL it is the ILIKE operator.
*/
class ILike extends FunctionNode
{
public $value = null;
public $expr = null;
public function parse(Parser $parser): void
{
$parser->match(TokenType::T_IDENTIFIER);
$parser->match(TokenType::T_OPEN_PARENTHESIS);
$this->value = $parser->StringPrimary();
$parser->match(TokenType::T_COMMA);
$this->expr = $parser->StringExpression();
$parser->match(TokenType::T_CLOSE_PARENTHESIS);
}
public function getSql(SqlWalker $sqlWalker): string
{
$platform = $sqlWalker->getConnection()->getDatabasePlatform();
//
if ($platform instanceof AbstractMySQLPlatform || $platform instanceof SQLitePlatform) {
$operator = 'LIKE';
} elseif ($platform instanceof PostgreSQLPlatform) {
//Use the case-insensitive operator, to have the same behavior as MySQL
$operator = 'ILIKE';
} else {
throw new \RuntimeException('Platform ' . gettype($platform) . ' does not support case insensitive like expressions.');
}
return '(' . $this->value->dispatch($sqlWalker) . ' ' . $operator . ' ' . $this->expr->dispatch($sqlWalker) . ')';
}
}

View File

@@ -44,15 +44,13 @@ class SQLiteRegexExtensionMiddlewareDriver extends AbstractDriverMiddleware
$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 && method_exists($native_connection, 'sqliteCreateFunction' )) {
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
if (method_exists($native_connection, 'sqliteCreateCollation')) {
$native_connection->sqliteCreateCollation('NATURAL_CMP', strnatcmp(...));
}
$native_connection->sqliteCreateCollation('NATURAL_CMP', strnatcmp(...));
}
}

View File

@@ -44,7 +44,7 @@ class DoNotUsePurgerFactory implements PurgerFactory
throw new \LogicException('Do not use doctrine:fixtures:load directly. Use partdb:fixtures:load instead!');
}
public function setEntityManager(EntityManagerInterface $em)
public function setEntityManager(EntityManagerInterface $em): void
{
// TODO: Implement setEntityManager() method.
}

View File

@@ -531,7 +531,7 @@ abstract class Attachment extends AbstractNamedDBElement
$url = str_replace(' ', '%20', $url);
//Only set if the URL is not empty
if ($url !== null && $url !== '') {
if ($url !== '') {
if (str_contains($url, '%BASE%') || str_contains($url, '%MEDIA%')) {
throw new InvalidArgumentException('You can not reference internal files via the url field! But nice try!');
}

View File

@@ -33,7 +33,7 @@ use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* @template-covariant AT of Attachment
* @template AT of Attachment
*/
#[ORM\MappedSuperclass(repositoryClass: AttachmentContainingDBElementRepository::class)]
abstract class AttachmentContainingDBElement extends AbstractNamedDBElement implements HasMasterAttachmentInterface, HasAttachmentsInterface

View File

@@ -33,8 +33,8 @@ use Symfony\Component\Validator\Constraints as Assert;
/**
* This abstract class is used for companies like suppliers or manufacturers.
*
* @template-covariant AT of Attachment
* @template-covariant PT of AbstractParameter
* @template AT of Attachment
* @template PT of AbstractParameter
* @extends AbstractPartsContainingDBElement<AT, PT>
*/
#[ORM\MappedSuperclass]

View File

@@ -31,8 +31,8 @@ use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* @template-covariant AT of Attachment
* @template-covariant PT of AbstractParameter
* @template AT of Attachment
* @template PT of AbstractParameter
* @extends AbstractStructuralDBElement<AT, PT>
*/
#[ORM\MappedSuperclass(repositoryClass: AbstractPartsContainingRepository::class)]

View File

@@ -53,8 +53,8 @@ use Symfony\Component\Serializer\Annotation\Groups;
*
* @see \App\Tests\Entity\Base\AbstractStructuralDBElementTest
*
* @template-covariant AT of Attachment
* @template-covariant PT of AbstractParameter
* @template AT of Attachment
* @template PT of AbstractParameter
* @template-use ParametersTrait<PT>
* @extends AttachmentContainingDBElement<AT>
* @uses ParametersTrait<PT>

View File

@@ -22,6 +22,8 @@ declare(strict_types=1);
*/
namespace App\Entity\LabelSystem;
use App\Entity\Base\AbstractDBElement;
use App\Entity\Base\AbstractNamedDBElement;
use App\Entity\Parts\Part;
use App\Entity\Parts\PartLot;
use App\Entity\Parts\StorageLocation;
@@ -34,7 +36,7 @@ enum LabelSupportedElement: string
/**
* Returns the entity class for the given element type
* @return string
* @return class-string<AbstractDBElement>
*/
public function getEntityClass(): string
{

View File

@@ -44,9 +44,9 @@ namespace App\Entity\LogSystem;
use App\Entity\Base\AbstractDBElement;
use App\Entity\UserSystem\User;
use App\Events\SecurityEvents;
use App\Helpers\IPAnonymizer;
use Doctrine\ORM\Mapping as ORM;
use InvalidArgumentException;
use Symfony\Component\HttpFoundation\IpUtils;
/**
* This log entry is created when something security related to a user happens.
@@ -127,14 +127,14 @@ class SecurityEventLogEntry extends AbstractLogEntry
* Sets the IP address used to log in the user.
*
* @param string $ip the IP address used to log in the user
* @param bool $anonymize Anonymize the IP address (remove last block) to be GPDR compliant
* @param bool $anonymize Anonymize the IP address (remove last block) to be GDPR compliant
*
* @return $this
*/
public function setIPAddress(string $ip, bool $anonymize = true): self
{
if ($anonymize) {
$ip = IpUtils::anonymize($ip);
$ip = IPAnonymizer::anonymize($ip);
}
$this->extra['i'] = $ip;

View File

@@ -22,8 +22,9 @@ declare(strict_types=1);
namespace App\Entity\LogSystem;
use App\Helpers\IPAnonymizer;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\IpUtils;
/**
* This log entry is created when a user logs in.
@@ -52,14 +53,14 @@ class UserLoginLogEntry extends AbstractLogEntry
* Sets the IP address used to log in the user.
*
* @param string $ip the IP address used to log in the user
* @param bool $anonymize Anonymize the IP address (remove last block) to be GPDR compliant
* @param bool $anonymize Anonymize the IP address (remove last block) to be GDPR compliant
*
* @return $this
*/
public function setIPAddress(string $ip, bool $anonymize = true): self
{
if ($anonymize) {
$ip = IpUtils::anonymize($ip);
$ip = IPAnonymizer::anonymize($ip);
}
$this->extra['i'] = $ip;

View File

@@ -22,8 +22,8 @@ declare(strict_types=1);
namespace App\Entity\LogSystem;
use App\Helpers\IPAnonymizer;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\IpUtils;
#[ORM\Entity]
class UserLogoutLogEntry extends AbstractLogEntry
@@ -49,14 +49,14 @@ class UserLogoutLogEntry extends AbstractLogEntry
* Sets the IP address used to log in the user.
*
* @param string $ip the IP address used to log in the user
* @param bool $anonymize Anonymize the IP address (remove last block) to be GPDR compliant
* @param bool $anonymize Anonymize the IP address (remove last block) to be GDPR compliant
*
* @return $this
*/
public function setIPAddress(string $ip, bool $anonymize = true): self
{
if ($anonymize) {
$ip = IpUtils::anonymize($ip);
$ip = IPAnonymizer::anonymize($ip);
}
$this->extra['i'] = $ip;

View File

@@ -22,6 +22,7 @@ declare(strict_types=1);
namespace App\Entity\Parts;
use App\ApiPlatform\Filter\TagFilter;
use Doctrine\Common\Collections\Criteria;
use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface;
use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter;
@@ -97,7 +98,8 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
#[ApiFilter(PropertyFilter::class)]
#[ApiFilter(EntityFilter::class, properties: ["category", "footprint", "manufacturer", "partUnit"])]
#[ApiFilter(PartStoragelocationFilter::class, properties: ["storage_location"])]
#[ApiFilter(LikeFilter::class, properties: ["name", "comment", "description", "ipn", "tags", "manufacturer_product_number"])]
#[ApiFilter(LikeFilter::class, properties: ["name", "comment", "description", "ipn", "manufacturer_product_number"])]
#[ApiFilter(TagFilter::class, properties: ["tags"])]
#[ApiFilter(BooleanFilter::class, properties: ["favorite" , "needs_review"])]
#[ApiFilter(RangeFilter::class, properties: ["mass", "minamount"])]
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]

View File

@@ -68,7 +68,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
#[ORM\Index(columns: ['needs_refill'], name: 'part_lots_idx_needs_refill')]
#[ORM\Index(columns: ['vendor_barcode'], name: 'part_lots_idx_barcode')]
#[ValidPartLot]
#[UniqueEntity(['vendor_barcode'], message: 'validator.part_lot.vendor_barcode_must_be_unique')]
#[UniqueEntity(['user_barcode'], message: 'validator.part_lot.vendor_barcode_must_be_unique')]
#[ApiResource(
operations: [
new Get(security: 'is_granted("read", object)'),
@@ -166,10 +166,10 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named
/**
* @var string|null The content of the barcode of this part lot (e.g. a barcode on the package put by the vendor)
*/
#[ORM\Column(type: Types::STRING, nullable: true)]
#[ORM\Column(name: "vendor_barcode", type: Types::STRING, nullable: true)]
#[Groups(['part_lot:read', 'part_lot:write'])]
#[Length(max: 255)]
protected ?string $vendor_barcode = null;
protected ?string $user_barcode = null;
public function __clone()
{
@@ -185,7 +185,6 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named
*
* @return bool|null True, if the part lot is expired. Returns null, if no expiration date was set.
*
* @throws Exception If an error with the DateTime occurs
*/
public function isExpired(): ?bool
{
@@ -376,19 +375,19 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named
* null if no barcode is set.
* @return string|null
*/
public function getVendorBarcode(): ?string
public function getUserBarcode(): ?string
{
return $this->vendor_barcode;
return $this->user_barcode;
}
/**
* Set the content of the barcode of this part lot (e.g. a barcode on the package put by the vendor).
* @param string|null $vendor_barcode
* @param string|null $user_barcode
* @return $this
*/
public function setVendorBarcode(?string $vendor_barcode): PartLot
public function setUserBarcode(?string $user_barcode): PartLot
{
$this->vendor_barcode = $vendor_barcode;
$this->user_barcode = $user_barcode;
return $this;
}

View File

@@ -103,7 +103,7 @@ class Supplier extends AbstractCompany
protected ?AbstractStructuralDBElement $parent = null;
/**
* @var Collection<int, Orderdetail>|Orderdetail[]
* @var Collection<int, Orderdetail>
*/
#[ORM\OneToMany(mappedBy: 'supplier', targetEntity: Orderdetail::class)]
protected Collection $orderdetails;

View File

@@ -333,7 +333,6 @@ class Project extends AbstractStructuralDBElement
{
//If this project has subprojects, and these have builds part, they must be included in the BOM
foreach ($this->getChildren() as $child) {
/** @var $child Project */
if (!$child->getBuildPart() instanceof Part) {
continue;
}

View File

@@ -256,7 +256,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
protected ?string $password = null;
#[Assert\NotBlank]
#[Assert\Regex('/^[\w\.\+\-\$]+$/', message: 'user.invalid_username')]
#[Assert\Regex('/^[\w\.\+\-\$]+[\w\.\+\-\$\@]*$/', message: 'user.invalid_username')]
#[Groups(['user:read'])]
protected string $name = '';
@@ -893,8 +893,6 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
* @param string[] $codes An array containing the backup codes
*
* @return $this
*
* @throws Exception If an error with the datetime occurs
*/
public function setBackupCodes(array $codes): self
{

View File

@@ -1,57 +0,0 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\WebpackEncoreBundle\Event\RenderAssetTagEvent;
/**
* This class fixes the wrong pathes generated by webpack using the auto publicPath mode.
* Basically it replaces the wrong /auto/ part of the path with the correct /build/ in all encore entrypoints.
*/
class WebpackAutoPathSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
RenderAssetTagEvent::class => 'onRenderAssetTag'
];
}
public function onRenderAssetTag(RenderAssetTagEvent $event): void
{
if ($event->isScriptTag()) {
$event->setAttribute('src', $this->resolveAuto($event->getUrl()));
}
if ($event->isLinkTag()) {
$event->setAttribute('href', $this->resolveAuto($event->getUrl()));
}
}
private function resolveAuto(string $path): string
{
//Replace the first occurence of /auto/ with /build/ to get the correct path
return preg_replace('/\/auto\//', '/build/', $path, 1);
}
}

View File

@@ -71,6 +71,22 @@ class LabelDialogType extends AbstractType
'label' => false,
'disabled' => !$this->security->isGranted('@labels.edit_options') || $options['disable_options'],
]);
$builder->add('save_profile_name', TextType::class, [
'required' => false,
'attr' =>[
'placeholder' => 'label_generator.save_profile_name',
]
]);
$builder->add('save_profile', SubmitType::class, [
'label' => 'label_generator.save_profile',
'disabled' => !$this->security->isGranted('@labels.create_profiles'),
'attr' => [
'class' => 'btn btn-outline-success'
]
]);
$builder->add('update', SubmitType::class, [
'label' => 'label_generator.update',
]);

View File

@@ -41,8 +41,9 @@ declare(strict_types=1);
namespace App\Form\LabelSystem;
use App\Services\LabelSystem\Barcodes\BarcodeSourceType;
use App\Services\LabelSystem\BarcodeScanner\BarcodeSourceType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\EnumType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
@@ -55,6 +56,8 @@ class ScanDialogType extends AbstractType
{
$builder->add('input', TextType::class, [
'label' => 'scan_dialog.input',
//Do not trim the input, otherwise this damages Format06 barcodes which end with non-printable characters
'trim' => false,
'attr' => [
'autofocus' => true,
'id' => 'scan_dialog_input',
@@ -71,9 +74,14 @@ class ScanDialogType extends AbstractType
null => 'scan_dialog.mode.auto',
BarcodeSourceType::INTERNAL => 'scan_dialog.mode.internal',
BarcodeSourceType::IPN => 'scan_dialog.mode.ipn',
BarcodeSourceType::VENDOR => 'scan_dialog.mode.vendor',
BarcodeSourceType::USER_DEFINED => 'scan_dialog.mode.user',
BarcodeSourceType::EIGP114 => 'scan_dialog.mode.eigp'
},
]);
$builder->add('info_mode', CheckboxType::class, [
'label' => 'scan_dialog.info_mode',
'required' => false,
]);
$builder->add('submit', SubmitType::class, [

View File

@@ -101,6 +101,8 @@ class PartBaseType extends AbstractType
'dto_value' => $dto?->category,
'label' => 'part.edit.category',
'disable_not_selectable' => true,
//Do not require category for new parts, so that the user must select the category by hand and cannot forget it (the requirement is handled by the constraint in the entity)
'required' => !$new_part,
])
->add('footprint', StructuralEntityType::class, [
'class' => Footprint::class,

View File

@@ -103,10 +103,12 @@ class PartLotType extends AbstractType
'help' => 'part_lot.owner.help',
]);
$builder->add('vendor_barcode', TextType::class, [
'label' => 'part_lot.edit.vendor_barcode',
$builder->add('user_barcode', TextType::class, [
'label' => 'part_lot.edit.user_barcode',
'help' => 'part_lot.edit.vendor_barcode.help',
'required' => false,
//Do not remove whitespace chars on the beginning and end of the string
'trim' => false,
]);
}

View File

@@ -39,7 +39,7 @@ class ExponentialNumberType extends AbstractType
return NumberType::class;
}
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
//We want to allow the full precision of the number, so disable rounding
@@ -47,7 +47,7 @@ class ExponentialNumberType extends AbstractType
]);
}
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->resetViewTransformers();

View File

@@ -43,7 +43,7 @@ class StructuralEntityChoiceHelper
/**
* Generates the choice attributes for the given AbstractStructuralDBElement.
* @return array|string[]
* @return array<string, mixed>
*/
public function generateChoiceAttr(AbstractNamedDBElement $choice, Options|array $options): array
{

View File

@@ -24,6 +24,7 @@ declare(strict_types=1);
namespace App\Form\Type\Helper;
use App\Entity\Base\AbstractNamedDBElement;
use App\Entity\Base\AbstractStructuralDBElement;
use App\Repository\StructuralDBElementRepository;
use App\Services\Trees\NodesListBuilder;
use Doctrine\ORM\EntityManagerInterface;
@@ -33,6 +34,9 @@ use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @template T of AbstractStructuralDBElement
*/
class StructuralEntityChoiceLoader extends AbstractChoiceLoader
{
private ?string $additional_element = null;
@@ -90,10 +94,14 @@ class StructuralEntityChoiceLoader extends AbstractChoiceLoader
}
}
/** @var class-string<T> $class */
$class = $this->options['class'];
/** @var StructuralDBElementRepository $repo */
/** @var StructuralDBElementRepository<T> $repo */
$repo = $this->entityManager->getRepository($class);
$entities = $repo->getNewEntityFromPath($value, '->');
$results = [];

View File

@@ -99,7 +99,6 @@ final class TriStateCheckboxType extends AbstractType implements DataTransformer
*
* @return mixed The value in the transformed representation
*
* @throws TransformationFailedException when the transformation fails
*/
public function transform(mixed $value)
{
@@ -142,8 +141,6 @@ final class TriStateCheckboxType extends AbstractType implements DataTransformer
* @param mixed $value The value in the transformed representation
*
* @return mixed The value in the original representation
*
* @throws TransformationFailedException when the transformation fails
*/
public function reverseTransform(mixed $value)
{

View File

@@ -36,6 +36,9 @@ class FilenameSanatizer
*/
public static function sanitizeFilename(string $filename): string
{
//Convert to ASCII
$filename = iconv('UTF-8', 'ASCII//TRANSLIT', $filename);
$filename = preg_replace(
'~
[<>:"/\\\|?*]| # file system reserved https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words

View File

@@ -0,0 +1,49 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2024 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\Helpers;
use Symfony\Component\HttpFoundation\IpUtils;
/**
* Utils to assist with IP anonymization.
* The IPUtils::anonymize has a certain edgecase with local-link addresses, which is handled here.
* See: https://github.com/Part-DB/Part-DB-server/issues/782
*/
final class IPAnonymizer
{
public static function anonymize(string $ip): string
{
/**
* If the IP contains a % symbol, then it is a local-link address with scoping according to RFC 4007
* In that case, we only care about the part before the % symbol, as the following functions, can only work with
* the IP address itself. As the scope can leak information (containing interface name), we do not want to
* include it in our anonymized IP data.
*/
if (str_contains($ip, '%')) {
$ip = substr($ip, 0, strpos($ip, '%'));
}
return IpUtils::anonymize($ip);
}
}

View File

@@ -75,8 +75,8 @@ class AttachmentRepository extends DBElementRepository
{
$qb = $this->createQueryBuilder('attachment');
$qb->select('COUNT(attachment)')
->where('attachment.path LIKE :http')
->orWhere('attachment.path LIKE :https');
->where('ILIKE(attachment.path, :http) = TRUE')
->orWhere('ILIKE(attachment.path, :https) = TRUE');
$qb->setParameter('http', 'http://%');
$qb->setParameter('https', 'https://%');
$query = $qb->getQuery();

View File

@@ -44,7 +44,7 @@ class ParameterRepository extends DBElementRepository
->select('parameter.name')
->addSelect('parameter.symbol')
->addSelect('parameter.unit')
->where('parameter.name LIKE :name');
->where('ILIKE(parameter.name, :name) = TRUE');
if ($exact) {
$qb->setParameter('name', $name);
} else {

View File

@@ -81,10 +81,10 @@ class PartRepository extends NamedDBElementRepository
->leftJoin('part.category', 'category')
->leftJoin('part.footprint', 'footprint')
->where('part.name LIKE :query')
->orWhere('part.description LIKE :query')
->orWhere('category.name LIKE :query')
->orWhere('footprint.name LIKE :query')
->where('ILIKE(part.name, :query) = TRUE')
->orWhere('ILIKE(part.description, :query) = TRUE')
->orWhere('ILIKE(category.name, :query) = TRUE')
->orWhere('ILIKE(footprint.name, :query) = TRUE')
;
$qb->setParameter('query', '%'.$query.'%');

View File

@@ -151,7 +151,7 @@ class StructuralDBElementRepository extends AttachmentContainingDBElementReposit
}
if (null === $entity) {
$class = $this->getClassName();
/** @var AbstractStructuralDBElement $entity */
/** @var TEntityClass $entity */
$entity = new $class;
$entity->setName($name);
$entity->setParent($parent);
@@ -265,7 +265,7 @@ class StructuralDBElementRepository extends AttachmentContainingDBElementReposit
}
$class = $this->getClassName();
/** @var AbstractStructuralDBElement $entity */
/** @var TEntityClass $entity */
$entity = new $class;
$entity->setName($name);

View File

@@ -25,6 +25,7 @@ namespace App\Security;
use App\Entity\UserSystem\User;
use Nbgrp\OneloginSamlBundle\Security\Http\Authenticator\Token\SamlToken;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAccountStatusException;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -50,13 +51,20 @@ class EnsureSAMLUserForSAMLLoginChecker implements EventSubscriberInterface
$token = $event->getAuthenticationToken();
$user = $token->getUser();
//If we are using SAML, we need to check that the user is a SAML user.
if ($token instanceof SamlToken) {
if ($user instanceof User && !$user->isSamlUser()) {
throw new CustomUserMessageAccountStatusException($this->translator->trans('saml.error.cannot_login_local_user_per_saml', [], 'security'));
}
} elseif ($user instanceof User && $user->isSamlUser()) {
//Ensure that you can not login locally with a SAML user (even if this should not happen, as the password is not set)
//Do not check for anonymous users
if (!$user instanceof User) {
return;
}
//Do not allow SAML users to login as local user
if ($token instanceof SamlToken && !$user->isSamlUser()) {
throw new CustomUserMessageAccountStatusException($this->translator->trans('saml.error.cannot_login_local_user_per_saml',
[], 'security'));
}
//Do not allow local users to login as SAML user via local username and password
if ($token instanceof UsernamePasswordToken && $user->isSamlUser()) {
//Ensure that you can not login locally with a SAML user (even though this should not happen, as the password is not set)
throw new CustomUserMessageAccountStatusException($this->translator->trans('saml.error.cannot_login_saml_user_locally', [], 'security'));
}
}

View File

@@ -40,12 +40,10 @@ final class UserChecker implements UserCheckerInterface
/**
* Checks the user account before authentication.
*
* @throws AccountStatusException
*/
public function checkPreAuth(UserInterface $user): void
{
// TODO: Implement checkPreAuth() method.
//We don't need to check the user before authentication, just implemented to fulfill the interface
}
/**

View File

@@ -94,7 +94,14 @@ class PartNormalizer implements NormalizerInterface, DenormalizerInterface, Norm
public function supportsDenormalization($data, string $type, string $format = null, array $context = []): bool
{
return !isset($context[self::ALREADY_CALLED]) && is_array($data) && is_a($type, Part::class, true);
//Only denormalize if we are doing a file import operation
if (!($context['partdb_import'] ?? false)) {
return false;
}
//Only make the denormalizer available on import operations
return !isset($context[self::ALREADY_CALLED])
&& is_array($data) && is_a($type, Part::class, true);
}
private function normalizeKeys(array &$data): array

View File

@@ -69,6 +69,15 @@ class StructuralElementDenormalizer implements DenormalizerInterface, Denormaliz
&& in_array('import', $context['groups'] ?? [], true);
}
/**
* @template T of AbstractStructuralDBElement
* @param $data
* @phpstan-param class-string<T> $type
* @param string|null $format
* @param array $context
* @return AbstractStructuralDBElement|null
* @phpstan-return T|null
*/
public function denormalize($data, string $type, string $format = null, array $context = []): ?AbstractStructuralDBElement
{
//Do not use API Platform's denormalizer
@@ -85,7 +94,7 @@ class StructuralElementDenormalizer implements DenormalizerInterface, Denormaliz
$deserialized_entity = $this->denormalizer->denormalize($data, $type, $format, $context);
//Check if we already have the entity in the database (via path)
/** @var StructuralDBElementRepository $repo */
/** @var StructuralDBElementRepository<T> $repo */
$repo = $this->entityManager->getRepository($type);
$path = $deserialized_entity->getFullPath(AbstractStructuralDBElement::PATH_DELIMITER_ARROW);

View File

@@ -54,7 +54,7 @@ class StructuralElementFromNameDenormalizer implements DenormalizerInterface
public function denormalize($data, string $type, string $format = null, array $context = []): AbstractStructuralDBElement|null
{
//Retrieve the repository for the given type
/** @var StructuralDBElementRepository $repo */
/** @var StructuralDBElementRepository<T> $repo */
$repo = $this->em->getRepository($type);
$path_delimiter = $context['path_delimiter'] ?? '->';

View File

@@ -46,7 +46,7 @@ class MoneyFormatter
public function format(string|float $value, ?Currency $currency = null, int $decimals = 5, bool $show_all_digits = false): string
{
$iso_code = $this->base_currency;
if ($currency instanceof Currency && ($currency->getIsoCode() !== null && $currency->getIsoCode() !== '')) {
if ($currency instanceof Currency && ($currency->getIsoCode() !== '')) {
$iso_code = $currency->getIsoCode();
}

View File

@@ -153,6 +153,7 @@ class BOMImporter
break;
}
//@phpstan-ignore-next-line We want to keep this check just to be safe when something changes
$new_index = self::MAP_KICAD_PCB_FIELDS[$index] ?? throw new \UnexpectedValueException('Invalid field index!');
$out[$new_index] = $field;
}

View File

@@ -44,6 +44,12 @@ use Symfony\Component\Validator\Validator\ValidatorInterface;
*/
class EntityImporter
{
/**
* The encodings that are supported by the importer, and that should be autodeceted.
*/
private const ENCODINGS = ["ASCII", "UTF-8", "ISO-8859-1", "ISO-8859-15", "Windows-1252", "UTF-16", "UTF-32"];
public function __construct(protected SerializerInterface $serializer, protected EntityManagerInterface $em, protected ValidatorInterface $validator)
{
}
@@ -58,13 +64,16 @@ class EntityImporter
* @phpstan-param class-string<T> $class_name
* @param AbstractStructuralDBElement|null $parent the element which will be used as parent element for new elements
* @param array $errors an associative array containing all validation errors
* @param-out array<string, array{'entity': object, 'violations': ConstraintViolationListInterface}> $errors
* @param-out list<array{'entity': object, 'violations': ConstraintViolationListInterface}> $errors
*
* @return AbstractNamedDBElement[] An array containing all valid imported entities (with the type $class_name)
* @return T[]
*/
public function massCreation(string $lines, string $class_name, ?AbstractStructuralDBElement $parent = null, array &$errors = []): array
{
//Try to detect the text encoding of the data and convert it to UTF-8
$lines = mb_convert_encoding($lines, 'UTF-8', mb_detect_encoding($lines, self::ENCODINGS));
//Expand every line to a single entry:
$names = explode("\n", $lines);
@@ -124,13 +133,15 @@ class EntityImporter
if ($repo instanceof StructuralDBElementRepository) {
$entities = $repo->getNewEntityFromPath($new_path);
$entity = end($entities);
if ($entity === false) {
throw new InvalidArgumentException('getNewEntityFromPath returned an empty array!');
}
} else { //Otherwise just create a new entity
$entity = new $class_name;
$entity->setName($name);
}
//Validate entity
$tmp = $this->validator->validate($entity);
//If no error occured, write entry to DB:
@@ -159,6 +170,9 @@ class EntityImporter
*/
public function importString(string $data, array $options = [], array &$errors = []): array
{
//Try to detect the text encoding of the data and convert it to UTF-8
$data = mb_convert_encoding($data, 'UTF-8', mb_detect_encoding($data, self::ENCODINGS));
$resolver = new OptionsResolver();
$this->configureOptions($resolver);
$options = $resolver->resolve($options);
@@ -215,6 +229,11 @@ class EntityImporter
//Iterate over each $entity write it to DB.
foreach ($entities as $key => $entity) {
//Ensure that entity is a NamedDBElement
if (!$entity instanceof AbstractNamedDBElement) {
throw new \RuntimeException("Encountered an entity that is not a NamedDBElement!");
}
//Validate entity
$tmp = $this->validator->validate($entity);
@@ -269,7 +288,7 @@ class EntityImporter
*
* @param File $file the file that should be used for importing
* @param array $options options for the import process
* @param AbstractNamedDBElement[] $entities The imported entities are returned in this array
* @param-out AbstractNamedDBElement[] $entities The imported entities are returned in this array
*
* @return array<string, array{'entity': object, 'violations': ConstraintViolationListInterface}> An associative array containing an ConstraintViolationList and the entity name as key are returned,
* if an error happened during validation. When everything was successfully, the array should be empty.
@@ -305,7 +324,7 @@ class EntityImporter
* @param array $options options for the import process
* @param-out array<string, array{'entity': object, 'violations': ConstraintViolationListInterface}> $errors
*
* @return array an array containing the deserialized elements
* @return AbstractNamedDBElement[] an array containing the deserialized elements
*/
public function importFile(File $file, array $options = [], array &$errors = []): array
{

View File

@@ -205,10 +205,6 @@ trait PKImportHelperTrait
*/
protected function setIDOfEntity(AbstractDBElement $element, int|string $id): void
{
if (!is_int($id) && !is_string($id)) {
throw new \InvalidArgumentException('ID must be an integer or string');
}
$id = (int) $id;
$metadata = $this->em->getClassMetadata($element::class);

View File

@@ -27,6 +27,7 @@ use App\Entity\Attachments\AttachmentType;
use App\Entity\Attachments\PartAttachment;
use App\Entity\Base\AbstractStructuralDBElement;
use App\Entity\Parameters\PartParameter;
use App\Entity\Parts\Category;
use App\Entity\Parts\Footprint;
use App\Entity\Parts\InfoProviderReference;
use App\Entity\Parts\Manufacturer;
@@ -36,6 +37,7 @@ use App\Entity\Parts\Supplier;
use App\Entity\PriceInformations\Currency;
use App\Entity\PriceInformations\Orderdetail;
use App\Entity\PriceInformations\Pricedetail;
use App\Repository\Parts\CategoryRepository;
use App\Services\InfoProviderSystem\DTOs\FileDTO;
use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
@@ -156,6 +158,12 @@ final class DTOtoEntityConverter
$entity->setMass($dto->mass);
//Try to map the category to an existing entity (but never create a new one)
if ($dto->category) {
//@phpstan-ignore-next-line For some reason php does not recognize the repo returns a category
$entity->setCategory($this->em->getRepository(Category::class)->findForInfoProvider($dto->category));
}
$entity->setManufacturer($this->getOrCreateEntity(Manufacturer::class, $dto->manufacturer));
$entity->setFootprint($this->getOrCreateEntity(Footprint::class, $dto->footprint));
@@ -166,9 +174,21 @@ final class DTOtoEntityConverter
//Set the provider reference on the part
$entity->setProviderReference(InfoProviderReference::fromPartDTO($dto));
$param_groups = [];
//Add parameters
foreach ($dto->parameters ?? [] as $parameter) {
$entity->addParameter($this->convertParameter($parameter));
$new_param = $this->convertParameter($parameter);
$key = $new_param->getName() . '##' . $new_param->getGroup();
//If there is already an parameter with the same name and group, rename the new parameter, by suffixing a number
if (count($param_groups[$key] ?? []) > 0) {
$new_param->setName($new_param->getName() . ' (' . (count($param_groups[$key]) + 1) . ')');
}
$param_groups[$key][] = $new_param;
$entity->addParameter($new_param);
}
//Add preview image
@@ -184,6 +204,8 @@ final class DTOtoEntityConverter
$entity->setMasterPictureAttachment($preview_image);
}
$attachments_grouped = [];
//Add other images
$images = $this->files_unique($dto->images ?? []);
foreach ($images as $image) {
@@ -192,14 +214,29 @@ final class DTOtoEntityConverter
continue;
}
$entity->addAttachment($this->convertFile($image, $image_type));
$attachment = $this->convertFile($image, $image_type);
$attachments_grouped[$attachment->getName()][] = $attachment;
if (count($attachments_grouped[$attachment->getName()] ?? []) > 1) {
$attachment->setName($attachment->getName() . ' (' . (count($attachments_grouped[$attachment->getName()]) + 1) . ')');
}
$entity->addAttachment($attachment);
}
//Add datasheets
$datasheet_type = $this->getDatasheetType();
$datasheets = $this->files_unique($dto->datasheets ?? []);
foreach ($datasheets as $datasheet) {
$entity->addAttachment($this->convertFile($datasheet, $datasheet_type));
$attachment = $this->convertFile($datasheet, $datasheet_type);
$attachments_grouped[$attachment->getName()][] = $attachment;
if (count($attachments_grouped[$attachment->getName()] ?? []) > 1) {
$attachment->setName($attachment->getName() . ' (' . (count($attachments_grouped[$attachment->getName()])) . ')');
}
$entity->addAttachment($attachment);
}
//Add orderdetails and prices

View File

@@ -0,0 +1,77 @@
<?php
namespace App\Services\InfoProviderSystem;
use App\Entity\Parts\Manufacturer;
use App\Entity\Parts\Part;
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
use Doctrine\ORM\EntityManagerInterface;
/**
* This service assists in finding existing local parts for a SearchResultDTO, so that the user
* does not accidentally add a duplicate.
*
* A part is considered to be a duplicate, if the provider reference matches, or if the manufacturer and the MPN of the
* DTO and the local part match. This checks also for alternative names of the manufacturer and the part name (as alternative
* for the MPN).
*/
final class ExistingPartFinder
{
public function __construct(private readonly EntityManagerInterface $em)
{
}
/**
* Return the first existing local part, that matches the search result.
* If no part is found, return null.
* @param SearchResultDTO $dto
* @return Part|null
*/
public function findFirstExisting(SearchResultDTO $dto): ?Part
{
$results = $this->findAllExisting($dto);
return count($results) > 0 ? $results[0] : null;
}
/**
* Returns all existing local parts that match the search result.
* If no part is found, return an empty array.
* @param SearchResultDTO $dto
* @return Part[]
*/
public function findAllExisting(SearchResultDTO $dto): array
{
$qb = $this->em->getRepository(Part::class)->createQueryBuilder('part');
$qb->select('part')
->leftJoin('part.manufacturer', 'manufacturer')
->Orwhere($qb->expr()->andX(
'part.providerReference.provider_key = :providerKey',
'part.providerReference.provider_id = :providerId',
))
//Or the manufacturer (allowing for alternative names) and the MPN (or part name) must match
->OrWhere(
$qb->expr()->andX(
$qb->expr()->orX(
"ILIKE(manufacturer.name, :manufacturerName) = TRUE",
"ILIKE(manufacturer.alternative_names, :manufacturerAltNames) = TRUE",
),
$qb->expr()->orX(
"ILIKE(part.manufacturer_product_number, :mpn) = TRUE",
"ILIKE(part.name, :mpn) = TRUE",
)
)
)
;
$qb->setParameter('providerKey', $dto->provider_key);
$qb->setParameter('providerId', $dto->provider_id);
$qb->setParameter('manufacturerName', $dto->manufacturer);
$qb->setParameter('manufacturerAltNames', '%'.$dto->manufacturer.'%');
$qb->setParameter('mpn', $dto->mpn);
return $qb->getQuery()->getResult();
}
}

View File

@@ -97,6 +97,9 @@ class LCSCProvider implements InfoProviderInterface
private function getRealDatasheetUrl(?string $url): string
{
if ($url !== null && trim($url) !== '' && preg_match("/^https:\/\/(datasheet\.lcsc\.com|www\.lcsc\.com\/datasheet)\/.*(C\d+)\.pdf$/", $url, $matches) > 0) {
if (preg_match("/^https:\/\/datasheet\.lcsc\.com\/lcsc\/(.*\.pdf)$/", $url, $rewriteMatches) > 0) {
$url = 'https://www.lcsc.com/datasheet/lcsc_datasheet_' . $rewriteMatches[1];
}
$response = $this->lcscClient->request('GET', $url, [
'headers' => [
'Referer' => 'https://www.lcsc.com/product-detail/_' . $matches[2] . '.html'

View File

@@ -205,7 +205,7 @@ class MouserProvider implements InfoProviderInterface
if (isset($arr['SearchResults'])) {
$products = $arr['SearchResults']['Parts'] ?? [];
} else {
throw new \RuntimeException('Unknown response format');
throw new \RuntimeException('Unknown response format: ' .json_encode($arr, JSON_THROW_ON_ERROR));
}
$result = [];

View File

@@ -771,11 +771,6 @@ class OEMSecretsProvider implements InfoProviderInterface
// Logic to extract parameters from the description
$extractedParameters = $this->parseDescriptionToParameters($description) ?? [];
// Ensure that $extractedParameters is an array
if (!is_array($extractedParameters)) {
$extractedParameters = [];
}
foreach ($extractedParameters as $newParam) {
$isDuplicate = false;
foreach ($parameters as $existingParam) {

View File

@@ -50,6 +50,16 @@ class TMEClient
return $this->token !== '' && $this->secret !== '';
}
/**
* Returns true if the client is using a private (account related token) instead of a deprecated anonymous token
* to authenticate with TME.
* @return bool
*/
public function isUsingPrivateToken(): bool
{
//Private tokens are longer than anonymous ones (50 instead of 45 characters)
return strlen($this->token) > 45;
}
/**
* Generates the signature for the given action and parameters.

View File

@@ -36,12 +36,19 @@ class TMEProvider implements InfoProviderInterface
private const VENDOR_NAME = 'TME';
/** @var bool If true, the prices are gross prices. If false, the prices are net prices. */
private readonly bool $get_gross_prices;
public function __construct(private readonly TMEClient $tmeClient, private readonly string $country,
private readonly string $language, private readonly string $currency,
/** @var bool If true, the prices are gross prices. If false, the prices are net prices. */
private readonly bool $get_gross_prices)
bool $get_gross_prices)
{
//If we have a private token, set get_gross_prices to false, as it is automatically determined by the account type then
if ($this->tmeClient->isUsingPrivateToken()) {
$this->get_gross_prices = false;
} else {
$this->get_gross_prices = $get_gross_prices;
}
}
public function getProviderInfo(): array

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