mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-02-20 16:52:41 +01:00
Compare commits
225 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac84c175af | ||
|
|
3b6014c229 | ||
|
|
9cb265c6f5 | ||
|
|
a47f0ccc12 | ||
|
|
e032f6b33d | ||
|
|
98e179ba06 | ||
|
|
2ebb4fef4c | ||
|
|
e72b120c12 | ||
|
|
7b87b00b44 | ||
|
|
2b793bf242 | ||
|
|
49ae906029 | ||
|
|
4f82a0f026 | ||
|
|
ae8edffdc8 | ||
|
|
2b67c1c631 | ||
|
|
d395cf66a0 | ||
|
|
956ed9e8ae | ||
|
|
6505af2a8b | ||
|
|
54c74bac6e | ||
|
|
5cf4c879dd | ||
|
|
aa00db48ce | ||
|
|
4a158db632 | ||
|
|
f57a0ecba2 | ||
|
|
3614c82632 | ||
|
|
9207d41f17 | ||
|
|
c5abd0ff3f | ||
|
|
2c2bde1e05 | ||
|
|
dcff8c0d9a | ||
|
|
44555e5289 | ||
|
|
9c4eff68a3 | ||
|
|
8f9122c706 | ||
|
|
3eb1b476dd | ||
|
|
0de9f48be4 | ||
|
|
162b482a8b | ||
|
|
2a46358ccf | ||
|
|
8146d6c293 | ||
|
|
5ab067cf86 | ||
|
|
720859197c | ||
|
|
ce064a0b37 | ||
|
|
bfd82fb415 | ||
|
|
655f656781 | ||
|
|
3f32841f49 | ||
|
|
4d7c021925 | ||
|
|
ac923fe669 | ||
|
|
d35b62995e | ||
|
|
ae7d2745db | ||
|
|
5ff47e63bc | ||
|
|
389341f613 | ||
|
|
00b51ad40d | ||
|
|
34c39597f5 | ||
|
|
4516e75b6f | ||
|
|
040518cca8 | ||
|
|
5d336e5fb9 | ||
|
|
e7d0103869 | ||
|
|
55cb10910f | ||
|
|
ecded8af93 | ||
|
|
20826daa18 | ||
|
|
742f1f4622 | ||
|
|
49cf20545f | ||
|
|
289e6f3d1c | ||
|
|
b246d17a33 | ||
|
|
c6b6616ee3 | ||
|
|
d6500c45aa | ||
|
|
6fd79688b0 | ||
|
|
c6478857bc | ||
|
|
8a711ffecb | ||
|
|
139ea879df | ||
|
|
6a0968cc02 | ||
|
|
5a1fa409d8 | ||
|
|
225da163bb | ||
|
|
801ed0fbaf | ||
|
|
ea44fe0f16 | ||
|
|
6081fe3295 | ||
|
|
6df65a0b9d | ||
|
|
0aec9419ec | ||
|
|
bb510a9240 | ||
|
|
cce3e1cfb8 | ||
|
|
4977f6c270 | ||
|
|
fe1715259a | ||
|
|
f4c0d84380 | ||
|
|
8a20584e27 | ||
|
|
218b0adb8f | ||
|
|
7d99607919 | ||
|
|
9adfcc7aec | ||
|
|
2da7463edf | ||
|
|
4a644d8712 | ||
|
|
afa17ca429 | ||
|
|
50708c6942 | ||
|
|
cbdf0a9392 | ||
|
|
b7c8ca2a48 | ||
|
|
3817ba774d | ||
|
|
e8771ea118 | ||
|
|
2f46fbfc7a | ||
|
|
78b0e1bf7e | ||
|
|
19530a9102 | ||
|
|
74051c5649 | ||
|
|
f3f391ab43 | ||
|
|
fc75621f1a | ||
|
|
fc3290271c | ||
|
|
71cd4057a7 | ||
|
|
485b35fbd4 | ||
|
|
172884ace8 | ||
|
|
b788c3745c | ||
|
|
7658cfcdbd | ||
|
|
52c8ea13af | ||
|
|
8f424f3273 | ||
|
|
3d7cf8f7f3 | ||
|
|
930adaf439 | ||
|
|
e5a14557a2 | ||
|
|
6a2ff9d153 | ||
|
|
bea90a7d94 | ||
|
|
e57d6e508a | ||
|
|
219b57a362 | ||
|
|
df8f54f5a4 | ||
|
|
fcbb1849ec | ||
|
|
684334ba22 | ||
|
|
5629215ce4 | ||
|
|
f63b6d7207 | ||
|
|
98dc553938 | ||
|
|
7ee01d9a05 | ||
|
|
dc6a67c2f0 | ||
|
|
affed459df | ||
|
|
8d4b8b02b8 | ||
|
|
8c430a3af0 | ||
|
|
b7573a40d7 | ||
|
|
624696711d | ||
|
|
56828e9e00 | ||
|
|
d2358c9550 | ||
|
|
ab11747fab | ||
|
|
44cb0fa434 | ||
|
|
13814695ac | ||
|
|
377e2eb613 | ||
|
|
bf4725a768 | ||
|
|
ed65abf786 | ||
|
|
8d29fe8679 | ||
|
|
f4f77c62c8 | ||
|
|
2cc08cdea1 | ||
|
|
03dc6d63ed | ||
|
|
bcb3ccec9a | ||
|
|
4bec8efea1 | ||
|
|
58b2c2bd69 | ||
|
|
636776c531 | ||
|
|
ca4a33d408 | ||
|
|
9db158f4d4 | ||
|
|
ea8b179df1 | ||
|
|
efc152e3c8 | ||
|
|
e68827bf3b | ||
|
|
58bf69882f | ||
|
|
915f313efd | ||
|
|
52d29099a2 | ||
|
|
c06fc926a1 | ||
|
|
79ab1a2277 | ||
|
|
7c03630e24 | ||
|
|
34a65419c7 | ||
|
|
7191ece7a5 | ||
|
|
0837f84a43 | ||
|
|
0bc4699cdc | ||
|
|
bb1285c35c | ||
|
|
21fc554589 | ||
|
|
a43af180a7 | ||
|
|
88ea920dfb | ||
|
|
132aac3951 | ||
|
|
7640ed08bc | ||
|
|
1dbf36b86b | ||
|
|
508de10191 | ||
|
|
ccfe259c69 | ||
|
|
0573f80525 | ||
|
|
8fb4e6c4ee | ||
|
|
05b2515b3b | ||
|
|
0ecb339fdf | ||
|
|
92ddebc289 | ||
|
|
1a3f0675bf | ||
|
|
c24019fd57 | ||
|
|
55641a234c | ||
|
|
9f52d364c9 | ||
|
|
edce70bc12 | ||
|
|
b8a7f81f55 | ||
|
|
ef9b2aefe5 | ||
|
|
cd1413a74e | ||
|
|
4e9d93957e | ||
|
|
9c4e9066f9 | ||
|
|
b4d1af2bce | ||
|
|
5ec676c40c | ||
|
|
5096aea5bb | ||
|
|
feedd190dc | ||
|
|
3423fffaca | ||
|
|
1624fd2e28 | ||
|
|
10b3094d5e | ||
|
|
580e638f67 | ||
|
|
e44428f87c | ||
|
|
379f7ef865 | ||
|
|
427f6e4d55 | ||
|
|
07a1e9fc3c | ||
|
|
78d64e8f1b | ||
|
|
559a9a9f3e | ||
|
|
ac6dd23fd6 | ||
|
|
1e515df0b5 | ||
|
|
35490762a6 | ||
|
|
c25e23d3d9 | ||
|
|
8bb8257e62 | ||
|
|
5f096927bd | ||
|
|
434826c125 | ||
|
|
89595cd5dc | ||
|
|
d991e15a94 | ||
|
|
6a1aefa5a5 | ||
|
|
272684e7eb | ||
|
|
9be3eba694 | ||
|
|
5a3fc0fb43 | ||
|
|
47ef8e9568 | ||
|
|
e4285bbc78 | ||
|
|
49b6a42791 | ||
|
|
b62fd602f2 | ||
|
|
923e40ed8f | ||
|
|
3c724a227a | ||
|
|
90d26eb16a | ||
|
|
b629744e1a | ||
|
|
b0ab43c39a | ||
|
|
2c33b381c1 | ||
|
|
c50a80e8df | ||
|
|
1534f780aa | ||
|
|
4c6ceab8e8 | ||
|
|
f3fc01b740 | ||
|
|
a201be5a01 | ||
|
|
ebf2035351 | ||
|
|
69fc28d5d6 | ||
|
|
4107535b19 |
@@ -26,10 +26,11 @@
|
||||
|
||||
# Pass the configuration from the docker env to the PHP environment (here you should list all .env options)
|
||||
PassEnv APP_ENV APP_DEBUG APP_SECRET
|
||||
PassEnv TRUSTED_PROXIES TRUSTED_HOSTS LOCK_DSN
|
||||
PassEnv DATABASE_URL ENFORCE_CHANGE_COMMENTS_FOR
|
||||
PassEnv DEFAULT_LANG DEFAULT_TIMEZONE BASE_CURRENCY INSTANCE_NAME ALLOW_ATTACHMENT_DOWNLOADS USE_GRAVATAR MAX_ATTACHMENT_FILE_SIZE DEFAULT_URI
|
||||
PassEnv MAILER_DSN ALLOW_EMAIL_PW_RESET EMAIL_SENDER_EMAIL EMAIL_SENDER_NAME
|
||||
PassEnv HISTORY_SAVE_CHANGED_FIELDS HISTORY_SAVE_CHANGED_DATA HISTORY_SAVE_REMOVED_DATA
|
||||
PassEnv HISTORY_SAVE_CHANGED_FIELDS HISTORY_SAVE_CHANGED_DATA HISTORY_SAVE_REMOVED_DATA HISTORY_SAVE_NEW_DATA
|
||||
PassEnv ERROR_PAGE_ADMIN_EMAIL ERROR_PAGE_SHOW_HELP
|
||||
PassEnv DEMO_MODE NO_URL_REWRITE_AVAILABLE FIXER_API_KEY BANNER
|
||||
PassEnv SAML_ENABLED SAML_ROLE_MAPPING SAML_UPDATE_GROUP_ON_LOGIN SAML_IDP_ENTITY_ID SAML_IDP_SINGLE_SIGN_ON_SERVICE SAML_IDP_SINGLE_LOGOUT_SERVICE SAML_IDP_X509_CERT SAML_SP_ENTITY_ID SAML_SP_X509_CERT SAMLP_SP_PRIVATE_KEY
|
||||
|
||||
3
.env
3
.env
@@ -71,6 +71,9 @@ HISTORY_SAVE_CHANGED_FIELDS=1
|
||||
HISTORY_SAVE_CHANGED_DATA=1
|
||||
# Save the data of an element that gets removed into log entry. This allows to undelete an element
|
||||
HISTORY_SAVE_REMOVED_DATA=1
|
||||
# Save the new data of an element that gets changed or added. This allows an easy comparison of the old and new data on the detail page
|
||||
# This option only becomes active when HISTORY_SAVE_CHANGED_DATA is set to 1
|
||||
HISTORY_SAVE_NEW_DATA=1
|
||||
|
||||
###################################################################################
|
||||
# Error pages settings
|
||||
|
||||
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# For sh files, always use LF line endings
|
||||
*.sh text eol=lf
|
||||
54
.github/workflows/codeql-analysis.yml
vendored
54
.github/workflows/codeql-analysis.yml
vendored
@@ -1,54 +0,0 @@
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [master]
|
||||
schedule:
|
||||
- cron: '0 14 * * 3'
|
||||
|
||||
jobs:
|
||||
analyse:
|
||||
name: Analyse
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
fetch-depth: 2
|
||||
|
||||
# If this run was triggered by a pull request event, then checkout
|
||||
# the head of the pull request instead of the merge commit.
|
||||
- run: git checkout HEAD^2
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
# Override language selection by uncommenting this and choosing your languages
|
||||
# with:
|
||||
# languages: go, javascript, csharp, python, cpp, java
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
7
.github/workflows/static_analysis.yml
vendored
7
.github/workflows/static_analysis.yml
vendored
@@ -38,9 +38,10 @@ jobs:
|
||||
|
||||
- name: Lint twig templates
|
||||
run: ./bin/console lint:twig templates --env=prod
|
||||
|
||||
- name: Lint translations
|
||||
run: ./bin/console lint:xliff translations
|
||||
|
||||
# This causes problems with emtpy language files
|
||||
#- name: Lint translations
|
||||
# run: ./bin/console lint:xliff translations
|
||||
|
||||
- name: Check dependencies for security
|
||||
uses: symfonycorp/security-checker-action@v5
|
||||
|
||||
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
php-versions: [ '7.4', '8.0', '8.1', '8.2' ]
|
||||
php-versions: [ '8.1', '8.2' ]
|
||||
db-type: [ 'mysql', 'sqlite' ]
|
||||
|
||||
env:
|
||||
@@ -109,7 +109,7 @@ jobs:
|
||||
run: php bin/console --env test doctrine:migrations:migrate -n
|
||||
|
||||
- name: Load fixtures
|
||||
run: php bin/console --env test doctrine:fixtures:load -n --purger reset_autoincrement_purger
|
||||
run: php bin/console --env test doctrine:fixtures:load -n
|
||||
|
||||
- name: Run PHPunit and generate coverage
|
||||
run: ./bin/phpunit --coverage-clover=coverage.xml
|
||||
|
||||
15
Dockerfile
15
Dockerfile
@@ -34,12 +34,25 @@ RUN sed -ri 's/^export ([^=]+)=(.*)$/: ${\1:=\2}\nexport \1/' "$APACHE_ENVVARS"
|
||||
ln -sfT /dev/stderr "$APACHE_LOG_DIR/error.log"; \
|
||||
ln -sfT /dev/stdout "$APACHE_LOG_DIR/access.log"; \
|
||||
ln -sfT /dev/stdout "$APACHE_LOG_DIR/other_vhosts_access.log"; \
|
||||
ln -sfT /dev/stderr /var/log/php8.1-fpm.log; \
|
||||
chown -R --no-dereference "$APACHE_RUN_USER:$APACHE_RUN_GROUP" "$APACHE_LOG_DIR";
|
||||
|
||||
# Enable php-fpm
|
||||
RUN a2enmod proxy_fcgi setenvif && a2enconf php8.1-fpm
|
||||
|
||||
# Configure php-fpm to log to stdout of the container (stdout of PID 1)
|
||||
# We have to use /proc/1/fd/1 because /dev/stdout or /proc/self/fd/1 does not point to the container stdout (because we use apache as entrypoint)
|
||||
# We also disable the clear_env option to allow the use of environment variables in php-fpm
|
||||
RUN { \
|
||||
echo '[global]'; \
|
||||
echo 'error_log = /proc/1/fd/1'; \
|
||||
echo; \
|
||||
echo '[www]'; \
|
||||
echo 'access.log = /proc/1/fd/1'; \
|
||||
echo 'catch_workers_output = yes'; \
|
||||
echo 'decorate_workers_output = no'; \
|
||||
echo 'clear_env = no'; \
|
||||
} | tee "/etc/php/8.1/fpm/pool.d/zz-docker.conf"
|
||||
|
||||
# PHP files should be handled by PHP, and should be preferred over any other file type
|
||||
RUN { \
|
||||
echo '<FilesMatch \.php$>'; \
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||

|
||||
[](https://codecov.io/gh/Part-DB/Part-DB-server)
|
||||

|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
@@ -60,10 +60,10 @@ Part-DB is also used by small companies and universities for managing their inve
|
||||
|
||||
## Requirements
|
||||
* A **web server** (like Apache2 or nginx) that is capable of running [Symfony 5](https://symfony.com/doc/current/reference/requirements.html),
|
||||
this includes a minimum PHP version of **PHP 7.4**
|
||||
this includes a minimum PHP version of **PHP 8.1**
|
||||
* A **MySQL** (at least 5.7) /**MariaDB** (at least 10.2.2) database server if you do not want to use SQLite.
|
||||
* Shell access to your server is highly suggested!
|
||||
* For building the client side assets **yarn** and **nodejs** is needed.
|
||||
* For building the client side assets **yarn** and **nodejs** (>= 18.0) is needed.
|
||||
|
||||
## Installation
|
||||
If you want to upgrade your legacy (< 1.0.0) version of Part-DB to this version, please read [this](https://docs.part-db.de/upgrade_legacy.html) first.
|
||||
|
||||
3
assets/bootstrap.js
vendored
3
assets/bootstrap.js
vendored
@@ -4,8 +4,7 @@ import { startStimulusApp } from '@symfony/stimulus-bridge';
|
||||
export const app = startStimulusApp(require.context(
|
||||
'@symfony/stimulus-bridge/lazy-controller-loader!./controllers',
|
||||
true,
|
||||
/\.(j|t)sx?$/
|
||||
/\.[jt]sx?$/
|
||||
));
|
||||
|
||||
// register any custom, 3rd party controllers here
|
||||
// app.register('some_controller_name', SomeImportedController);
|
||||
|
||||
@@ -181,7 +181,8 @@ Editor.defaultConfig = {
|
||||
'DejaVu Serif, serif',
|
||||
'Helvetica, Arial, sans-serif',
|
||||
'Times New Roman, Times, serif',
|
||||
'Courier New, Courier, monospace'
|
||||
'Courier New, Courier, monospace',
|
||||
'Unifont, monospace',
|
||||
],
|
||||
supportAllValues: true
|
||||
},
|
||||
|
||||
@@ -76,6 +76,7 @@ const PLACEHOLDERS = [
|
||||
['[[FOOTPRINT_FULL]]', 'Footprint (Full path)'],
|
||||
['[[MASS]]', 'Mass'],
|
||||
['[[MPN]]', 'Manufacturer Product Number (MPN)'],
|
||||
['[[IPN]]', 'Internal Part Number (IPN)'],
|
||||
['[[TAGS]]', 'Tags'],
|
||||
['[[M_STATUS]]', 'Manufacturing status'],
|
||||
['[[DESCRIPTION]]', 'Description'],
|
||||
|
||||
@@ -39,6 +39,7 @@ Object.assign( window.CKEDITOR_TRANSLATIONS[ 'de' ].dictionary, {
|
||||
'Footprint (Full path)': 'Footprint (Vollständiger Pfad)',
|
||||
'Mass': 'Gewicht',
|
||||
'Manufacturer Product Number (MPN)': 'Hersteller Produktnummer (MPN)',
|
||||
'Internal Part Number (IPN)': 'Internal Part Number (IPN)',
|
||||
'Tags': 'Tags',
|
||||
'Manufacturing status': 'Herstellungsstatus',
|
||||
'Description': 'Beschreibung',
|
||||
|
||||
@@ -18,43 +18,118 @@
|
||||
*/
|
||||
|
||||
import {Controller} from "@hotwired/stimulus";
|
||||
import Darkmode from "darkmode-js/src";
|
||||
import "darkmode-js"
|
||||
|
||||
export default class extends Controller {
|
||||
|
||||
_darkmode;
|
||||
|
||||
connect() {
|
||||
if (typeof window.getComputedStyle(document.body).mixBlendMode == 'undefined') {
|
||||
console.warn("The browser does not support mix blend mode. Darkmode will not work.");
|
||||
this.setMode(this.getMode());
|
||||
document.querySelectorAll('input[name="darkmode"]').forEach((radio) => {
|
||||
radio.addEventListener('change', this._radioChanged.bind(this));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for the change of radio buttons
|
||||
* @private
|
||||
*/
|
||||
_radioChanged(event) {
|
||||
const new_mode = this.getSelectedMode();
|
||||
this.setMode(new_mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current mode from the local storage
|
||||
* @return {'dark', 'light', 'auto'}
|
||||
*/
|
||||
getMode() {
|
||||
return localStorage.getItem('darkmode') ?? 'auto';
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the mode in the local storage and apply it and change the state of the radio buttons
|
||||
* @param mode
|
||||
*/
|
||||
setMode(mode) {
|
||||
if (mode !== 'dark' && mode !== 'light' && mode !== 'auto') {
|
||||
console.warn('Invalid darkmode mode: ' + mode);
|
||||
mode = 'auto';
|
||||
}
|
||||
|
||||
localStorage.setItem('darkmode', mode);
|
||||
|
||||
this.setButtonMode(mode);
|
||||
|
||||
if (mode === 'auto') {
|
||||
this._setDarkmodeAuto();
|
||||
} else if (mode === 'dark') {
|
||||
this._enableDarkmode();
|
||||
} else if (mode === 'light') {
|
||||
this._disableDarkmode();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the selected mode via the radio buttons
|
||||
* @return {'dark', 'light', 'auto'}
|
||||
*/
|
||||
getSelectedMode() {
|
||||
return document.querySelector('input[name="darkmode"]:checked').value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the state of the radio buttons
|
||||
* @param mode
|
||||
*/
|
||||
setButtonMode(mode) {
|
||||
document.querySelector('input[name="darkmode"][value="' + mode + '"]').checked = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable darkmode by adding the data-bs-theme="dark" to the html tag
|
||||
* @private
|
||||
*/
|
||||
_enableDarkmode() {
|
||||
//Add data-bs-theme="dark" to the html tag
|
||||
document.documentElement.setAttribute('data-bs-theme', 'dark');
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable darkmode by adding the data-bs-theme="light" to the html tag
|
||||
* @private
|
||||
*/
|
||||
_disableDarkmode() {
|
||||
//Set data-bs-theme to light
|
||||
document.documentElement.setAttribute('data-bs-theme', 'light');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the darkmode to auto and enable/disable it depending on the system settings, also add
|
||||
* an event listener to change the darkmode if the system settings change
|
||||
* @private
|
||||
*/
|
||||
_setDarkmodeAuto() {
|
||||
if (this.getMode() !== 'auto') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const darkmode = new Darkmode();
|
||||
this._darkmode = darkmode;
|
||||
|
||||
//Unhide darkmode button
|
||||
this._showWidget();
|
||||
|
||||
//Set the switch according to our current darkmode state
|
||||
const toggler = document.getElementById("toggleDarkmode");
|
||||
toggler.checked = darkmode.isActivated();
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
console.error(e);
|
||||
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
this._enableDarkmode();
|
||||
} else {
|
||||
this._disableDarkmode();
|
||||
}
|
||||
|
||||
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
|
||||
console.log('Prefered color scheme changed to ' + event.matches ? 'dark' : 'light');
|
||||
this._setDarkmodeAuto();
|
||||
});
|
||||
}
|
||||
|
||||
_showWidget() {
|
||||
this.element.classList.remove('hidden');
|
||||
}
|
||||
|
||||
toggleDarkmode() {
|
||||
this._darkmode.toggle();
|
||||
/**
|
||||
* Check if darkmode is activated
|
||||
* @return {boolean}
|
||||
*/
|
||||
isDarkmodeActivated() {
|
||||
return document.documentElement.getAttribute('data-bs-theme') === 'dark';
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,8 @@
|
||||
|
||||
import { Controller } from '@hotwired/stimulus';
|
||||
import { marked } from "marked";
|
||||
import { mangle } from "marked-mangle";
|
||||
import { gfmHeadingId } from "marked-gfm-heading-id";
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
import "../../css/app/markdown.css";
|
||||
@@ -81,6 +83,10 @@ export default class extends Controller {
|
||||
*/
|
||||
configureMarked()
|
||||
{
|
||||
marked.use(mangle());
|
||||
marked.use(gfmHeadingId({
|
||||
}));
|
||||
|
||||
marked.setOptions({
|
||||
gfm: true,
|
||||
});
|
||||
|
||||
@@ -61,7 +61,7 @@ export default class extends Controller {
|
||||
|
||||
if(!prototype) {
|
||||
console.warn("Prototype is not set, we cannot create a new element. This is most likely due to missing permissions.");
|
||||
bootbox.alert("You do not have the permsissions to create a new element. (No protoype element is set)");
|
||||
bootbox.alert("You do not have the permissions to create a new element. (No protoype element is set)");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -71,6 +71,8 @@ export default class extends Controller {
|
||||
if (data) {
|
||||
//Do not save the start value (current page), as we want to always start at the first page on a page reload
|
||||
data.start = 0;
|
||||
//50 is the default length supplied by datatables, reset it to that value
|
||||
data.length = 50;
|
||||
}
|
||||
|
||||
return data;
|
||||
@@ -97,7 +99,7 @@ export default class extends Controller {
|
||||
},
|
||||
buttons: [{
|
||||
"extend": 'colvis',
|
||||
'className': 'mr-2 btn-light',
|
||||
'className': 'mr-2 btn-outline-secondary',
|
||||
'columns': ':not(.no-colvis)',
|
||||
"text": "<i class='fa fa-cog'></i>"
|
||||
}],
|
||||
@@ -123,6 +125,22 @@ export default class extends Controller {
|
||||
console.error("Error initializing datatables: " + err);
|
||||
});
|
||||
|
||||
//Fix height of the length selector
|
||||
promise.then((dt) => {
|
||||
//Find all length selectors (select with name dt_length), which are inside a label
|
||||
const lengthSelectors = document.querySelectorAll('label select[name="dt_length"]');
|
||||
//And remove the surrounding label, while keeping the select with all event handlers
|
||||
lengthSelectors.forEach((selector) => {
|
||||
selector.parentElement.replaceWith(selector);
|
||||
});
|
||||
|
||||
//Find all column visibility buttons (button with buttons-colvis class) and remove the btn-secondary class
|
||||
const colVisButtons = document.querySelectorAll('button.buttons-colvis');
|
||||
colVisButtons.forEach((button) => {
|
||||
button.classList.remove('btn-secondary');
|
||||
});
|
||||
});
|
||||
|
||||
//Dispatch an event to let others know that the datatables has been loaded
|
||||
promise.then((dt) => {
|
||||
const event = new CustomEvent(EVENT_DT_LOADED, {bubbles: true});
|
||||
|
||||
@@ -44,6 +44,7 @@ export default class extends Controller
|
||||
const title = this.element.dataset.deleteTitle;
|
||||
|
||||
const form = this.element;
|
||||
const submitter = event.submitter;
|
||||
const that = this;
|
||||
|
||||
const confirm = bootbox.confirm({
|
||||
@@ -58,6 +59,14 @@ export default class extends Controller
|
||||
const submit_btn = document.createElement('button');
|
||||
submit_btn.type = 'submit';
|
||||
submit_btn.style.display = 'none';
|
||||
|
||||
//If the clicked button has a value, set it on the submit button
|
||||
if (submitter.value) {
|
||||
submit_btn.value = submitter.value;
|
||||
}
|
||||
if (submitter.name) {
|
||||
submit_btn.name = submitter.name;
|
||||
}
|
||||
form.appendChild(submit_btn);
|
||||
submit_btn.click();
|
||||
} else {
|
||||
|
||||
40
assets/controllers/elements/json_formatter_controller.js
Normal file
40
assets/controllers/elements/json_formatter_controller.js
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
|
||||
import {Controller} from "@hotwired/stimulus";
|
||||
|
||||
import JSONFormatter from 'json-formatter-js';
|
||||
|
||||
/**
|
||||
* This controller implements an element that renders a JSON object as a collapsible tree.
|
||||
* The JSON object is passed as a data attribute.
|
||||
* You have to apply the controller to a div element or similar block element which can contain other elements.
|
||||
*/
|
||||
export default class extends Controller {
|
||||
connect() {
|
||||
const depth_to_open = this.element.dataset.depthToOpen ?? 0;
|
||||
const json_string = this.element.dataset.json;
|
||||
const json_object = JSON.parse(json_string);
|
||||
|
||||
const formatter = new JSONFormatter(json_object, depth_to_open);
|
||||
|
||||
this.element.appendChild(formatter.render());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
|
||||
import {Controller} from "@hotwired/stimulus";
|
||||
import { zxcvbn, zxcvbnOptions } from '@zxcvbn-ts/core';
|
||||
import * as zxcvbnCommonPackage from '@zxcvbn-ts/language-common';
|
||||
import * as zxcvbnEnPackage from '@zxcvbn-ts/language-en';
|
||||
import * as zxcvbnDePackage from '@zxcvbn-ts/language-de';
|
||||
import * as zxcvbnFrPackage from '@zxcvbn-ts/language-fr';
|
||||
import * as zxcvbnJaPackage from '@zxcvbn-ts/language-ja';
|
||||
import {trans, USER_PASSWORD_STRENGTH_VERY_WEAK, USER_PASSWORD_STRENGTH_WEAK, USER_PASSWORD_STRENGTH_MEDIUM,
|
||||
USER_PASSWORD_STRENGTH_STRONG, USER_PASSWORD_STRENGTH_VERY_STRONG} from '../../translator.js';
|
||||
|
||||
/* stimulusFetch: 'lazy' */
|
||||
export default class extends Controller {
|
||||
|
||||
_passwordInput;
|
||||
|
||||
static targets = ["badge", "warning"]
|
||||
|
||||
_getTranslations() {
|
||||
//Get the current locale
|
||||
const locale = document.documentElement.lang;
|
||||
if (locale.includes('de')) {
|
||||
return zxcvbnDePackage.translations;
|
||||
} else if (locale.includes('fr')) {
|
||||
return zxcvbnFrPackage.translations;
|
||||
} else if (locale.includes('ja')) {
|
||||
return zxcvbnJaPackage.translations;
|
||||
}
|
||||
|
||||
//Fallback to english
|
||||
return zxcvbnEnPackage.translations;
|
||||
}
|
||||
|
||||
connect() {
|
||||
//Find the password input field
|
||||
this._passwordInput = this.element.querySelector('input[type="password"]');
|
||||
|
||||
//Configure zxcvbn
|
||||
const options = {
|
||||
graphs: zxcvbnCommonPackage.adjacencyGraphs,
|
||||
dictionary: {
|
||||
...zxcvbnCommonPackage.dictionary,
|
||||
// We could use the english dictionary here too, but it is very big. So we just use the common words
|
||||
//...zxcvbnEnPackage.dictionary,
|
||||
},
|
||||
translations: this._getTranslations(),
|
||||
};
|
||||
zxcvbnOptions.setOptions(options);
|
||||
|
||||
//Add event listener to the password input field
|
||||
this._passwordInput.addEventListener('input', this._onPasswordInput.bind(this));
|
||||
}
|
||||
|
||||
_onPasswordInput() {
|
||||
//Retrieve the password
|
||||
const password = this._passwordInput.value;
|
||||
|
||||
//Estimate the password strength
|
||||
const result = zxcvbn(password);
|
||||
|
||||
//Update the badge
|
||||
this.badgeTarget.parentElement.classList.remove("d-none");
|
||||
this._setBadgeToLevel(result.score);
|
||||
|
||||
this.warningTarget.innerHTML = result.feedback.warning;
|
||||
}
|
||||
|
||||
_setBadgeToLevel(level) {
|
||||
let text, classes;
|
||||
|
||||
switch (level) {
|
||||
case 0:
|
||||
text = trans(USER_PASSWORD_STRENGTH_VERY_WEAK);
|
||||
classes = "bg-danger badge-danger";
|
||||
break;
|
||||
case 1:
|
||||
text = trans(USER_PASSWORD_STRENGTH_WEAK);
|
||||
classes = "bg-warning badge-warning";
|
||||
break;
|
||||
case 2:
|
||||
text = trans(USER_PASSWORD_STRENGTH_MEDIUM)
|
||||
classes = "bg-info badge-info";
|
||||
break;
|
||||
case 3:
|
||||
text = trans(USER_PASSWORD_STRENGTH_STRONG);
|
||||
classes = "bg-primary badge-primary";
|
||||
break;
|
||||
case 4:
|
||||
text = trans(USER_PASSWORD_STRENGTH_VERY_STRONG);
|
||||
classes = "bg-success badge-success";
|
||||
break;
|
||||
default:
|
||||
text = "???";
|
||||
classes = "bg-secondary badge-secondary";
|
||||
}
|
||||
|
||||
this.badgeTarget.innerHTML = text;
|
||||
//Remove all classes
|
||||
this.badgeTarget.className = '';
|
||||
//Re-add the classes
|
||||
this.badgeTarget.classList.add("badge");
|
||||
this.badgeTarget.classList.add(...classes.split(" "));
|
||||
}
|
||||
}
|
||||
@@ -57,10 +57,29 @@ export default class extends Controller {
|
||||
'<small class="text-muted float-end">(' + addHint +')</small>' +
|
||||
'</div>';
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
//Add callbacks to update validity
|
||||
onInitialize: this.updateValidity.bind(this),
|
||||
onChange: this.updateValidity.bind(this),
|
||||
};
|
||||
|
||||
this._tomSelect = new TomSelect(this.element, settings);
|
||||
this._tomSelect.sync();
|
||||
}
|
||||
|
||||
|
||||
updateValidity() {
|
||||
//Mark this input as invalid, if the selected option is disabled
|
||||
|
||||
const input = this.element;
|
||||
const selectedOption = input.options[input.selectedIndex];
|
||||
|
||||
if (selectedOption && selectedOption.disabled) {
|
||||
input.setCustomValidity("This option was disabled. Please select another option.");
|
||||
} else {
|
||||
input.setCustomValidity("");
|
||||
}
|
||||
}
|
||||
|
||||
getTomSelect() {
|
||||
|
||||
@@ -81,6 +81,14 @@ export default class extends Controller {
|
||||
this._tree.remove();
|
||||
}
|
||||
|
||||
const BS53Theme = {
|
||||
getOptions() {
|
||||
return {
|
||||
onhoverColor: 'var(--bs-secondary-bg)',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
this._tree = new BSTreeView(this.treeTarget, {
|
||||
levels: 1,
|
||||
showTags: this._showTags,
|
||||
@@ -93,7 +101,7 @@ export default class extends Controller {
|
||||
}
|
||||
},
|
||||
//onNodeContextmenu: contextmenu_handler,
|
||||
}, [BS5Theme, FAIconTheme]);
|
||||
}, [BS5Theme, BS53Theme, FAIconTheme]);
|
||||
|
||||
this.treeTarget.addEventListener(EVENT_INITIALIZED, (event) => {
|
||||
/** @type {BSTreeView} */
|
||||
|
||||
@@ -99,10 +99,25 @@ label:not(.form-check-label, .custom-control-label) {
|
||||
|
||||
form .col-form-label.required:after, form label.required:after {
|
||||
bottom: 4px;
|
||||
color: var(--bs-dark);
|
||||
color: var(--bs-secondary-color);
|
||||
content: "\2022";
|
||||
filter: opacity(75%);
|
||||
position: relative;
|
||||
right: -2px;
|
||||
z-index: 700;
|
||||
}
|
||||
|
||||
/****************************************
|
||||
* HTML diff styling
|
||||
****************************************/
|
||||
|
||||
/* Insertations are marked with green background and bold */
|
||||
ins {
|
||||
background-color: #95f095;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
del {
|
||||
background-color: #f09595;
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -79,7 +79,7 @@ ul.structural_link li {
|
||||
/* Add a slash symbol (/) before/behind each list item */
|
||||
ul.structural_link li+li:before {
|
||||
padding: 2px;
|
||||
color: grey;
|
||||
color: var(--bs-tertiary-color);
|
||||
/*content: "/\00a0";*/
|
||||
font-family: "Font Awesome 5 Free";
|
||||
font-weight: 900;
|
||||
@@ -89,13 +89,13 @@ ul.structural_link li+li:before {
|
||||
|
||||
/* Add a color to all links inside the list */
|
||||
ul.structural_link li a {
|
||||
color: #0275d8;
|
||||
color: var(--bs-link-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Add a color on mouse-over */
|
||||
ul.structural_link li a:hover {
|
||||
color: #01447e;
|
||||
color: var(--bs-link-hover-color);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
|
||||
@@ -78,8 +78,6 @@ body {
|
||||
overflow: -moz-scrollbars-none;
|
||||
/* Use standard version for hiding the scrollbar */
|
||||
scrollbar-width: none;
|
||||
|
||||
background-color: var(--light);
|
||||
}
|
||||
|
||||
#sidebar-container {
|
||||
|
||||
@@ -91,7 +91,7 @@ th.select-checkbox {
|
||||
/** Fix datatables select-checkbox position */
|
||||
table.dataTable tr.selected td.select-checkbox:after
|
||||
{
|
||||
margin-top: -28px !important;
|
||||
margin-top: -25px !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -116,23 +116,33 @@ table.dataTable > thead > tr > th.select-checkbox:before,
|
||||
table.dataTable > thead > tr > th.select-checkbox:after {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 1.2em;
|
||||
top: 0.9em;
|
||||
left: 50%;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
width: 1em !important;
|
||||
height: 1em !important;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
table.dataTable > thead > tr > th.select-checkbox:before {
|
||||
content: " ";
|
||||
margin-top: -5px;
|
||||
margin-left: -6px;
|
||||
border: 1px solid black;
|
||||
border: 2px solid var(--bs-tertiary-color);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
table.dataTable > tbody > tr > td.select-checkbox:before, table.dataTable > tbody > tr > th.select-checkbox:before {
|
||||
border: 2px solid var(--bs-tertiary-color) !important;
|
||||
}
|
||||
|
||||
table.dataTable > tbody > tr > td.select-checkbox:before, table.dataTable > tbody > tr > td.select-checkbox:after, table.dataTable > tbody > tr > th.select-checkbox:before, table.dataTable > tbody > tr > th.select-checkbox:after {
|
||||
width: 1em !important;
|
||||
height: 1em !important;
|
||||
}
|
||||
|
||||
table.dataTable > thead > tr.selected > th.select-checkbox:after {
|
||||
content: "✓";
|
||||
font-size: 20px;
|
||||
margin-top: -23px;
|
||||
margin-top: -20px;
|
||||
margin-left: -6px;
|
||||
text-align: center;
|
||||
/*text-shadow: 1px 1px #B0BED9, -1px -1px #B0BED9, 1px -1px #B0BED9, -1px 1px #B0BED9; */
|
||||
|
||||
@@ -36,3 +36,42 @@
|
||||
.ck-html-label .ck-content hr {
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
/***********************************************
|
||||
* Hide CKEditor powered by message
|
||||
***********************************************/
|
||||
.ck-powered-by {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/***********************************************
|
||||
* Use Bootstrap color vars for CKEditor
|
||||
***********************************************/
|
||||
:root {
|
||||
--ck-color-base-foreground: var(--bs-secondary-bg);
|
||||
--ck-color-base-background: var(--bs-body-bg);
|
||||
--ck-color-base-border: var(--bs-border-color);
|
||||
--ck-color-base-action: var(--bs-success);
|
||||
--ck-color-base-focus: var(--bs-primary-border-subtle);
|
||||
--ck-color-base-text: var(--bs-body-color);
|
||||
--ck-color-base-active: var(--bs-primary-bg-subtle);
|
||||
--ck-color-base-active-focus: var(--bs-primary);
|
||||
--ck-color-base-error: var(--bs-danger);
|
||||
|
||||
/* Improve contrast between text and toolbar */
|
||||
--ck-color-toolbar-background: var(--bs-tertiary-bg);
|
||||
|
||||
/* Buttons */
|
||||
--ck-color-button-default-hover-background: var(--bs-secondary-bg);
|
||||
--ck-color-button-default-active-background: var(--bs-secondary-bg);
|
||||
|
||||
--ck-color-button-on-background: var(--bs-body-bg);
|
||||
--ck-color-button-on-hover-background: var(--bs-secondary-bg);
|
||||
--ck-color-button-on-active-background: var(--bs-secondary-bg);
|
||||
--ck-color-button-on-disabled-background: var(--bs-secondary-bg);
|
||||
--ck-color-button-on-color: var(--bs-primary)
|
||||
|
||||
}
|
||||
@@ -18,6 +18,29 @@
|
||||
*/
|
||||
|
||||
.tagsinput.ts-wrapper.multi .ts-control > div {
|
||||
background: var(--bs-secondary);
|
||||
color: var(--bs-white);
|
||||
}
|
||||
background: var(--bs-secondary-bg);
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
|
||||
/*********
|
||||
* BS 5.3 compatible dark mode
|
||||
***************/
|
||||
|
||||
.ts-dropdown .active {
|
||||
background-color: var(--bs-secondary-bg) !important;
|
||||
color: var(--bs-body-color) !important;
|
||||
}
|
||||
|
||||
.ts-dropdown, .ts-control, .ts-control input {
|
||||
color: var(--bs-body-color) !important;
|
||||
}
|
||||
|
||||
.ts-dropdown, .ts-dropdown.form-control, .ts-dropdown.form-select {
|
||||
background: var(--bs-body-bg);
|
||||
}
|
||||
|
||||
.ts-dropdown .optgroup-header {
|
||||
color: var(--bs-tertiary-color);
|
||||
background: var(--bs-body-bg);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
3
assets/fonts/dompdf/.gitignore
vendored
Normal file
3
assets/fonts/dompdf/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Ignore font files
|
||||
*.otf
|
||||
*.ttf
|
||||
1
assets/fonts/dompdf/README.md
Normal file
1
assets/fonts/dompdf/README.md
Normal file
@@ -0,0 +1 @@
|
||||
Put your font ttf files in this folder to make them available to the label generator.
|
||||
@@ -22,7 +22,6 @@
|
||||
|
||||
import '../css/app/layout.css';
|
||||
import '../css/app/helpers.css';
|
||||
import '../css/app/darkmode.css';
|
||||
import '../css/app/tables.css';
|
||||
import '../css/app/bs-overrides.css';
|
||||
import '../css/app/treeview.css';
|
||||
|
||||
@@ -62,7 +62,7 @@ class RegisterEventHelper {
|
||||
this.registerLoadHandler(() => {
|
||||
$(".tooltip").remove();
|
||||
//Exclude dropdown buttons from tooltips, otherwise we run into endless errors from bootstrap (bootstrap.esm.js:614 Bootstrap doesn't allow more than one instance per element. Bound instance: bs.dropdown.)
|
||||
$('a[title], button[title]:not([data-bs-toggle="dropdown"]), p[title], span[title], h6[title], h3[title], i.fas[title]')
|
||||
$('a[title], label[title], button[title]:not([data-bs-toggle="dropdown"]), p[title], span[title], h6[title], h3[title], i.fas[title]')
|
||||
//@ts-ignore
|
||||
.tooltip("hide").tooltip({container: "body", placement: "auto", boundary: 'window'});
|
||||
});
|
||||
|
||||
@@ -21,8 +21,13 @@
|
||||
|
||||
class WebauthnTFA {
|
||||
|
||||
// Decodes a Base64Url string
|
||||
_base64UrlDecode = (input) => {
|
||||
_b64UrlSafeEncode = (str) => {
|
||||
const b64 = btoa(str);
|
||||
return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
||||
}
|
||||
|
||||
// Decodes a Base64Url string
|
||||
_b64UrlSafeDecode = (input) => {
|
||||
input = input
|
||||
.replace(/-/g, '+')
|
||||
.replace(/_/g, '/');
|
||||
@@ -39,13 +44,16 @@ class WebauthnTFA {
|
||||
};
|
||||
|
||||
// Converts an array of bytes into a Base64Url string
|
||||
_arrayToBase64String = (a) => btoa(String.fromCharCode(...a));
|
||||
_arrayToBase64String = (a) => {
|
||||
const str = String.fromCharCode(...a);
|
||||
return this._b64UrlSafeEncode(str);
|
||||
}
|
||||
|
||||
// Prepares the public key options object returned by the Webauthn Framework
|
||||
_preparePublicKeyOptions = publicKey => {
|
||||
//Convert challenge from Base64Url string to Uint8Array
|
||||
publicKey.challenge = Uint8Array.from(
|
||||
this._base64UrlDecode(publicKey.challenge),
|
||||
this._b64UrlSafeDecode(publicKey.challenge),
|
||||
c => c.charCodeAt(0)
|
||||
);
|
||||
|
||||
@@ -67,7 +75,7 @@ class WebauthnTFA {
|
||||
return {
|
||||
...data,
|
||||
id: Uint8Array.from(
|
||||
this._base64UrlDecode(data.id),
|
||||
this._b64UrlSafeDecode(data.id),
|
||||
c => c.charCodeAt(0)
|
||||
),
|
||||
};
|
||||
@@ -81,7 +89,7 @@ class WebauthnTFA {
|
||||
return {
|
||||
...data,
|
||||
id: Uint8Array.from(
|
||||
this._base64UrlDecode(data.id),
|
||||
this._b64UrlSafeDecode(data.id),
|
||||
c => c.charCodeAt(0)
|
||||
),
|
||||
};
|
||||
|
||||
16
assets/translator.js
Normal file
16
assets/translator.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { localeFallbacks } from '../var/translations/configuration';
|
||||
import { trans, getLocale, setLocale, setLocaleFallbacks } from '@symfony/ux-translator';
|
||||
/*
|
||||
* This file is part of the Symfony UX Translator package.
|
||||
*
|
||||
* If folder "../var/translations" does not exist, or some translations are missing,
|
||||
* you must warmup your Symfony cache to refresh JavaScript translations.
|
||||
*
|
||||
* If you use TypeScript, you can rename this file to "translator.ts" to take advantage of types checking.
|
||||
*/
|
||||
|
||||
setLocaleFallbacks(localeFallbacks);
|
||||
|
||||
export { trans };
|
||||
|
||||
export * from '../var/translations';
|
||||
116
composer.json
116
composer.json
@@ -2,74 +2,78 @@
|
||||
"type": "project",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"require": {
|
||||
"php": "^7.4 || ^8.0",
|
||||
"php": "^8.1",
|
||||
"ext-ctype": "*",
|
||||
"ext-dom": "*",
|
||||
"ext-gd": "*",
|
||||
"ext-iconv": "*",
|
||||
"ext-intl": "*",
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*",
|
||||
"ext-dom": "*",
|
||||
"beberlei/doctrineextensions": "^1.2",
|
||||
"brick/math": "^0.8.15",
|
||||
"composer/package-versions-deprecated": "1.11.99.4",
|
||||
"doctrine/annotations": "^1.6",
|
||||
"brick/math": "^0.11.0",
|
||||
"composer/package-versions-deprecated": "^1.11.99.5",
|
||||
"doctrine/annotations": "1.14.3",
|
||||
"doctrine/data-fixtures": "^1.6.6",
|
||||
"doctrine/dbal": "^3.4.6",
|
||||
"doctrine/doctrine-bundle": "^2.0",
|
||||
"doctrine/doctrine-migrations-bundle": "^3.0",
|
||||
"doctrine/orm": "^2.9",
|
||||
"dompdf/dompdf": "^2.0.0",
|
||||
"dompdf/dompdf": "dev-master#87bea32efe0b0db309e1d31537201f64d5508280 as v2.0.3",
|
||||
"erusev/parsedown": "^1.7",
|
||||
"florianv/swap": "^4.0",
|
||||
"florianv/swap-bundle": "dev-master",
|
||||
"gregwar/captcha-bundle": "^2.1.0",
|
||||
"hslavich/oneloginsaml-bundle": "^2.10",
|
||||
"jbtronics/2fa-webauthn": "^1.0.0",
|
||||
"jbtronics/2fa-webauthn": "^v2.0.0",
|
||||
"jbtronics/dompdf-font-loader-bundle": "^1.0.0",
|
||||
"jfcherng/php-diff": "^6.14",
|
||||
"league/csv": "^9.8.0",
|
||||
"league/html-to-markdown": "^5.0.1",
|
||||
"liip/imagine-bundle": "^2.2",
|
||||
"nbgrp/onelogin-saml-bundle": "^1.3",
|
||||
"nelexa/zip": "^4.0",
|
||||
"nelmio/security-bundle": "^3.0",
|
||||
"nyholm/psr7": "^1.1",
|
||||
"ocramius/proxy-manager": "2.2.*",
|
||||
"omines/datatables-bundle": "^0.5.0",
|
||||
"omines/datatables-bundle": "^0.7.2",
|
||||
"part-db/label-fonts": "^1.0",
|
||||
"php-translation/symfony-bundle": "^0.13.0",
|
||||
"phpdocumentor/reflection-docblock": "^5.2",
|
||||
"s9e/text-formatter": "^2.1",
|
||||
"scheb/2fa-backup-code": "^5.13",
|
||||
"scheb/2fa-bundle": "^5.13",
|
||||
"scheb/2fa-google-authenticator": "^5.13",
|
||||
"scheb/2fa-trusted-device": "^5.13",
|
||||
"sensio/framework-extra-bundle": "^6.1.1",
|
||||
"scheb/2fa-backup-code": "^6.8.0",
|
||||
"scheb/2fa-bundle": "^6.8.0",
|
||||
"scheb/2fa-google-authenticator": "^6.8.0",
|
||||
"scheb/2fa-trusted-device": "^6.8.0",
|
||||
"shivas/versioning-bundle": "^4.0",
|
||||
"spatie/db-dumper": "^2.21",
|
||||
"spatie/db-dumper": "^3.3.1",
|
||||
"symfony/apache-pack": "^1.0",
|
||||
"symfony/asset": "5.4.*",
|
||||
"symfony/console": "5.4.*",
|
||||
"symfony/dotenv": "5.4.*",
|
||||
"symfony/expression-language": "5.4.*",
|
||||
"symfony/flex": "^1.1",
|
||||
"symfony/form": "5.4.*",
|
||||
"symfony/framework-bundle": "5.4.*",
|
||||
"symfony/http-client": "5.4.*",
|
||||
"symfony/http-kernel": "5.4.*",
|
||||
"symfony/mailer": "5.4.*",
|
||||
"symfony/asset": "6.3.*",
|
||||
"symfony/console": "6.3.*",
|
||||
"symfony/dotenv": "6.3.*",
|
||||
"symfony/expression-language": "6.3.*",
|
||||
"symfony/flex": "^v2.3.1",
|
||||
"symfony/form": "6.3.*",
|
||||
"symfony/framework-bundle": "6.3.*",
|
||||
"symfony/http-client": "6.3.*",
|
||||
"symfony/http-kernel": "6.3.*",
|
||||
"symfony/mailer": "6.3.*",
|
||||
"symfony/monolog-bundle": "^3.1",
|
||||
"symfony/process": "5.4.*",
|
||||
"symfony/property-access": "5.4.*",
|
||||
"symfony/property-info": "5.4.*",
|
||||
"symfony/proxy-manager-bridge": "5.4.*",
|
||||
"symfony/rate-limiter": "5.4.*",
|
||||
"symfony/runtime": "5.4.*",
|
||||
"symfony/security-bundle": "5.4.*",
|
||||
"symfony/serializer": "5.4.*",
|
||||
"symfony/translation": "5.4.*",
|
||||
"symfony/twig-bundle": "5.4.*",
|
||||
"symfony/process": "6.3.*",
|
||||
"symfony/property-access": "6.3.*",
|
||||
"symfony/property-info": "6.3.*",
|
||||
"symfony/proxy-manager-bridge": "6.3.*",
|
||||
"symfony/rate-limiter": "6.3.*",
|
||||
"symfony/runtime": "6.3.*",
|
||||
"symfony/security-bundle": "6.3.*",
|
||||
"symfony/serializer": "6.3.*",
|
||||
"symfony/translation": "6.3.*",
|
||||
"symfony/twig-bundle": "6.3.*",
|
||||
"symfony/ux-translator": "2.x-dev",
|
||||
"symfony/ux-turbo": "^2.0",
|
||||
"symfony/validator": "5.4.*",
|
||||
"symfony/web-link": "5.4.*",
|
||||
"symfony/webpack-encore-bundle": "^1.1",
|
||||
"symfony/yaml": "5.4.*",
|
||||
"symfony/validator": "6.3.*",
|
||||
"symfony/web-link": "6.3.*",
|
||||
"symfony/webpack-encore-bundle": "^v2.0.1",
|
||||
"symfony/yaml": "6.3.*",
|
||||
"tecnickcom/tc-lib-barcode": "^1.15",
|
||||
"twig/cssinliner-extra": "^3.0",
|
||||
"twig/extra-bundle": "^3.0",
|
||||
@@ -77,30 +81,30 @@
|
||||
"twig/inky-extra": "^3.0",
|
||||
"twig/intl-extra": "^3.0",
|
||||
"twig/markdown-extra": "^3.0",
|
||||
"web-auth/webauthn-symfony-bundle": "^3.3",
|
||||
"webmozart/assert": "^1.4",
|
||||
"doctrine/data-fixtures": "^1.6.6"
|
||||
|
||||
"web-auth/webauthn-symfony-bundle": "^4.0.0",
|
||||
"webmozart/assert": "^1.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"dama/doctrine-test-bundle": "^7.0",
|
||||
"doctrine/doctrine-fixtures-bundle": "^3.2",
|
||||
"ekino/phpstan-banned-code": "^v1.0.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",
|
||||
"psalm/plugin-symfony": "^v5.0.1",
|
||||
"rector/rector": "^0.17.0",
|
||||
"roave/security-advisories": "dev-latest",
|
||||
"symfony/browser-kit": "^5.2",
|
||||
"symfony/css-selector": "^5.2",
|
||||
"symfony/debug-bundle": "^5.2",
|
||||
"symfony/browser-kit": "6.3.*",
|
||||
"symfony/css-selector": "6.3.*",
|
||||
"symfony/debug-bundle": "6.3.*",
|
||||
"symfony/maker-bundle": "^1.13",
|
||||
"symfony/phpunit-bridge": "5.4.*",
|
||||
"symfony/stopwatch": "^5.2",
|
||||
"symfony/web-profiler-bundle": "^5.2",
|
||||
"symfony/phpunit-bridge": "6.3.*",
|
||||
"symfony/stopwatch": "6.3.*",
|
||||
"symfony/web-profiler-bundle": "6.3.*",
|
||||
"symplify/easy-coding-standard": "^11.0",
|
||||
"vimeo/psalm": "^5.6.0",
|
||||
"doctrine/doctrine-fixtures-bundle": "^3.2"
|
||||
"vimeo/psalm": "^5.6.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-bcmath": "Used to improve price calculation performance",
|
||||
@@ -111,7 +115,7 @@
|
||||
"*": "dist"
|
||||
},
|
||||
"platform": {
|
||||
"php": "7.4.0"
|
||||
"php": "8.1.0"
|
||||
},
|
||||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
@@ -143,7 +147,7 @@
|
||||
"post-update-cmd": [
|
||||
"@auto-scripts"
|
||||
],
|
||||
"phpstan": "vendor/bin/phpstan analyse src --level 2 --memory-limit 1G"
|
||||
"phpstan": "vendor/bin/phpstan analyse src --level 5 --memory-limit 1G"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/symfony": "*"
|
||||
@@ -151,9 +155,7 @@
|
||||
"extra": {
|
||||
"symfony": {
|
||||
"allow-contrib": false,
|
||||
"require": "5.4.*"
|
||||
"require": "6.3.*"
|
||||
}
|
||||
},
|
||||
"repositories": [
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
5896
composer.lock
generated
5896
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,23 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Symfony\Component\Dotenv\Dotenv;
|
||||
|
||||
require dirname(__DIR__).'/vendor/autoload.php';
|
||||
|
||||
if (!class_exists(Dotenv::class)) {
|
||||
throw new LogicException('Please run "composer require symfony/dotenv" to load the ".env" files configuring the application.');
|
||||
}
|
||||
|
||||
// Load cached env vars if the .env.local.php file exists
|
||||
// Run "composer dump-env prod" to create it (requires symfony/flex >=1.2)
|
||||
if (is_array($env = @include dirname(__DIR__).'/.env.local.php') && (!isset($env['APP_ENV']) || ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? $env['APP_ENV']) === $env['APP_ENV'])) {
|
||||
(new Dotenv(false))->populate($env);
|
||||
} else {
|
||||
// load all the .env files
|
||||
(new Dotenv(false))->loadEnv(dirname(__DIR__).'/.env');
|
||||
}
|
||||
|
||||
$_SERVER += $_ENV;
|
||||
$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null) ?: 'dev';
|
||||
$_SERVER['APP_DEBUG'] = $_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? 'prod' !== $_SERVER['APP_ENV'];
|
||||
$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = (int) $_SERVER['APP_DEBUG'] || filter_var($_SERVER['APP_DEBUG'], FILTER_VALIDATE_BOOLEAN) ? '1' : '0';
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
return [
|
||||
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
|
||||
Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true],
|
||||
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
|
||||
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
|
||||
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
|
||||
@@ -27,5 +26,8 @@ return [
|
||||
Scheb\TwoFactorBundle\SchebTwoFactorBundle::class => ['all' => true],
|
||||
SpomkyLabs\CborBundle\SpomkyLabsCborBundle::class => ['all' => true],
|
||||
Webauthn\Bundle\WebauthnBundle::class => ['all' => true],
|
||||
Hslavich\OneloginSamlBundle\HslavichOneloginSamlBundle::class => ['all' => true],
|
||||
Nbgrp\OneloginSamlBundle\NbgrpOneloginSamlBundle::class => ['all' => true],
|
||||
Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true],
|
||||
Symfony\UX\Translator\UxTranslatorBundle::class => ['all' => true],
|
||||
Jbtronics\DompdfFontLoaderBundle\DompdfFontLoaderBundle::class => ['all' => true],
|
||||
];
|
||||
|
||||
@@ -21,12 +21,15 @@ doctrine:
|
||||
|
||||
orm:
|
||||
auto_generate_proxy_classes: true
|
||||
enable_lazy_ghost_objects: true
|
||||
report_fields_where_declared: true
|
||||
validate_xml_mapping: true
|
||||
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
|
||||
auto_mapping: true
|
||||
mappings:
|
||||
App:
|
||||
is_bundle: false
|
||||
type: annotation
|
||||
type: attribute
|
||||
dir: '%kernel.project_dir%/src/Entity'
|
||||
prefix: 'App\Entity'
|
||||
alias: App
|
||||
|
||||
11
config/packages/dompdf_font_loader.yaml
Normal file
11
config/packages/dompdf_font_loader.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
dompdf_font_loader:
|
||||
auto_install: true
|
||||
|
||||
fonts:
|
||||
unifont:
|
||||
normal: "%kernel.project_dir%/vendor/part-db/label-fonts/fonts/unifont.ttf"
|
||||
|
||||
# Enable autodiscovery of fonts, so that font installation is much easier
|
||||
autodiscovery:
|
||||
paths:
|
||||
- "%kernel.project_dir%/assets/fonts/dompdf"
|
||||
@@ -2,8 +2,9 @@
|
||||
framework:
|
||||
secret: '%env(APP_SECRET)%'
|
||||
csrf_protection: true
|
||||
handle_all_throwables: true
|
||||
|
||||
# Must be set to true, to enable the change of HTTP methhod via _method parameter, otherwise our delete routines does not work anymore
|
||||
# Must be set to true, to enable the change of HTTP method via _method parameter, otherwise our delete routines does not work anymore
|
||||
# TODO: Rework delete routines to work without _method parameter as it is not recommended anymore (see https://github.com/symfony/symfony/issues/45278)
|
||||
http_method_override: true
|
||||
|
||||
@@ -29,9 +30,6 @@ framework:
|
||||
php_errors:
|
||||
log: true
|
||||
|
||||
form:
|
||||
legacy_error_messages: false # Enable to use the new Form component validation messages
|
||||
|
||||
when@test:
|
||||
framework:
|
||||
test: true
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
# See https://github.com/SAML-Toolkits/php-saml for more information about the SAML settings
|
||||
|
||||
hslavich_onelogin_saml:
|
||||
# Basic settings
|
||||
idp:
|
||||
entityId: '%env(string:SAML_IDP_ENTITY_ID)%'
|
||||
singleSignOnService:
|
||||
url: '%env(string:SAML_IDP_SINGLE_SIGN_ON_SERVICE)%'
|
||||
binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'
|
||||
singleLogoutService:
|
||||
url: '%env(string:SAML_IDP_SINGLE_LOGOUT_SERVICE)%'
|
||||
binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'
|
||||
x509cert: '%env(string:SAML_IDP_X509_CERT)%'
|
||||
sp:
|
||||
entityId: '%env(string:SAML_SP_ENTITY_ID)%'
|
||||
assertionConsumerService:
|
||||
url: '%partdb.default_uri%saml/acs'
|
||||
binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'
|
||||
singleLogoutService:
|
||||
url: '%partdb.default_uri%logout'
|
||||
binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'
|
||||
x509cert: '%env(string:SAML_SP_X509_CERT)%'
|
||||
privateKey: '%env(string:SAMLP_SP_PRIVATE_KEY)%'
|
||||
|
||||
# Optional settings
|
||||
#baseurl: 'http://myapp.com'
|
||||
strict: true
|
||||
debug: false
|
||||
security:
|
||||
allowRepeatAttributeName: true
|
||||
# nameIdEncrypted: false
|
||||
authnRequestsSigned: true
|
||||
logoutRequestSigned: true
|
||||
logoutResponseSigned: true
|
||||
# wantMessagesSigned: false
|
||||
# wantAssertionsSigned: true
|
||||
# wantNameIdEncrypted: false
|
||||
# requestedAuthnContext: true
|
||||
# signMetadata: false
|
||||
# wantXMLValidation: true
|
||||
# relaxDestinationValidation: false
|
||||
# destinationStrictlyMatches: true
|
||||
# rejectUnsolicitedResponsesWithInResponseTo: false
|
||||
# signatureAlgorithm: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'
|
||||
# digestAlgorithm: 'http://www.w3.org/2001/04/xmlenc#sha256'
|
||||
#contactPerson:
|
||||
# technical:
|
||||
# givenName: 'Tech User'
|
||||
# emailAddress: 'techuser@example.com'
|
||||
# support:
|
||||
# givenName: 'Support User'
|
||||
# emailAddress: 'supportuser@example.com'
|
||||
# administrative:
|
||||
# givenName: 'Administrative User'
|
||||
# emailAddress: 'administrativeuser@example.com'
|
||||
#organization:
|
||||
# en:
|
||||
# name: 'Part-DB-name'
|
||||
# displayname: 'Displayname'
|
||||
# url: 'http://example.com'
|
||||
10
config/packages/http_discovery.yaml
Normal file
10
config/packages/http_discovery.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
services:
|
||||
Psr\Http\Message\RequestFactoryInterface: '@http_discovery.psr17_factory'
|
||||
Psr\Http\Message\ResponseFactoryInterface: '@http_discovery.psr17_factory'
|
||||
Psr\Http\Message\ServerRequestFactoryInterface: '@http_discovery.psr17_factory'
|
||||
Psr\Http\Message\StreamFactoryInterface: '@http_discovery.psr17_factory'
|
||||
Psr\Http\Message\UploadedFileFactoryInterface: '@http_discovery.psr17_factory'
|
||||
Psr\Http\Message\UriFactoryInterface: '@http_discovery.psr17_factory'
|
||||
|
||||
http_discovery.psr17_factory:
|
||||
class: Http\Discovery\Psr17Factory
|
||||
@@ -1,2 +0,0 @@
|
||||
framework:
|
||||
lock: '%env(LOCK_DSN)%'
|
||||
62
config/packages/nbgrp_onelogin_saml.yaml
Normal file
62
config/packages/nbgrp_onelogin_saml.yaml
Normal file
@@ -0,0 +1,62 @@
|
||||
# See https://github.com/SAML-Toolkits/php-saml for more information about the SAML settings
|
||||
|
||||
nbgrp_onelogin_saml:
|
||||
onelogin_settings:
|
||||
default:
|
||||
# Basic settings
|
||||
idp:
|
||||
entityId: '%env(string:SAML_IDP_ENTITY_ID)%'
|
||||
singleSignOnService:
|
||||
url: '%env(string:SAML_IDP_SINGLE_SIGN_ON_SERVICE)%'
|
||||
binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'
|
||||
singleLogoutService:
|
||||
url: '%env(string:SAML_IDP_SINGLE_LOGOUT_SERVICE)%'
|
||||
binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'
|
||||
x509cert: '%env(string:SAML_IDP_X509_CERT)%'
|
||||
sp:
|
||||
entityId: '%env(string:SAML_SP_ENTITY_ID)%'
|
||||
assertionConsumerService:
|
||||
url: '%partdb.default_uri%saml/acs'
|
||||
binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'
|
||||
singleLogoutService:
|
||||
url: '%partdb.default_uri%logout'
|
||||
binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'
|
||||
x509cert: '%env(string:SAML_SP_X509_CERT)%'
|
||||
privateKey: '%env(string:SAMLP_SP_PRIVATE_KEY)%'
|
||||
|
||||
# Optional settings
|
||||
#baseurl: 'http://myapp.com'
|
||||
strict: true
|
||||
debug: false
|
||||
security:
|
||||
allowRepeatAttributeName: true
|
||||
# nameIdEncrypted: false
|
||||
authnRequestsSigned: true
|
||||
logoutRequestSigned: true
|
||||
logoutResponseSigned: true
|
||||
# wantMessagesSigned: false
|
||||
# wantAssertionsSigned: true
|
||||
# wantNameIdEncrypted: false
|
||||
# requestedAuthnContext: true
|
||||
# signMetadata: false
|
||||
# wantXMLValidation: true
|
||||
# relaxDestinationValidation: false
|
||||
# destinationStrictlyMatches: true
|
||||
# rejectUnsolicitedResponsesWithInResponseTo: false
|
||||
# signatureAlgorithm: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'
|
||||
# digestAlgorithm: 'http://www.w3.org/2001/04/xmlenc#sha256'
|
||||
#contactPerson:
|
||||
# technical:
|
||||
# givenName: 'Tech User'
|
||||
# emailAddress: 'techuser@example.com'
|
||||
# support:
|
||||
# givenName: 'Support User'
|
||||
# emailAddress: 'supportuser@example.com'
|
||||
# administrative:
|
||||
# givenName: 'Administrative User'
|
||||
# emailAddress: 'administrativeuser@example.com'
|
||||
#organization:
|
||||
# en:
|
||||
# name: 'Part-DB-name'
|
||||
# displayname: 'Displayname'
|
||||
# url: 'http://example.com'
|
||||
@@ -1,10 +1,10 @@
|
||||
# See the configuration reference at https://symfony.com/bundles/SchebTwoFactorBundle/5.x/configuration.html
|
||||
# See the configuration reference at https://symfony.com/bundles/SchebTwoFactorBundle/6.x/configuration.html
|
||||
scheb_two_factor:
|
||||
|
||||
google:
|
||||
enabled: true # If Google Authenticator should be enabled, default false
|
||||
server_name: '%partdb.title%' # Server name used in QR code
|
||||
issuer: 'Part-DB' # Issuer name used in QR code
|
||||
server_name: '$$DOMAIN$$' # This field is replaced by the domain name of the server in DecoratedGoogleAuthenticator
|
||||
issuer: '%partdb.title%' # Issuer name used in QR code
|
||||
digits: 6 # Number of digits in authentication code
|
||||
window: 1 # How many codes before/after the current one would be accepted as valid
|
||||
template: security/2fa_form.html.twig
|
||||
@@ -23,6 +23,6 @@ scheb_two_factor:
|
||||
security_tokens:
|
||||
- Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken
|
||||
# If you're using guard-based authentication, you have to use this one:
|
||||
# - Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken
|
||||
# - Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken
|
||||
# If you're using authenticator-based security (introduced in Symfony 5.1), you have to use this one:
|
||||
# - Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken
|
||||
- Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
security:
|
||||
enable_authenticator_manager: true
|
||||
|
||||
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
|
||||
password_hashers:
|
||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
sensio_framework_extra:
|
||||
router:
|
||||
annotations: false
|
||||
@@ -1,4 +1,2 @@
|
||||
framework:
|
||||
test: true
|
||||
session:
|
||||
storage_id: session.storage.mock_file
|
||||
|
||||
4
config/packages/uid.yaml
Normal file
4
config/packages/uid.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
framework:
|
||||
uid:
|
||||
default_uuid_version: 7
|
||||
time_based_uuid_version: 7
|
||||
3
config/packages/ux_translator.yaml
Normal file
3
config/packages/ux_translator.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
ux_translator:
|
||||
# The directory where the JavaScript translations are dumped
|
||||
dump_directory: '%kernel.project_dir%/var/translations'
|
||||
@@ -4,7 +4,9 @@ when@dev:
|
||||
intercept_redirects: false
|
||||
|
||||
framework:
|
||||
profiler: { only_exceptions: false }
|
||||
profiler:
|
||||
only_exceptions: false
|
||||
collect_serializer_data: true
|
||||
|
||||
when@test:
|
||||
web_profiler:
|
||||
|
||||
@@ -19,7 +19,7 @@ parameters:
|
||||
######################################################################################################################
|
||||
# Users and Privacy
|
||||
######################################################################################################################
|
||||
partdb.gpdr_compliance: true # If this option is activated, IP addresses are anonymized to be GPDR compliant
|
||||
partdb.gdpr_compliance: true # If this option is activated, IP addresses are anonymized to be GDPR compliant
|
||||
partdb.users.use_gravatar: '%env(bool:USE_GRAVATAR)%' # Set to false, if no Gravatar images should be used for user profiles.
|
||||
partdb.users.email_pw_reset: '%env(bool:ALLOW_EMAIL_PW_RESET)%' # Config if users are able, to reset their password by email. By default this enabled, when a mail server is configured.
|
||||
|
||||
@@ -125,3 +125,9 @@ parameters:
|
||||
env(DEFAULT_URI): 'https://partdb.changeme.invalid/'
|
||||
|
||||
env(SAML_ROLE_MAPPING): '{}'
|
||||
|
||||
env(HISTORY_SAVE_CHANGED_DATA): 1
|
||||
env(HISTORY_SAVE_CHANGED_FIELDS): 1
|
||||
env(HISTORY_SAVE_REMOVED_DATA): 1
|
||||
env(HISTORY_SAVE_NEW_DATA): 1
|
||||
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
#index:
|
||||
# path: /
|
||||
# controller: App\Controller\DefaultController::index
|
||||
|
||||
# Redirect every url without an locale to the locale of the user/the global base locale
|
||||
|
||||
scan_qr:
|
||||
path: /scan/{type}/{id}
|
||||
controller: App\Controller\ScanController:scanQRCode
|
||||
controller: App\Controller\ScanController::scanQRCode
|
||||
|
||||
csp_report:
|
||||
path: /csp/report
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
controllers:
|
||||
resource: ../../src/Controller/
|
||||
type: annotation
|
||||
resource:
|
||||
path: ../../src/Controller/
|
||||
namespace: App\Controller
|
||||
type: attribute
|
||||
prefix: '{_locale}'
|
||||
|
||||
defaults:
|
||||
@@ -11,4 +13,4 @@ controllers:
|
||||
|
||||
kernel:
|
||||
resource: ../../src/Kernel.php
|
||||
type: annotation
|
||||
type: attribute
|
||||
@@ -1,4 +1,4 @@
|
||||
hslavich_saml_sp:
|
||||
resource: "@HslavichOneloginSamlBundle/Resources/config/routing.yml"
|
||||
nbgrp_saml:
|
||||
resource: "@NbgrpOneloginSamlBundle/Resources/config/routes.php"
|
||||
# Only load the SAML routes if SAML is enabled
|
||||
condition: "env('SAML_ENABLED') == '1' or env('SAML_ENABLED') == 'true'"
|
||||
@@ -14,11 +14,11 @@ services:
|
||||
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
|
||||
bind:
|
||||
bool $demo_mode: '%partdb.demo_mode%'
|
||||
bool $gpdr_compliance : '%partdb.gpdr_compliance%'
|
||||
bool $kernel_debug: '%kernel.debug%'
|
||||
bool $gdpr_compliance: '%partdb.gdpr_compliance%'
|
||||
bool $kernel_debug_enabled: '%kernel.debug%'
|
||||
string $kernel_cache_dir: '%kernel.cache_dir%'
|
||||
string $partdb_title: '%partdb.title%'
|
||||
string $default_currency: '%partdb.default_currency%'
|
||||
string $base_currency: '%partdb.default_currency%'
|
||||
|
||||
_instanceof:
|
||||
App\Services\LabelSystem\PlaceholderProviders\PlaceholderProviderInterface:
|
||||
@@ -78,6 +78,7 @@ services:
|
||||
$save_changed_fields: '%env(bool:HISTORY_SAVE_CHANGED_FIELDS)%'
|
||||
$save_changed_data: '%env(bool:HISTORY_SAVE_CHANGED_DATA)%'
|
||||
$save_removed_data: '%env(bool:HISTORY_SAVE_REMOVED_DATA)%'
|
||||
$save_new_data: '%env(bool:HISTORY_SAVE_NEW_DATA)%'
|
||||
tags:
|
||||
- { name: 'doctrine.event_subscriber' }
|
||||
|
||||
@@ -87,7 +88,7 @@ services:
|
||||
|
||||
App\Form\AttachmentFormType:
|
||||
arguments:
|
||||
$allow_attachments_downloads: '%partdb.attachments.allow_downloads%'
|
||||
$allow_attachments_download: '%partdb.attachments.allow_downloads%'
|
||||
$max_file_size: '%partdb.attachments.max_file_size%'
|
||||
|
||||
App\Services\Attachments\AttachmentSubmitHandler:
|
||||
@@ -96,12 +97,6 @@ services:
|
||||
$mimeTypes: '@mime_types'
|
||||
$max_upload_size: '%partdb.attachments.max_file_size%'
|
||||
|
||||
App\EventSubscriber\LogSystem\LogoutLoggerListener:
|
||||
tags:
|
||||
- name: 'kernel.event_listener'
|
||||
event: 'Symfony\Component\Security\Http\Event\LogoutEvent'
|
||||
dispatcher: security.event_dispatcher.main
|
||||
|
||||
App\Services\LogSystem\EventCommentNeededHelper:
|
||||
arguments:
|
||||
$enforce_change_comments_for: '%partdb.enforce_change_comments_for%'
|
||||
@@ -182,7 +177,7 @@ services:
|
||||
|
||||
App\EventSubscriber\UserSystem\SetUserTimezoneSubscriber:
|
||||
arguments:
|
||||
$timezone: '%partdb.timezone%'
|
||||
$default_timezone: '%partdb.timezone%'
|
||||
|
||||
App\Controller\SecurityController:
|
||||
arguments:
|
||||
@@ -226,6 +221,11 @@ services:
|
||||
tags:
|
||||
- { name: 'app.label_placeholder_provider', priority: 10}
|
||||
|
||||
App\Services\LabelSystem\DompdfFactory:
|
||||
arguments:
|
||||
$fontDirectory: '%kernel.project_dir%/var/dompdf/fonts/'
|
||||
$tmpDirectory: '%kernel.project_dir%/var/dompdf/tmp/'
|
||||
|
||||
####################################################################################################################
|
||||
# Trees
|
||||
####################################################################################################################
|
||||
@@ -264,7 +264,7 @@ services:
|
||||
tags:
|
||||
- { name: 'doctrine.fixtures.purger_factory', alias: 'reset_autoincrement_purger' }
|
||||
|
||||
# We are needing this service inside of a migration, where only the container is injected. So we need to define it as public, to access it from the container.
|
||||
# We are needing this service inside a migration, where only the container is injected. So we need to define it as public, to access it from the container.
|
||||
App\Services\UserSystem\PermissionPresetsHelper:
|
||||
public: true
|
||||
|
||||
@@ -288,3 +288,14 @@ services:
|
||||
autowire: true
|
||||
tags:
|
||||
- { name: monolog.processor }
|
||||
|
||||
when@test:
|
||||
services:
|
||||
# Decorate the doctrine fixtures load command to use our custom purger by default
|
||||
doctrine.fixtures_load_command.custom:
|
||||
decorates: doctrine.fixtures_load_command
|
||||
class: Doctrine\Bundle\FixturesBundle\Command\LoadDataFixturesDoctrineCommand
|
||||
arguments:
|
||||
- '@doctrine.fixtures.loader'
|
||||
- '@doctrine'
|
||||
- { default: '@App\Doctrine\Purger\ResetAutoIncrementPurgerFactory' }
|
||||
@@ -46,8 +46,9 @@ The following configuration options can only be changed by the server administra
|
||||
### History/Eventlog related settings
|
||||
The following options are used to configure, which (and how much) data is written to the system log:
|
||||
* `HISTORY_SAVE_CHANGED_FIELDS`: When this option is set to true, the name of the fields which are changed, are saved to the DB (so for example it is logged that a user has changed, that the user has changed the name and description of the field, but not the data/content of these changes)
|
||||
* `HISTORY_SAVE_CHANGED_DATA`: When this option is set to true, the changed data is saved to log (so it is logged, that a user has changed the name of a part and what the name was before). This can increase database size, when you have a lot of changes to enties.
|
||||
* `HISTORY_SAVE_CHANGED_DATA`: When this option is set to true, the changed data is saved to log (so it is logged, that a user has changed the name of a part and what the name was before). This can increase database size, when you have a lot of changes to entities.
|
||||
* `HISTORY_SAVE_REMOVED_DATA`: When this option is set to true, removed data is saved to log, meaning that you can easily undelete an entity, when it was removed accidentally.
|
||||
* `HISTORY_SAVE_NEW_DATA`: When this option is set to true, the new data (the data after a change) is saved to element changed log entries. This allows you to easily see the changes between two revisions of an entity. This can increase database size, when you have a lot of changes to entities.
|
||||
|
||||
If you wanna use want to revert changes or view older revisions of entities, then `HISTORY_SAVE_CHANGED_FIELDS`, `HISTORY_SAVE_CHANGED_DATA` and `HISTORY_SAVE_REMOVED_DATA` all have to be true.
|
||||
|
||||
@@ -97,7 +98,7 @@ The following options are available:
|
||||
|
||||
* `partdb.global_theme`: The default theme to use, when no user specific theme is set. Should be one of the themes from the `partdb.available_themes` config option.
|
||||
* `partdb.locale_menu`: The codes of the languages, which should be shown in the language chooser menu (the one with the user icon in the navbar). The first language in the list will be the default language.
|
||||
* `partdb.gpdr_compliance`: When set to true (default value), IP addresses which are saved in the database will be anonymized, by removing the last byte of the IP. This is required by the GDPR (General Data Protection Regulation) in the EU.
|
||||
* `partdb.gdpr_compliance`: When set to true (default value), IP addresses which are saved in the database will be anonymized, by removing the last byte of the IP. This is required by the GDPR (General Data Protection Regulation) in the EU.
|
||||
* `partdb.sidebar.items`: The panel contents which should be shown in the sidebar by default. You can also change the number of sidebar panels by changing the number of items in this list.
|
||||
* `partdb.sidebar.root_node_enable`: Show a root node in the sidebar trees, of which all nodes are children of
|
||||
* `partdb.sidebar.root_expanded`: Expand the root node in the sidebar trees by default
|
||||
|
||||
@@ -22,7 +22,7 @@ sudo apt install git curl zip ca-certificates software-properties-common apt-tra
|
||||
```
|
||||
|
||||
### Install PHP and apache2
|
||||
Part-DB is written in [PHP](https://php.net) and therefore needs an PHP interpreter to run. Part-DB needs PHP 7.3 or higher, however it is recommended to use the most recent version of PHP for performance reasons and future compatibility.
|
||||
Part-DB is written in [PHP](https://php.net) and therefore needs an PHP interpreter to run. Part-DB needs PHP 8.1 or higher, however it is recommended to use the most recent version of PHP for performance reasons and future compatibility.
|
||||
|
||||
As Debian 11 does not ship PHP 8.1 in it's default repositories, we have to add a repository for it. You can skip this step if your distribution is shipping a recent version of PHP or you want to use the built-in PHP version.
|
||||
```bash
|
||||
|
||||
@@ -92,4 +92,27 @@ The following variables are in injected into Twig and can be accessed using `{%
|
||||
| `{% raw %}{{ element }}{% endraw %}` | The target element, selected in label dialog |
|
||||
| `{% raw %}{{ user }}{% endraw %}` | The current logged in user. Null if you are not logged in |
|
||||
| `{% raw %}{{ install_title }}{% endraw %}` | The name of the current Part-DB instance (similar to [[INSTALL_NAME]] placeholder). |
|
||||
| `{% raw %}{{ page }}{% endraw %}` | The page number (the nth-element for which the label is generated |
|
||||
| `{% raw %}{{ page }}{% endraw %}` | The page number (the nth-element for which the label is generated |
|
||||
|
||||
|
||||
## Use custom fonts for PDF labels
|
||||
You can use your own fonts for label generation. To do this, put the TTF files of the fonts you want to use into the `assets/fonts/dompdf` folder.
|
||||
The filename will be used as name for the font family and you can use a `_bold` (or `_b`), `_italic` (or `_i`) or `_bold_italic` (or `_bi`) suffix to define
|
||||
different styles of the font. So for example, if you copy the file `myfont.ttf` and `myfont_bold.ttf` into the `assets/fonts/dompdf` folder, you can use the font family `myfont` with regular and bold style.
|
||||
Afterwards regenerate cache with `php bin/console cache:clear`, so the new fonts will be available for label generation.
|
||||
|
||||
The fonts will not be availble from the UI directly, you have to use it in the HTML directly either by defining a `style="font-family: 'myfont';"` attribute on the HTML element or by using a CSS class.
|
||||
You can define the font globally for the label, by adding following statement to the "Additional styles (CSS)" option in the label generator settings:
|
||||
```css
|
||||
* {
|
||||
font-family: 'myfont';
|
||||
}
|
||||
```
|
||||
|
||||
## Non-latin characters in PDF labels
|
||||
The default used font (DejaVu) does not support all characters. Especially characters from non-latin languages like Chinese, Japanese, Korean, Arabic, Hebrew, Cyrillic, etc. are not supported.
|
||||
For this we use [Unifont](http://unifoundry.com/unifont.html) as fallback font. This font supports all (or most) unicode characters, but is not as beautiful as DejaVu.
|
||||
|
||||
If you want to use a different (more beautiful) font, you can use the [custom fonts](#use-custom-fonts-for-pdf-labels) feature.
|
||||
There is the [Noto](https://www.google.com/get/noto/) font family from Google, which supports a lot of languages and is available in different styles (regular, bold, italic, bold-italic).
|
||||
For example you can use [Noto CJK](https://github.com/notofonts/noto-cjk) for more beautful Chinese, Japanese and Korean characters.
|
||||
@@ -8,9 +8,6 @@ use App\Migration\AbstractMultiPlatformMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20230417211732 extends AbstractMultiPlatformMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
|
||||
65
migrations/Version20230528000149.php
Normal file
65
migrations/Version20230528000149.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use App\Migration\AbstractMultiPlatformMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20230528000149 extends AbstractMultiPlatformMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add other_ui column to webauthn_keys table, needed for future compatibility with more complex webauthn authenticators';
|
||||
}
|
||||
|
||||
public function mySQLUp(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE webauthn_keys ADD other_ui LONGTEXT DEFAULT NULL COMMENT \'(DC2Type:array)\'');
|
||||
}
|
||||
|
||||
public function mySQLDown(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE webauthn_keys DROP other_ui');
|
||||
}
|
||||
|
||||
public function sqLiteUp(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE TEMPORARY TABLE __temp__webauthn_keys AS SELECT id, user_id, public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, name, last_modified, datetime_added FROM webauthn_keys');
|
||||
$this->addSql('DROP TABLE webauthn_keys');
|
||||
$this->addSql('CREATE TABLE webauthn_keys (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user_id INTEGER DEFAULT NULL, public_key_credential_id CLOB NOT NULL --(DC2Type:base64)
|
||||
, type VARCHAR(255) NOT NULL, transports CLOB NOT NULL --(DC2Type:array)
|
||||
, attestation_type VARCHAR(255) NOT NULL, trust_path CLOB NOT NULL --(DC2Type:trust_path)
|
||||
, aaguid CLOB NOT NULL --(DC2Type:aaguid)
|
||||
, credential_public_key CLOB NOT NULL --(DC2Type:base64)
|
||||
, user_handle VARCHAR(255) NOT NULL, counter INTEGER NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, other_ui CLOB DEFAULT NULL --(DC2Type:array)
|
||||
, CONSTRAINT FK_799FD143A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE)');
|
||||
$this->addSql('INSERT INTO webauthn_keys (id, user_id, public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, name, last_modified, datetime_added) SELECT id, user_id, public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, name, last_modified, datetime_added FROM __temp__webauthn_keys');
|
||||
$this->addSql('DROP TABLE __temp__webauthn_keys');
|
||||
$this->addSql('CREATE INDEX IDX_799FD143A76ED395 ON webauthn_keys (user_id)');
|
||||
}
|
||||
|
||||
public function sqLiteDown(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE TEMPORARY TABLE __temp__webauthn_keys AS SELECT id, user_id, public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, name, last_modified, datetime_added FROM webauthn_keys');
|
||||
$this->addSql('DROP TABLE webauthn_keys');
|
||||
$this->addSql('CREATE TABLE webauthn_keys (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user_id INTEGER DEFAULT NULL, public_key_credential_id CLOB NOT NULL --
|
||||
(DC2Type:base64)
|
||||
, type VARCHAR(255) NOT NULL, transports CLOB NOT NULL --
|
||||
(DC2Type:array)
|
||||
, attestation_type VARCHAR(255) NOT NULL, trust_path CLOB NOT NULL --
|
||||
(DC2Type:trust_path)
|
||||
, aaguid CLOB NOT NULL --
|
||||
(DC2Type:aaguid)
|
||||
, credential_public_key CLOB NOT NULL --
|
||||
(DC2Type:base64)
|
||||
, user_handle VARCHAR(255) NOT NULL, counter INTEGER NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_799FD143A76ED395 FOREIGN KEY (user_id) REFERENCES "users" (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
|
||||
$this->addSql('INSERT INTO webauthn_keys (id, user_id, public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, name, last_modified, datetime_added) SELECT id, user_id, public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, name, last_modified, datetime_added FROM __temp__webauthn_keys');
|
||||
$this->addSql('DROP TABLE __temp__webauthn_keys');
|
||||
$this->addSql('CREATE INDEX IDX_799FD143A76ED395 ON webauthn_keys (user_id)');
|
||||
}
|
||||
}
|
||||
76
package.json
76
package.json
@@ -7,10 +7,12 @@
|
||||
"@hotwired/turbo": "^7.0.1",
|
||||
"@popperjs/core": "^2.10.2",
|
||||
"@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",
|
||||
"bootstrap": "^5.1.3",
|
||||
"core-js": "^3.23.0",
|
||||
"intl-messageformat": "^10.2.5",
|
||||
"jquery": "^3.5.1",
|
||||
"popper.js": "^1.14.7",
|
||||
"regenerator-runtime": "^0.13.9",
|
||||
@@ -28,45 +30,50 @@
|
||||
"build": "encore production --progress"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ckeditor/ckeditor5-alignment": "^37.1.0",
|
||||
"@ckeditor/ckeditor5-autoformat": "^37.1.0",
|
||||
"@ckeditor/ckeditor5-basic-styles": "^37.1.0",
|
||||
"@ckeditor/ckeditor5-block-quote": "^37.1.0",
|
||||
"@ckeditor/ckeditor5-code-block": "^37.1.0",
|
||||
"@ckeditor/ckeditor5-dev-utils": "^37.0.0",
|
||||
"@ckeditor/ckeditor5-alignment": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-autoformat": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-basic-styles": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-block-quote": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-code-block": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-dev-utils": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-dev-webpack-plugin": "^31.1.13",
|
||||
"@ckeditor/ckeditor5-editor-classic": "^37.1.0",
|
||||
"@ckeditor/ckeditor5-essentials": "^37.1.0",
|
||||
"@ckeditor/ckeditor5-find-and-replace": "^37.1.0",
|
||||
"@ckeditor/ckeditor5-font": "^37.1.0",
|
||||
"@ckeditor/ckeditor5-heading": "^37.1.0",
|
||||
"@ckeditor/ckeditor5-highlight": "^37.1.0",
|
||||
"@ckeditor/ckeditor5-horizontal-line": "^37.1.0",
|
||||
"@ckeditor/ckeditor5-html-embed": "^37.1.0",
|
||||
"@ckeditor/ckeditor5-html-support": "^37.1.0",
|
||||
"@ckeditor/ckeditor5-image": "^37.1.0",
|
||||
"@ckeditor/ckeditor5-indent": "^37.1.0",
|
||||
"@ckeditor/ckeditor5-link": "^37.1.0",
|
||||
"@ckeditor/ckeditor5-list": "^37.1.0",
|
||||
"@ckeditor/ckeditor5-markdown-gfm": "^37.1.0",
|
||||
"@ckeditor/ckeditor5-media-embed": "^37.1.0",
|
||||
"@ckeditor/ckeditor5-paragraph": "^37.1.0",
|
||||
"@ckeditor/ckeditor5-paste-from-office": "^37.1.0",
|
||||
"@ckeditor/ckeditor5-remove-format": "^37.1.0",
|
||||
"@ckeditor/ckeditor5-source-editing": "^37.1.0",
|
||||
"@ckeditor/ckeditor5-special-characters": "^37.1.0",
|
||||
"@ckeditor/ckeditor5-table": "^37.1.0",
|
||||
"@ckeditor/ckeditor5-theme-lark": "^37.1.0",
|
||||
"@ckeditor/ckeditor5-upload": "^37.1.0",
|
||||
"@ckeditor/ckeditor5-watchdog": "^37.1.0",
|
||||
"@ckeditor/ckeditor5-word-count": "^37.1.0",
|
||||
"@ckeditor/ckeditor5-editor-classic": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-essentials": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-find-and-replace": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-font": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-heading": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-highlight": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-horizontal-line": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-html-embed": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-html-support": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-image": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-indent": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-link": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-list": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-markdown-gfm": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-media-embed": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-paragraph": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-paste-from-office": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-remove-format": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-source-editing": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-special-characters": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-table": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-theme-lark": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-upload": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-watchdog": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-word-count": "^38.0.1",
|
||||
"@jbtronics/bs-treeview": "^1.0.1",
|
||||
"@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",
|
||||
"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",
|
||||
"darkmode-js": "^1.5.0",
|
||||
"datatables.net-bs5": "^1.10.20",
|
||||
"datatables.net-buttons-bs5": "^2.2.2",
|
||||
"datatables.net-colreorder-bs5": "^1.5.1",
|
||||
@@ -77,9 +84,12 @@
|
||||
"emoji.json": "^14.0.0",
|
||||
"exports-loader": "^3.0.0",
|
||||
"html5-qrcode": "^2.2.1",
|
||||
"json-formatter-js": "^2.3.4",
|
||||
"jszip": "^3.2.0",
|
||||
"katex": "^0.16.0",
|
||||
"marked": "^4.3.0",
|
||||
"marked": "^5.1.0",
|
||||
"marked-gfm-heading-id": "^3.0.4",
|
||||
"marked-mangle": "^1.0.1",
|
||||
"pdfmake": "^0.2.2",
|
||||
"stimulus-use": "^0.52.0",
|
||||
"tom-select": "^2.1.0",
|
||||
|
||||
52
phpstan.neon
52
phpstan.neon
@@ -1,11 +1,55 @@
|
||||
parameters:
|
||||
|
||||
level: 5
|
||||
|
||||
paths:
|
||||
- src
|
||||
# - tests
|
||||
|
||||
excludePaths:
|
||||
- src/DataTables/Adapter/*
|
||||
- src/Configuration/*
|
||||
- src/Doctrine/Purger/*
|
||||
|
||||
|
||||
|
||||
inferPrivatePropertyTypeFromConstructor: true
|
||||
treatPhpDocTypesAsCertain: false
|
||||
|
||||
symfony:
|
||||
container_xml_path: '%rootDir%/../../../var/cache/dev/App_KernelDevDebugContainer.xml'
|
||||
|
||||
excludes_analyse:
|
||||
- src/DataTables/Adapter/*
|
||||
- src/Configuration/*
|
||||
- src/Doctrine/Purger/*
|
||||
checkUninitializedProperties: true
|
||||
|
||||
checkFunctionNameCase: true
|
||||
|
||||
checkAlwaysTrueInstanceof: false
|
||||
checkAlwaysTrueCheckTypeFunctionCall: false
|
||||
checkAlwaysTrueStrictComparison: false
|
||||
reportAlwaysTrueInLastCondition: false
|
||||
|
||||
reportMaybesInPropertyPhpDocTypes: false
|
||||
reportMaybesInMethodSignatures: false
|
||||
|
||||
strictRules:
|
||||
disallowedLooseComparison: false
|
||||
booleansInConditions: false
|
||||
uselessCast: false
|
||||
requireParentConstructorCall: true
|
||||
disallowedConstructs: false
|
||||
overwriteVariablesWithLoop: false
|
||||
closureUsesThis: false
|
||||
matchingInheritedMethodNames: true
|
||||
numericOperandsInArithmeticOperators: true
|
||||
strictCalls: true
|
||||
switchConditionsMatchingType: false
|
||||
noVariableVariables: false
|
||||
|
||||
ignoreErrors:
|
||||
# Ignore errors caused by complex mapping with AbstractStructuralDBElement
|
||||
- '#AbstractStructuralDBElement does not have a field named \$parent#'
|
||||
- '#AbstractStructuralDBElement does not have a field named \$name#'
|
||||
|
||||
# Ignore errors related to the use of the ParametersTrait in Part entity
|
||||
- '#expects .*PartParameter, .*AbstractParameter given.#'
|
||||
- '#Part::getParameters\(\) should return .*AbstractParameter#'
|
||||
@@ -1,20 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd" backupGlobals="false" colors="true" bootstrap="tests/bootstrap.php">
|
||||
<coverage processUncoveredFiles="true">
|
||||
<include>
|
||||
<directory suffix=".php">src</directory>
|
||||
</include>
|
||||
</coverage>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.5/phpunit.xsd"
|
||||
backupGlobals="false"
|
||||
colors="true"
|
||||
bootstrap="tests/bootstrap.php"
|
||||
convertDeprecationsToExceptions="false"
|
||||
>
|
||||
<php>
|
||||
<ini name="error_reporting" value="-1"/>
|
||||
<server name="APP_ENV" value="test" force="true"/>
|
||||
<server name="SHELL_VERBOSITY" value="-1"/>
|
||||
<server name="SYMFONY_PHPUNIT_REMOVE" value=""/>
|
||||
<server name="SYMFONY_PHPUNIT_VERSION" value="9"/>
|
||||
<server name="SYMFONY_PHPUNIT_VERSION" value="9.5"/>
|
||||
<ini name="memory_limit" value="512M"/>
|
||||
<ini name="display_errors" value="1"/>
|
||||
</php>
|
||||
<coverage processUncoveredFiles="true">
|
||||
<include>
|
||||
<directory suffix=".php">src</directory>
|
||||
</include>
|
||||
</coverage>
|
||||
|
||||
<testsuites>
|
||||
<testsuite name="Project Test Suite">
|
||||
<directory>tests</directory>
|
||||
|
||||
63
rector.php
Normal file
63
rector.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Rector\CodingStyle\Rector\FuncCall\CountArrayToEmptyArrayComparisonRector;
|
||||
use Rector\Config\RectorConfig;
|
||||
use Rector\Doctrine\Set\DoctrineSetList;
|
||||
use Rector\PHPUnit\Rector\ClassMethod\AddDoesNotPerformAssertionToNonAssertingTestRector;
|
||||
use Rector\PHPUnit\Set\PHPUnitLevelSetList;
|
||||
use Rector\PHPUnit\Set\PHPUnitSetList;
|
||||
use Rector\Set\ValueObject\LevelSetList;
|
||||
use Rector\Set\ValueObject\SetList;
|
||||
use Rector\Symfony\Set\SymfonyLevelSetList;
|
||||
use Rector\Symfony\Set\SymfonySetList;
|
||||
use Rector\TypeDeclaration\Rector\StmtsAwareInterface\DeclareStrictTypesRector;
|
||||
|
||||
return static function (RectorConfig $rectorConfig): void {
|
||||
$rectorConfig->symfonyContainerXml(__DIR__ . '/var/cache/dev/App_KernelDevDebugContainer.xml');
|
||||
$rectorConfig->symfonyContainerPhp(__DIR__ . '/tests/symfony-container.php');
|
||||
|
||||
//Import class names instead of using fully qualified class names
|
||||
$rectorConfig->importNames();
|
||||
//But keep the fully qualified class names for classes in the global namespace
|
||||
$rectorConfig->importShortClasses(false);
|
||||
|
||||
$rectorConfig->paths([
|
||||
__DIR__ . '/config',
|
||||
__DIR__ . '/public',
|
||||
__DIR__ . '/src',
|
||||
__DIR__ . '/tests',
|
||||
]);
|
||||
|
||||
// register a single rule
|
||||
//$rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class);
|
||||
|
||||
$rectorConfig->rules([
|
||||
DeclareStrictTypesRector::class,
|
||||
]);
|
||||
|
||||
// define sets of rules
|
||||
$rectorConfig->sets([
|
||||
//PHP rules
|
||||
SetList::CODE_QUALITY,
|
||||
LevelSetList::UP_TO_PHP_81,
|
||||
|
||||
//Symfony rules
|
||||
SymfonyLevelSetList::UP_TO_SYMFONY_62,
|
||||
SymfonySetList::SYMFONY_CODE_QUALITY,
|
||||
|
||||
//Doctrine rules
|
||||
DoctrineSetList::ANNOTATIONS_TO_ATTRIBUTES,
|
||||
DoctrineSetList::DOCTRINE_CODE_QUALITY,
|
||||
|
||||
//PHPUnit rules
|
||||
PHPUnitLevelSetList::UP_TO_PHPUNIT_90,
|
||||
PHPUnitSetList::PHPUNIT_CODE_QUALITY,
|
||||
]);
|
||||
|
||||
$rectorConfig->skip([
|
||||
AddDoesNotPerformAssertionToNonAssertingTestRector::class,
|
||||
CountArrayToEmptyArrayComparisonRector::class,
|
||||
]);
|
||||
};
|
||||
@@ -22,6 +22,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Command\Attachments;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use App\Services\Attachments\AttachmentManager;
|
||||
use App\Services\Attachments\AttachmentPathResolver;
|
||||
use App\Services\Attachments\AttachmentReverseSearch;
|
||||
@@ -40,30 +41,21 @@ use function count;
|
||||
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
|
||||
#[AsCommand('partdb:attachments:clean-unused|app:clean-attachments', 'Lists (and deletes if wanted) attachments files that are not used anymore (abandoned files).')]
|
||||
class CleanAttachmentsCommand extends Command
|
||||
{
|
||||
protected static $defaultName = 'partdb:attachments:clean-unused|app:clean-attachments';
|
||||
|
||||
protected AttachmentManager $attachment_helper;
|
||||
protected AttachmentReverseSearch $reverseSearch;
|
||||
protected MimeTypes $mimeTypeGuesser;
|
||||
protected AttachmentPathResolver $pathResolver;
|
||||
|
||||
public function __construct(AttachmentManager $attachmentHelper, AttachmentReverseSearch $reverseSearch, AttachmentPathResolver $pathResolver)
|
||||
public function __construct(protected AttachmentManager $attachment_helper, protected AttachmentReverseSearch $reverseSearch, protected AttachmentPathResolver $pathResolver)
|
||||
{
|
||||
$this->attachment_helper = $attachmentHelper;
|
||||
$this->pathResolver = $pathResolver;
|
||||
$this->reverseSearch = $reverseSearch;
|
||||
$this->mimeTypeGuesser = new MimeTypes();
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setDescription('Lists (and deletes if wanted) attachments files that are not used anymore (abandoned files).')
|
||||
->setHelp('This command allows to find all files in the media folder which are not associated with an attachment anymore.'.
|
||||
' These files are not needed and can eventually deleted.');
|
||||
$this->setHelp('This command allows to find all files in the media folder which are not associated with an attachment anymore.'.
|
||||
' These files are not needed and can eventually deleted.');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
@@ -91,7 +83,7 @@ class CleanAttachmentsCommand extends Command
|
||||
|
||||
foreach ($finder as $file) {
|
||||
//If not attachment object uses this file, print it
|
||||
if (0 === count($this->reverseSearch->findAttachmentsByFile($file))) {
|
||||
if ([] === $this->reverseSearch->findAttachmentsByFile($file)) {
|
||||
$file_list[] = $file;
|
||||
$table->addRow([
|
||||
$fs->makePathRelative($file->getPathname(), $mediaPath),
|
||||
@@ -101,14 +93,14 @@ class CleanAttachmentsCommand extends Command
|
||||
}
|
||||
}
|
||||
|
||||
if (count($file_list) > 0) {
|
||||
if ($file_list !== []) {
|
||||
$table->render();
|
||||
|
||||
$continue = $io->confirm(sprintf('Found %d abandoned files. Do you want to delete them? This can not be undone!', count($file_list)), false);
|
||||
|
||||
if (!$continue) {
|
||||
//We are finished here, when no files should be deleted
|
||||
return 0;
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
//Delete the files
|
||||
@@ -121,7 +113,7 @@ class CleanAttachmentsCommand extends Command
|
||||
$io->success('No abandoned files found.');
|
||||
}
|
||||
|
||||
return 0;
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Doctrine\DBAL\Platforms\SqlitePlatform;
|
||||
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PhpZip\Constants\ZipCompressionMethod;
|
||||
@@ -16,19 +20,11 @@ use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand('partdb:backup', 'Backup the files and the database of Part-DB')]
|
||||
class BackupCommand extends Command
|
||||
{
|
||||
protected static $defaultName = 'partdb:backup';
|
||||
protected static $defaultDescription = 'Backup the files and the database of Part-DB';
|
||||
|
||||
private string $project_dir;
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
public function __construct(string $project_dir, EntityManagerInterface $entityManager)
|
||||
public function __construct(private readonly string $project_dir, private readonly EntityManagerInterface $entityManager)
|
||||
{
|
||||
$this->project_dir = $project_dir;
|
||||
$this->entityManager = $entityManager;
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
@@ -71,13 +67,10 @@ class BackupCommand extends Command
|
||||
$io->info('Backup Part-DB to '.$output_filepath);
|
||||
|
||||
//Check if the file already exists
|
||||
if (file_exists($output_filepath)) {
|
||||
//Then ask the user, if he wants to overwrite the file
|
||||
if (!$io->confirm('The file '.realpath($output_filepath).' already exists. Do you want to overwrite it?', false)) {
|
||||
$io->error('Backup aborted!');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
//Then ask the user, if he wants to overwrite the file
|
||||
if (file_exists($output_filepath) && !$io->confirm('The file '.realpath($output_filepath).' already exists. Do you want to overwrite it?', false)) {
|
||||
$io->error('Backup aborted!');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$io->note('Starting backup...');
|
||||
@@ -115,8 +108,6 @@ class BackupCommand extends Command
|
||||
/**
|
||||
* Constructs the MySQL PDO DSN.
|
||||
* Taken from https://github.com/doctrine/dbal/blob/3.5.x/src/Driver/PDO/MySQL/Driver.php
|
||||
*
|
||||
* @param array $params
|
||||
*/
|
||||
private function configureDumper(array $params, DbDumper $dumper): void
|
||||
{
|
||||
@@ -166,7 +157,7 @@ class BackupCommand extends Command
|
||||
$io->error('Could not dump database: '.$e->getMessage());
|
||||
$io->error('This can maybe be fixed by installing the mysqldump binary and adding it to the PATH variable!');
|
||||
}
|
||||
} elseif ($connection->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\SqlitePlatform) {
|
||||
} elseif ($connection->getDatabasePlatform() instanceof SqlitePlatform) {
|
||||
$io->note('SQLite database detected. Copy DB file to ZIP...');
|
||||
$params = $connection->getParams();
|
||||
$zip->addFile($params['path'], 'var/app.db');
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
@@ -17,9 +20,9 @@
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
@@ -27,23 +30,17 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ContainerBagInterface;
|
||||
|
||||
#[AsCommand('partdb:check-requirements', 'Checks if the requirements Part-DB needs or recommends are fulfilled.')]
|
||||
class CheckRequirementsCommand extends Command
|
||||
{
|
||||
protected static $defaultName = 'partdb:check-requirements';
|
||||
|
||||
protected ContainerBagInterface $params;
|
||||
|
||||
public function __construct(ContainerBagInterface $params)
|
||||
public function __construct(protected ContainerBagInterface $params)
|
||||
{
|
||||
$this->params = $params;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setDescription('Checks if the requirements Part-DB needs or recommends are fulfilled.')
|
||||
->addOption('only_issues', 'i', InputOption::VALUE_NONE, 'Only show issues, not success messages.')
|
||||
$this->addOption('only_issues', 'i', InputOption::VALUE_NONE, 'Only show issues, not success messages.')
|
||||
;
|
||||
}
|
||||
|
||||
@@ -66,105 +63,124 @@ class CheckRequirementsCommand extends Command
|
||||
|
||||
}
|
||||
|
||||
protected function checkPHP(SymfonyStyle $io, $only_issues = false): void
|
||||
protected function checkPHP(SymfonyStyle $io, bool $only_issues = false): void
|
||||
{
|
||||
//Check PHP versions
|
||||
$io->isVerbose() && $io->comment('Checking PHP version...');
|
||||
if (PHP_VERSION_ID < 80100) {
|
||||
if ($io->isVerbose()) {
|
||||
$io->comment('Checking PHP version...');
|
||||
}
|
||||
//We recommend PHP 8.2, but 8.1 is the minimum
|
||||
if (PHP_VERSION_ID < 80200) {
|
||||
$io->warning('You are using PHP '. PHP_VERSION .'. This will work, but a newer version is recommended.');
|
||||
} else {
|
||||
!$only_issues && $io->success('PHP version is sufficient.');
|
||||
} elseif (!$only_issues) {
|
||||
$io->success('PHP version is sufficient.');
|
||||
}
|
||||
|
||||
//Check if opcache is enabled
|
||||
$io->isVerbose() && $io->comment('Checking Opcache...');
|
||||
if ($io->isVerbose()) {
|
||||
$io->comment('Checking Opcache...');
|
||||
}
|
||||
$opcache_enabled = ini_get('opcache.enable') === '1';
|
||||
if (!$opcache_enabled) {
|
||||
$io->warning('Opcache is not enabled. This will work, but performance will be better with opcache enabled. Set opcache.enable=1 in your php.ini to enable it');
|
||||
} else {
|
||||
!$only_issues && $io->success('Opcache is enabled.');
|
||||
} elseif (!$only_issues) {
|
||||
$io->success('Opcache is enabled.');
|
||||
}
|
||||
|
||||
//Check if opcache is configured correctly
|
||||
$io->isVerbose() && $io->comment('Checking Opcache configuration...');
|
||||
if ($io->isVerbose()) {
|
||||
$io->comment('Checking Opcache configuration...');
|
||||
}
|
||||
if ($opcache_enabled && (ini_get('opcache.memory_consumption') < 256 || ini_get('opcache.max_accelerated_files') < 20000)) {
|
||||
$io->warning('Opcache configuration can be improved. See https://symfony.com/doc/current/performance.html for more info.');
|
||||
} else {
|
||||
!$only_issues && $io->success('Opcache configuration is already performance optimized.');
|
||||
} elseif (!$only_issues) {
|
||||
$io->success('Opcache configuration is already performance optimized.');
|
||||
}
|
||||
}
|
||||
|
||||
protected function checkPartDBConfig(SymfonyStyle $io, $only_issues = false): void
|
||||
protected function checkPartDBConfig(SymfonyStyle $io, bool $only_issues = false): void
|
||||
{
|
||||
//Check if APP_ENV is set to prod
|
||||
$io->isVerbose() && $io->comment('Checking debug mode...');
|
||||
if($this->params->get('kernel.debug')) {
|
||||
if ($io->isVerbose()) {
|
||||
$io->comment('Checking debug mode...');
|
||||
}
|
||||
if ($this->params->get('kernel.debug')) {
|
||||
$io->warning('You have activated debug mode, this is will leak informations in a production environment.');
|
||||
} else {
|
||||
!$only_issues && $io->success('Debug mode disabled.');
|
||||
} elseif (!$only_issues) {
|
||||
$io->success('Debug mode disabled.');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected function checkPHPExtensions(SymfonyStyle $io, $only_issues = false): void
|
||||
protected function checkPHPExtensions(SymfonyStyle $io, bool $only_issues = false): void
|
||||
{
|
||||
//Get all installed PHP extensions
|
||||
$extensions = get_loaded_extensions();
|
||||
$io->isVerbose() && $io->comment('Your PHP installation has '. count($extensions) .' extensions installed: '. implode(', ', $extensions));
|
||||
if ($io->isVerbose()) {
|
||||
$io->comment('Your PHP installation has '. count($extensions) .' extensions installed: '. implode(', ', $extensions));
|
||||
}
|
||||
|
||||
$db_drivers_count = 0;
|
||||
if(!in_array('pdo_mysql', $extensions)) {
|
||||
if(!in_array('pdo_mysql', $extensions, true)) {
|
||||
$io->error('pdo_mysql is not installed. You will not be able to use MySQL databases.');
|
||||
} else {
|
||||
!$only_issues && $io->success('PHP extension pdo_mysql is installed.');
|
||||
if (!$only_issues) {
|
||||
$io->success('PHP extension pdo_mysql is installed.');
|
||||
}
|
||||
$db_drivers_count++;
|
||||
}
|
||||
|
||||
if(!in_array('pdo_sqlite', $extensions)) {
|
||||
if(!in_array('pdo_sqlite', $extensions, true)) {
|
||||
$io->error('pdo_sqlite is not installed. You will not be able to use SQLite. databases');
|
||||
} else {
|
||||
!$only_issues && $io->success('PHP extension pdo_sqlite is installed.');
|
||||
if (!$only_issues) {
|
||||
$io->success('PHP extension pdo_sqlite is installed.');
|
||||
}
|
||||
$db_drivers_count++;
|
||||
}
|
||||
|
||||
$io->isVerbose() && $io->comment('You have '. $db_drivers_count .' database drivers installed.');
|
||||
if ($io->isVerbose()) {
|
||||
$io->comment('You have '. $db_drivers_count .' database drivers installed.');
|
||||
}
|
||||
if ($db_drivers_count === 0) {
|
||||
$io->error('You have no database drivers installed. You have to install at least one database driver!');
|
||||
}
|
||||
|
||||
if(!in_array('curl', $extensions)) {
|
||||
if (!in_array('curl', $extensions, true)) {
|
||||
$io->warning('curl extension is not installed. Install curl extension for better performance');
|
||||
} else {
|
||||
!$only_issues && $io->success('PHP extension curl is installed.');
|
||||
} elseif (!$only_issues) {
|
||||
$io->success('PHP extension curl is installed.');
|
||||
}
|
||||
|
||||
$gd_installed = in_array('gd', $extensions);
|
||||
if(!$gd_installed) {
|
||||
$gd_installed = in_array('gd', $extensions, true);
|
||||
if (!$gd_installed) {
|
||||
$io->error('GD is not installed. GD is required for image processing.');
|
||||
} else {
|
||||
!$only_issues && $io->success('PHP extension GD is installed.');
|
||||
} elseif (!$only_issues) {
|
||||
$io->success('PHP extension GD is installed.');
|
||||
}
|
||||
|
||||
//Check if GD has jpeg support
|
||||
$io->isVerbose() && $io->comment('Checking if GD has jpeg support...');
|
||||
if ($io->isVerbose()) {
|
||||
$io->comment('Checking if GD has jpeg support...');
|
||||
}
|
||||
if ($gd_installed) {
|
||||
$gd_info = gd_info();
|
||||
if($gd_info['JPEG Support'] === false) {
|
||||
if ($gd_info['JPEG Support'] === false) {
|
||||
$io->warning('Your GD does not have jpeg support. You will not be able to generate thumbnails of jpeg images.');
|
||||
} else {
|
||||
!$only_issues && $io->success('GD has jpeg support.');
|
||||
} elseif (!$only_issues) {
|
||||
$io->success('GD has jpeg support.');
|
||||
}
|
||||
|
||||
if($gd_info['PNG Support'] === false) {
|
||||
if ($gd_info['PNG Support'] === false) {
|
||||
$io->warning('Your GD does not have png support. You will not be able to generate thumbnails of png images.');
|
||||
} else {
|
||||
!$only_issues && $io->success('GD has png support.');
|
||||
} elseif (!$only_issues) {
|
||||
$io->success('GD has png support.');
|
||||
}
|
||||
|
||||
if($gd_info['WebP Support'] === false) {
|
||||
if ($gd_info['WebP Support'] === false) {
|
||||
$io->warning('Your GD does not have WebP support. You will not be able to generate thumbnails of WebP images.');
|
||||
} else {
|
||||
!$only_issues && $io->success('GD has WebP support.');
|
||||
} elseif (!$only_issues) {
|
||||
$io->success('GD has WebP support.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,4 +189,4 @@ class CheckRequirementsCommand extends Command
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Command\Currencies;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use App\Entity\PriceInformations\Currency;
|
||||
use App\Services\Tools\ExchangeRateUpdater;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
@@ -35,30 +36,17 @@ use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use function count;
|
||||
use function strlen;
|
||||
|
||||
#[AsCommand('partdb:currencies:update-exchange-rates|partdb:update-exchange-rates|app:update-exchange-rates', 'Updates the currency exchange rates.')]
|
||||
class UpdateExchangeRatesCommand extends Command
|
||||
{
|
||||
protected static $defaultName = 'partdb:currencies:update-exchange-rates|partdb:update-exchange-rates|app:update-exchange-rates';
|
||||
|
||||
protected string $base_current;
|
||||
protected EntityManagerInterface $em;
|
||||
protected ExchangeRateUpdater $exchangeRateUpdater;
|
||||
|
||||
public function __construct(string $base_current, EntityManagerInterface $entityManager, ExchangeRateUpdater $exchangeRateUpdater)
|
||||
public function __construct(protected string $base_current, protected EntityManagerInterface $em, protected ExchangeRateUpdater $exchangeRateUpdater)
|
||||
{
|
||||
//$this->swap = $swap;
|
||||
$this->base_current = $base_current;
|
||||
|
||||
$this->em = $entityManager;
|
||||
$this->exchangeRateUpdater = $exchangeRateUpdater;
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setDescription('Updates the currency exchange rates.')
|
||||
->addArgument('iso_code', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'The ISO Codes of the currencies that should be updated.');
|
||||
$this->addArgument('iso_code', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'The ISO Codes of the currencies that should be updated.');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
@@ -69,7 +57,7 @@ class UpdateExchangeRatesCommand extends Command
|
||||
if (3 !== strlen($this->base_current)) {
|
||||
$io->error('Chosen Base current is not valid. Check your settings!');
|
||||
|
||||
return 1;
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$io->note('Update currency exchange rates with base currency: '.$this->base_current);
|
||||
@@ -78,11 +66,7 @@ class UpdateExchangeRatesCommand extends Command
|
||||
$iso_code = $input->getArgument('iso_code');
|
||||
$repo = $this->em->getRepository(Currency::class);
|
||||
|
||||
if (!empty($iso_code)) {
|
||||
$candidates = $repo->findBy(['iso_code' => $iso_code]);
|
||||
} else {
|
||||
$candidates = $repo->findAll();
|
||||
}
|
||||
$candidates = empty($iso_code) ? $repo->findAll() : $repo->findBy(['iso_code' => $iso_code]);
|
||||
|
||||
$success_counter = 0;
|
||||
|
||||
@@ -106,6 +90,6 @@ class UpdateExchangeRatesCommand extends Command
|
||||
|
||||
$io->success(sprintf('%d (of %d) currency exchange rates were updated.', $success_counter, count($candidates)));
|
||||
|
||||
return 0;
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Command\Logs;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use App\Entity\UserSystem\User;
|
||||
use App\Entity\Base\AbstractNamedDBElement;
|
||||
use App\Entity\LogSystem\AbstractLogEntry;
|
||||
use App\Repository\LogEntryRepository;
|
||||
@@ -36,23 +38,14 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
#[AsCommand('partdb:logs:show|app:show-logs', 'List the last event log entries.')]
|
||||
class ShowEventLogCommand extends Command
|
||||
{
|
||||
protected static $defaultName = 'partdb:logs:show|app:show-logs';
|
||||
protected EntityManagerInterface $entityManager;
|
||||
protected TranslatorInterface $translator;
|
||||
protected ElementTypeNameGenerator $elementTypeNameGenerator;
|
||||
protected LogEntryRepository $repo;
|
||||
protected LogEntryExtraFormatter $formatter;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager,
|
||||
TranslatorInterface $translator, ElementTypeNameGenerator $elementTypeNameGenerator, LogEntryExtraFormatter $formatter)
|
||||
public function __construct(protected EntityManagerInterface $entityManager,
|
||||
protected TranslatorInterface $translator, protected ElementTypeNameGenerator $elementTypeNameGenerator, protected LogEntryExtraFormatter $formatter)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
$this->translator = $translator;
|
||||
$this->elementTypeNameGenerator = $elementTypeNameGenerator;
|
||||
$this->formatter = $formatter;
|
||||
|
||||
$this->repo = $this->entityManager->getRepository(AbstractLogEntry::class);
|
||||
parent::__construct();
|
||||
}
|
||||
@@ -74,7 +67,7 @@ class ShowEventLogCommand extends Command
|
||||
if ($page > $max_page && $max_page > 0) {
|
||||
$io->error("There is no page ${page}! The maximum page is ${max_page}.");
|
||||
|
||||
return 1;
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$io->note("There are a total of ${total_count} log entries in the DB.");
|
||||
@@ -84,21 +77,19 @@ class ShowEventLogCommand extends Command
|
||||
$this->showPage($output, $desc, $limit, $page, $max_page, $showExtra);
|
||||
|
||||
if ($onePage) {
|
||||
return 0;
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$continue = $io->confirm('Do you want to show the next page?');
|
||||
++$page;
|
||||
}
|
||||
|
||||
return 0;
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setDescription('List the last event log entries.')
|
||||
->addOption('count', 'c', InputOption::VALUE_REQUIRED, 'How many log entries should be shown per page.', 50)
|
||||
$this->addOption('count', 'c', InputOption::VALUE_REQUIRED, 'How many log entries should be shown per page.', 50)
|
||||
->addOption('oldest_first', null, InputOption::VALUE_NONE, 'Show older entries first.')
|
||||
->addOption('page', 'p', InputOption::VALUE_REQUIRED, 'Which page should be shown?', 1)
|
||||
->addOption('onePage', null, InputOption::VALUE_NONE, 'Show only one page (dont ask to go to next).')
|
||||
@@ -147,14 +138,12 @@ class ShowEventLogCommand extends Command
|
||||
$target_class = $this->elementTypeNameGenerator->getLocalizedTypeLabel($entry->getTargetClass());
|
||||
}
|
||||
|
||||
if ($entry->getUser()) {
|
||||
if ($entry->getUser() instanceof User) {
|
||||
$user = $entry->getUser()->getFullName(true);
|
||||
} elseif ($entry->isCLIEntry()) {
|
||||
$user = $entry->getCLIUsername() . ' [CLI]';
|
||||
} else {
|
||||
if ($entry->isCLIEntry()) {
|
||||
$user = $entry->getCLIUsername() . ' [CLI]';
|
||||
} else {
|
||||
$user = $entry->getUsername() . ' [deleted]';
|
||||
}
|
||||
$user = $entry->getUsername() . ' [deleted]';
|
||||
}
|
||||
|
||||
$row = [
|
||||
|
||||
@@ -22,6 +22,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Command\Migrations;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use App\Entity\Attachments\AttachmentType;
|
||||
use App\Entity\Base\AbstractNamedDBElement;
|
||||
use App\Entity\ProjectSystem\Project;
|
||||
@@ -47,6 +48,7 @@ use function count;
|
||||
/**
|
||||
* This command converts the BBCode used by old Part-DB versions (<1.0), to the current used Markdown format.
|
||||
*/
|
||||
#[AsCommand('partdb:migrations:convert-bbcode|app:convert-bbcode', 'Converts BBCode used in old Part-DB versions to newly used Markdown')]
|
||||
class ConvertBBCodeCommand extends Command
|
||||
{
|
||||
/**
|
||||
@@ -57,18 +59,10 @@ class ConvertBBCodeCommand extends Command
|
||||
* @var string The regex (performed in PHP) used to check if a property really contains BBCODE
|
||||
*/
|
||||
protected const BBCODE_REGEX = '/\\[.+\\].*\\[\\/.+\\]/';
|
||||
|
||||
protected static $defaultName = 'partdb:migrations:convert-bbcode|app:convert-bbcode';
|
||||
|
||||
protected EntityManagerInterface $em;
|
||||
protected PropertyAccessorInterface $propertyAccessor;
|
||||
protected BBCodeToMarkdownConverter $converter;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager, PropertyAccessorInterface $propertyAccessor)
|
||||
public function __construct(protected EntityManagerInterface $em, protected PropertyAccessorInterface $propertyAccessor)
|
||||
{
|
||||
$this->em = $entityManager;
|
||||
$this->propertyAccessor = $propertyAccessor;
|
||||
|
||||
$this->converter = new BBCodeToMarkdownConverter();
|
||||
|
||||
parent::__construct();
|
||||
@@ -76,9 +70,7 @@ class ConvertBBCodeCommand extends Command
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setDescription('Converts BBCode used in old Part-DB versions to newly used Markdown')
|
||||
->setHelp('Older versions of Part-DB (<1.0) used BBCode for rich text formatting.
|
||||
$this->setHelp('Older versions of Part-DB (<1.0) used BBCode for rich text formatting.
|
||||
Part-DB now uses Markdown which offers more features but is incompatible with BBCode.
|
||||
When you upgrade from an pre 1.0 version you have to run this command to convert your comment fields');
|
||||
|
||||
@@ -129,25 +121,25 @@ class ConvertBBCodeCommand extends Command
|
||||
|
||||
//Fetch resulting classes
|
||||
$results = $qb->getQuery()->getResult();
|
||||
$io->note(sprintf('Found %d entities, that need to be converted!', count($results)));
|
||||
$io->note(sprintf('Found %d entities, that need to be converted!', is_countable($results) ? count($results) : 0));
|
||||
|
||||
//In verbose mode print the names of the entities
|
||||
foreach ($results as $result) {
|
||||
/** @var AbstractNamedDBElement $result */
|
||||
$io->writeln(
|
||||
'Convert entity: '.$result->getName().' ('.get_class($result).': '.$result->getID().')',
|
||||
'Convert entity: '.$result->getName().' ('.$result::class.': '.$result->getID().')',
|
||||
OutputInterface::VERBOSITY_VERBOSE
|
||||
);
|
||||
foreach ($properties as $property) {
|
||||
//Retrieve bbcode from entity
|
||||
$bbcode = $this->propertyAccessor->getValue($result, $property);
|
||||
//Check if the current property really contains BBCode
|
||||
if (!preg_match(static::BBCODE_REGEX, $bbcode)) {
|
||||
if (!preg_match(static::BBCODE_REGEX, (string) $bbcode)) {
|
||||
continue;
|
||||
}
|
||||
$io->writeln(
|
||||
'BBCode (old): '
|
||||
.str_replace('\n', ' ', substr($bbcode, 0, 255)),
|
||||
.str_replace('\n', ' ', substr((string) $bbcode, 0, 255)),
|
||||
OutputInterface::VERBOSITY_VERY_VERBOSE
|
||||
);
|
||||
$markdown = $this->converter->convert($bbcode);
|
||||
@@ -168,6 +160,6 @@ class ConvertBBCodeCommand extends Command
|
||||
$io->success('Changes saved to DB successfully!');
|
||||
}
|
||||
|
||||
return 0;
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
@@ -17,9 +20,9 @@
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
namespace App\Command\Migrations;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use App\Services\ImportExportSystem\PartKeeprImporter\PKDatastructureImporter;
|
||||
use App\Services\ImportExportSystem\PartKeeprImporter\MySQLDumpXMLConverter;
|
||||
use App\Services\ImportExportSystem\PartKeeprImporter\PKImportHelper;
|
||||
@@ -33,34 +36,19 @@ use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand('partdb:migrations:import-partkeepr', 'Import a PartKeepr database XML dump into Part-DB')]
|
||||
class ImportPartKeeprCommand extends Command
|
||||
{
|
||||
|
||||
protected static $defaultName = 'partdb:migrations:import-partkeepr';
|
||||
|
||||
protected EntityManagerInterface $em;
|
||||
protected MySQLDumpXMLConverter $xml_converter;
|
||||
protected PKDatastructureImporter $datastructureImporter;
|
||||
protected PKImportHelper $importHelper;
|
||||
protected PKPartImporter $partImporter;
|
||||
protected PKOptionalImporter $optionalImporter;
|
||||
|
||||
public function __construct(EntityManagerInterface $em, MySQLDumpXMLConverter $xml_converter,
|
||||
PKDatastructureImporter $datastructureImporter, PKPartImporter $partImporter, PKImportHelper $importHelper,
|
||||
PKOptionalImporter $optionalImporter)
|
||||
public function __construct(protected EntityManagerInterface $em, protected MySQLDumpXMLConverter $xml_converter,
|
||||
protected PKDatastructureImporter $datastructureImporter, protected PKPartImporter $partImporter, protected PKImportHelper $importHelper,
|
||||
protected PKOptionalImporter $optionalImporter)
|
||||
{
|
||||
parent::__construct(self::$defaultName);
|
||||
$this->em = $em;
|
||||
$this->datastructureImporter = $datastructureImporter;
|
||||
$this->importHelper = $importHelper;
|
||||
$this->partImporter = $partImporter;
|
||||
$this->xml_converter = $xml_converter;
|
||||
$this->optionalImporter = $optionalImporter;
|
||||
}
|
||||
|
||||
protected function configure()
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setDescription('Import a PartKeepr database XML dump into Part-DB');
|
||||
$this->setHelp('This command allows you to import a PartKeepr database exported by mysqldump as XML file into Part-DB');
|
||||
|
||||
$this->addArgument('file', InputArgument::REQUIRED, 'The file to which should be imported.');
|
||||
@@ -100,7 +88,7 @@ class ImportPartKeeprCommand extends Command
|
||||
if (!$this->importHelper->checkVersion($data)) {
|
||||
$db_version = $this->importHelper->getDatabaseSchemaVersion($data);
|
||||
$io->error('The version of the imported database is not supported! (Version: '.$db_version.')');
|
||||
return 1;
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
//Import the mandatory data
|
||||
@@ -118,7 +106,7 @@ class ImportPartKeeprCommand extends Command
|
||||
$io->success('Imported '.$count.' users.');
|
||||
}
|
||||
|
||||
return 0;
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function doImport(SymfonyStyle $io, array $data): void
|
||||
@@ -155,4 +143,4 @@ class ImportPartKeeprCommand extends Command
|
||||
$io->success('Imported '.$count.' parts.');
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
@@ -17,9 +20,9 @@
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
namespace App\Command\User;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use App\Entity\UserSystem\User;
|
||||
use App\Security\SamlUserFactory;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
@@ -30,25 +33,17 @@ use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand('partdb:user:convert-to-saml-user|partdb:users:convert-to-saml-user', 'Converts a local user to a SAML user (and vice versa)')]
|
||||
class ConvertToSAMLUserCommand extends Command
|
||||
{
|
||||
protected static $defaultName = 'partdb:user:convert-to-saml-user|partdb:users:convert-to-saml-user';
|
||||
|
||||
protected EntityManagerInterface $entityManager;
|
||||
protected bool $saml_enabled;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager, bool $saml_enabled)
|
||||
public function __construct(protected EntityManagerInterface $entityManager, protected bool $saml_enabled)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->entityManager = $entityManager;
|
||||
$this->saml_enabled = $saml_enabled;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setDescription('Converts a local user to a SAML user (and vice versa)')
|
||||
->setHelp('This converts a local user, which can login via the login form, to a SAML user, which can only login via SAML. This is useful if you want to migrate from a local user system to a SAML user system.')
|
||||
$this->setHelp('This converts a local user, which can login via the login form, to a SAML user, which can only login via SAML. This is useful if you want to migrate from a local user system to a SAML user system.')
|
||||
->addArgument('user', InputArgument::REQUIRED, 'The username (or email) of the user')
|
||||
->addOption('to-local', null, InputOption::VALUE_NONE, 'Converts a SAML user to a local user')
|
||||
;
|
||||
@@ -70,7 +65,7 @@ class ConvertToSAMLUserCommand extends Command
|
||||
if (!$user) {
|
||||
$io->error('User not found!');
|
||||
|
||||
return 1;
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$io->info('User found: '.$user->getFullName(true) . ': '.$user->getEmail().' [ID: ' . $user->getID() . ']');
|
||||
@@ -87,7 +82,7 @@ class ConvertToSAMLUserCommand extends Command
|
||||
$io->confirm('You are going to convert a SAML user to a local user. This means, that the user can only login via the login form. '
|
||||
. 'The permissions and groups settings of the user will remain unchanged. Do you really want to continue?');
|
||||
|
||||
$user->setSAMLUser(false);
|
||||
$user->setSamlUser(false);
|
||||
$user->setPassword(SamlUserFactory::SAML_PASSWORD_PLACEHOLDER);
|
||||
|
||||
$this->entityManager->flush();
|
||||
@@ -102,7 +97,7 @@ class ConvertToSAMLUserCommand extends Command
|
||||
$io->confirm('You are going to convert a local user to a SAML user. This means, that the user can only login via SAML afterwards. The password in the DB will be removed. '
|
||||
. 'The permissions and groups settings of the user will remain unchanged. Do you really want to continue?');
|
||||
|
||||
$user->setSAMLUser(true);
|
||||
$user->setSamlUser(true);
|
||||
$user->setPassword(SamlUserFactory::SAML_PASSWORD_PLACEHOLDER);
|
||||
|
||||
$this->entityManager->flush();
|
||||
@@ -112,4 +107,4 @@ class ConvertToSAMLUserCommand extends Command
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Command\User;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use App\Entity\UserSystem\User;
|
||||
use App\Events\SecurityEvent;
|
||||
use App\Events\SecurityEvents;
|
||||
@@ -34,28 +35,17 @@ use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
|
||||
#[AsCommand('partdb:users:set-password|app:set-password|users:set-password|partdb:user:set-password', 'Sets the password of a user')]
|
||||
class SetPasswordCommand extends Command
|
||||
{
|
||||
protected static $defaultName = 'partdb:users:set-password|app:set-password|users:set-password|partdb:user:set-password';
|
||||
|
||||
protected EntityManagerInterface $entityManager;
|
||||
protected UserPasswordHasherInterface $encoder;
|
||||
protected EventDispatcherInterface $eventDispatcher;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager, UserPasswordHasherInterface $passwordEncoder, EventDispatcherInterface $eventDispatcher)
|
||||
public function __construct(protected EntityManagerInterface $entityManager, protected UserPasswordHasherInterface $encoder, protected EventDispatcherInterface $eventDispatcher)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
$this->encoder = $passwordEncoder;
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setDescription('Sets the password of a user')
|
||||
->setHelp('This password allows you to set the password of a user, without knowing the old password.')
|
||||
$this->setHelp('This password allows you to set the password of a user, without knowing the old password.')
|
||||
->addArgument('user', InputArgument::REQUIRED, 'The username or email of the user')
|
||||
;
|
||||
}
|
||||
@@ -67,17 +57,17 @@ class SetPasswordCommand extends Command
|
||||
|
||||
$user = $this->entityManager->getRepository(User::class)->findByEmailOrName($user_name);
|
||||
|
||||
if (!$user) {
|
||||
if (!$user instanceof User) {
|
||||
$io->error(sprintf('No user with the given username %s found in the database!', $user_name));
|
||||
|
||||
return 1;
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$io->note('User found!');
|
||||
|
||||
if ($user->isSamlUser()) {
|
||||
$io->error('This user is a SAML user, so you can not change the password!');
|
||||
return 1;
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$proceed = $io->confirm(
|
||||
@@ -85,7 +75,7 @@ class SetPasswordCommand extends Command
|
||||
$user->getFullName(true), $user->getID()));
|
||||
|
||||
if (!$proceed) {
|
||||
return 1;
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$success = false;
|
||||
@@ -116,6 +106,6 @@ class SetPasswordCommand extends Command
|
||||
$security_event = new SecurityEvent($user);
|
||||
$this->eventDispatcher->dispatch($security_event, SecurityEvents::PASSWORD_CHANGED);
|
||||
|
||||
return 0;
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
@@ -17,9 +20,9 @@
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
namespace App\Command\User;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use App\Entity\UserSystem\Group;
|
||||
use App\Entity\UserSystem\PermissionData;
|
||||
use App\Entity\UserSystem\User;
|
||||
@@ -31,22 +34,12 @@ use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand('partdb:users:upgrade-permissions-schema', '(Manually) upgrades the permissions schema of all users to the latest version.')]
|
||||
final class UpgradePermissionsSchemaCommand extends Command
|
||||
{
|
||||
protected static $defaultName = 'partdb:users:upgrade-permissions-schema';
|
||||
protected static $defaultDescription = '(Manually) upgrades the permissions schema of all users to the latest version.';
|
||||
|
||||
private PermissionSchemaUpdater $permissionSchemaUpdater;
|
||||
private EntityManagerInterface $em;
|
||||
private EventCommentHelper $eventCommentHelper;
|
||||
|
||||
public function __construct(PermissionSchemaUpdater $permissionSchemaUpdater, EntityManagerInterface $entityManager, EventCommentHelper $eventCommentHelper)
|
||||
public function __construct(private readonly PermissionSchemaUpdater $permissionSchemaUpdater, private readonly EntityManagerInterface $em, private readonly EventCommentHelper $eventCommentHelper)
|
||||
{
|
||||
parent::__construct(self::$defaultName);
|
||||
|
||||
$this->permissionSchemaUpdater = $permissionSchemaUpdater;
|
||||
$this->eventCommentHelper = $eventCommentHelper;
|
||||
$this->em = $entityManager;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
@@ -81,26 +74,22 @@ final class UpgradePermissionsSchemaCommand extends Command
|
||||
}
|
||||
|
||||
$io->info('Found '. count($groups_to_upgrade) .' groups and '. count($users_to_upgrade) .' users that need an update.');
|
||||
if (empty($groups_to_upgrade) && empty($users_to_upgrade)) {
|
||||
if ($groups_to_upgrade === [] && $users_to_upgrade === []) {
|
||||
$io->success('All users and group permissions schemas are up-to-date. No update needed.');
|
||||
|
||||
return 0;
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
//List all users and groups that need an update
|
||||
$io->section('Groups that need an update:');
|
||||
$io->listing(array_map(static function (Group $group) {
|
||||
return $group->getName() . ' (ID: '. $group->getID() .', Current version: ' . $group->getPermissions()->getSchemaVersion() . ')';
|
||||
}, $groups_to_upgrade));
|
||||
$io->listing(array_map(static fn(Group $group): string => $group->getName() . ' (ID: '. $group->getID() .', Current version: ' . $group->getPermissions()->getSchemaVersion() . ')', $groups_to_upgrade));
|
||||
|
||||
$io->section('Users that need an update:');
|
||||
$io->listing(array_map(static function (User $user) {
|
||||
return $user->getUsername() . ' (ID: '. $user->getID() .', Current version: ' . $user->getPermissions()->getSchemaVersion() . ')';
|
||||
}, $users_to_upgrade));
|
||||
$io->listing(array_map(static fn(User $user): string => $user->getUsername() . ' (ID: '. $user->getID() .', Current version: ' . $user->getPermissions()->getSchemaVersion() . ')', $users_to_upgrade));
|
||||
|
||||
if(!$io->confirm('Continue with the update?', false)) {
|
||||
$io->warning('Update aborted.');
|
||||
return 0;
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
//Update all users and groups
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
@@ -17,9 +20,9 @@
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
namespace App\Command\User;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use App\Entity\UserSystem\User;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
@@ -29,24 +32,17 @@ use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand('partdb:users:enable|partdb:user:enable', 'Enables/Disable the login of one or more users')]
|
||||
class UserEnableCommand extends Command
|
||||
{
|
||||
protected static $defaultName = 'partdb:users:enable|partdb:user:enable';
|
||||
|
||||
protected EntityManagerInterface $entityManager;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager, string $name = null)
|
||||
public function __construct(protected EntityManagerInterface $entityManager, string $name = null)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
|
||||
parent::__construct($name);
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setDescription('Enables/Disable the login of one or more users')
|
||||
->setHelp('This allows you to allow or prevent the login of certain user. Use the --disable option to disable the login for the given users')
|
||||
$this->setHelp('This allows you to allow or prevent the login of certain user. Use the --disable option to disable the login for the given users')
|
||||
->addArgument('users', InputArgument::IS_ARRAY, 'The usernames of the users to use')
|
||||
->addOption('all', 'a', InputOption::VALUE_NONE, 'Enable/Disable all users')
|
||||
->addOption('disable', 'd', InputOption::VALUE_NONE, 'Disable the login of the given users')
|
||||
@@ -73,7 +69,7 @@ class UserEnableCommand extends Command
|
||||
} else { //Otherwise, fetch the users from DB
|
||||
foreach ($usernames as $username) {
|
||||
$user = $repo->findByEmailOrName($username);
|
||||
if ($user === null) {
|
||||
if (!$user instanceof User) {
|
||||
$io->error('No user found with username: '.$username);
|
||||
return self::FAILURE;
|
||||
}
|
||||
@@ -87,9 +83,7 @@ class UserEnableCommand extends Command
|
||||
$io->note('The following users will be enabled:');
|
||||
}
|
||||
$io->table(['Username', 'Enabled/Disabled'],
|
||||
array_map(static function(User $user) {
|
||||
return [$user->getFullName(true), $user->isDisabled() ? 'Disabled' : 'Enabled'];
|
||||
}, $users));
|
||||
array_map(static fn(User $user) => [$user->getFullName(true), $user->isDisabled() ? 'Disabled' : 'Enabled'], $users));
|
||||
|
||||
if(!$io->confirm('Do you want to continue?')) {
|
||||
$io->warning('Aborting!');
|
||||
@@ -107,4 +101,4 @@ class UserEnableCommand extends Command
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
@@ -17,9 +20,10 @@
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
namespace App\Command\User;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use App\Entity\UserSystem\Group;
|
||||
use App\Entity\UserSystem\User;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
@@ -28,24 +32,17 @@ use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand('partdb:users:list|users:list', 'Lists all users')]
|
||||
class UserListCommand extends Command
|
||||
{
|
||||
protected static $defaultName = 'partdb:users:list|users:list';
|
||||
|
||||
protected EntityManagerInterface $entityManager;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
public function __construct(protected EntityManagerInterface $entityManager)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setDescription('Lists all users')
|
||||
->setHelp('This command lists all users in the database.')
|
||||
$this->setHelp('This command lists all users in the database.')
|
||||
->addOption('local', 'l', null, 'Only list local users')
|
||||
->addOption('saml', 's', null, 'Only list SAML users')
|
||||
;
|
||||
@@ -82,13 +79,13 @@ class UserListCommand extends Command
|
||||
|
||||
foreach ($users as $user) {
|
||||
$table->addRow([
|
||||
$user->getId(),
|
||||
$user->getID(),
|
||||
$user->getUsername(),
|
||||
$user->getFullName(),
|
||||
$user->getEmail(),
|
||||
$user->getGroup() !== null ? $user->getGroup()->getName() . ' (ID: ' . $user->getGroup()->getID() . ')' : 'No group',
|
||||
$user->getGroup() instanceof Group ? $user->getGroup()->getName() . ' (ID: ' . $user->getGroup()->getID() . ')' : 'No group',
|
||||
$user->isDisabled() ? 'Yes' : 'No',
|
||||
$user->isSAMLUser() ? 'SAML' : 'Local',
|
||||
$user->isSamlUser() ? 'SAML' : 'Local',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -98,4 +95,4 @@ class UserListCommand extends Command
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
@@ -17,9 +20,9 @@
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
namespace App\Command\User;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use App\Entity\UserSystem\User;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Services\UserSystem\PermissionManager;
|
||||
@@ -34,22 +37,14 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
#[AsCommand('partdb:users:permissions|partdb:user:permissions', 'View and edit the permissions of a given user')]
|
||||
class UsersPermissionsCommand extends Command
|
||||
{
|
||||
protected static $defaultName = 'partdb:users:permissions|partdb:user:permissions';
|
||||
protected static $defaultDescription = 'View and edit the permissions of a given user';
|
||||
|
||||
protected EntityManagerInterface $entityManager;
|
||||
protected UserRepository $userRepository;
|
||||
protected PermissionManager $permissionResolver;
|
||||
protected TranslatorInterface $translator;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager, PermissionManager $permissionResolver, TranslatorInterface $translator)
|
||||
public function __construct(protected EntityManagerInterface $entityManager, protected PermissionManager $permissionResolver, protected TranslatorInterface $translator)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
$this->userRepository = $entityManager->getRepository(User::class);
|
||||
$this->permissionResolver = $permissionResolver;
|
||||
$this->translator = $translator;
|
||||
|
||||
parent::__construct(self::$defaultName);
|
||||
}
|
||||
@@ -73,12 +68,12 @@ class UsersPermissionsCommand extends Command
|
||||
//Find user
|
||||
$io->note('Finding user with username: ' . $username);
|
||||
$user = $this->userRepository->findByEmailOrName($username);
|
||||
if ($user === null) {
|
||||
if (!$user instanceof User) {
|
||||
$io->error('No user found with username: ' . $username);
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$io->note(sprintf('Found user %s with ID %d', $user->getFullName(true), $user->getId()));
|
||||
$io->note(sprintf('Found user %s with ID %d', $user->getFullName(true), $user->getID()));
|
||||
|
||||
$edit_mapping = $this->renderPermissionTable($output, $user, $inherit);
|
||||
|
||||
@@ -102,7 +97,7 @@ class UsersPermissionsCommand extends Command
|
||||
|
||||
|
||||
$new_value_str = $io->ask('Enter the new value for the permission (A = allow, D = disallow, I = inherit)');
|
||||
switch (strtolower($new_value_str)) {
|
||||
switch (strtolower((string) $new_value_str)) {
|
||||
case 'a':
|
||||
case 'allow':
|
||||
$new_value = true;
|
||||
@@ -209,11 +204,11 @@ class UsersPermissionsCommand extends Command
|
||||
|
||||
if ($permission_value === true) {
|
||||
return '<fg=green>Allow</>';
|
||||
} else if ($permission_value === false) {
|
||||
} elseif ($permission_value === false) {
|
||||
return '<fg=red>Disallow</>';
|
||||
} else if ($permission_value === null && !$inherit) {
|
||||
} elseif ($permission_value === null && !$inherit) {
|
||||
return '<fg=blue>Inherit</>';
|
||||
} else if ($permission_value === null && $inherit) {
|
||||
} elseif ($permission_value === null && $inherit) {
|
||||
return '<fg=red>Disallow (Inherited)</>';
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
@@ -17,9 +20,9 @@
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use App\Services\Misc\GitVersionInfo;
|
||||
use Shivas\VersioningBundle\Service\VersionManagerInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
@@ -27,25 +30,16 @@ use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand('partdb:version|app:version', 'Shows the currently installed version of Part-DB.')]
|
||||
class VersionCommand extends Command
|
||||
{
|
||||
protected static $defaultName = 'partdb:version|app:version';
|
||||
|
||||
protected VersionManagerInterface $versionManager;
|
||||
protected GitVersionInfo $gitVersionInfo;
|
||||
|
||||
public function __construct(VersionManagerInterface $versionManager, GitVersionInfo $gitVersionInfo)
|
||||
public function __construct(protected VersionManagerInterface $versionManager, protected GitVersionInfo $gitVersionInfo)
|
||||
{
|
||||
$this->versionManager = $versionManager;
|
||||
$this->gitVersionInfo = $gitVersionInfo;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setDescription('Shows the currently installed version of Part-DB.')
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
@@ -66,6 +60,6 @@ class VersionCommand extends Command
|
||||
$io->info('OS: '. php_uname());
|
||||
$io->info('PHP extension: '. implode(', ', get_loaded_extensions()));
|
||||
|
||||
return 0;
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,8 +37,9 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* @Route("/attachment_type")
|
||||
* @see \App\Tests\Controller\AdminPages\AttachmentTypeControllerTest
|
||||
*/
|
||||
#[Route(path: '/attachment_type')]
|
||||
class AttachmentTypeController extends BaseAdminController
|
||||
{
|
||||
protected string $entity_class = AttachmentType::class;
|
||||
@@ -48,44 +49,34 @@ class AttachmentTypeController extends BaseAdminController
|
||||
protected string $attachment_class = AttachmentTypeAttachment::class;
|
||||
protected ?string $parameter_class = AttachmentTypeParameter::class;
|
||||
|
||||
/**
|
||||
* @Route("/{id}", name="attachment_type_delete", methods={"DELETE"})
|
||||
*/
|
||||
#[Route(path: '/{id}', name: 'attachment_type_delete', methods: ['DELETE'])]
|
||||
public function delete(Request $request, AttachmentType $entity, StructuralElementRecursionHelper $recursionHelper): RedirectResponse
|
||||
{
|
||||
return $this->_delete($request, $entity, $recursionHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}/edit/{timestamp}", requirements={"id"="\d+"}, name="attachment_type_edit")
|
||||
* @Route("/{id}", requirements={"id"="\d+"})
|
||||
*/
|
||||
#[Route(path: '/{id}/edit/{timestamp}', requirements: ['id' => '\d+'], name: 'attachment_type_edit')]
|
||||
#[Route(path: '/{id}', requirements: ['id' => '\d+'])]
|
||||
public function edit(AttachmentType $entity, Request $request, EntityManagerInterface $em, ?string $timestamp = null): Response
|
||||
{
|
||||
return $this->_edit($entity, $request, $em, $timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/new", name="attachment_type_new")
|
||||
* @Route("/{id}/clone", name="attachment_type_clone")
|
||||
* @Route("/")
|
||||
*/
|
||||
#[Route(path: '/new', name: 'attachment_type_new')]
|
||||
#[Route(path: '/{id}/clone', name: 'attachment_type_clone')]
|
||||
#[Route(path: '/')]
|
||||
public function new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?AttachmentType $entity = null): Response
|
||||
{
|
||||
return $this->_new($request, $em, $importer, $entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/export", name="attachment_type_export_all")
|
||||
*/
|
||||
#[Route(path: '/export', name: 'attachment_type_export_all')]
|
||||
public function exportAll(EntityManagerInterface $em, EntityExporter $exporter, Request $request): Response
|
||||
{
|
||||
return $this->_exportAll($em, $exporter, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}/export", name="attachment_type_export")
|
||||
*/
|
||||
#[Route(path: '/{id}/export', name: 'attachment_type_export')]
|
||||
public function exportEntity(AttachmentType $entity, EntityExporter $exporter, Request $request): Response
|
||||
{
|
||||
return $this->_exportEntity($entity, $exporter, $request);
|
||||
|
||||
@@ -29,6 +29,7 @@ use App\Entity\Base\AbstractNamedDBElement;
|
||||
use App\Entity\Base\AbstractPartsContainingDBElement;
|
||||
use App\Entity\Base\AbstractStructuralDBElement;
|
||||
use App\Entity\Base\PartsContainingRepositoryInterface;
|
||||
use App\Entity\LabelSystem\LabelProcessMode;
|
||||
use App\Entity\LabelSystem\LabelProfile;
|
||||
use App\Entity\Parameters\AbstractParameter;
|
||||
use App\Entity\UserSystem\User;
|
||||
@@ -72,29 +73,16 @@ abstract class BaseAdminController extends AbstractController
|
||||
protected string $route_base = '';
|
||||
protected string $attachment_class = '';
|
||||
protected ?string $parameter_class = '';
|
||||
|
||||
protected UserPasswordHasherInterface $passwordEncoder;
|
||||
protected TranslatorInterface $translator;
|
||||
protected AttachmentSubmitHandler $attachmentSubmitHandler;
|
||||
protected EventCommentHelper $commentHelper;
|
||||
|
||||
protected HistoryHelper $historyHelper;
|
||||
protected TimeTravel $timeTravel;
|
||||
protected DataTableFactory $dataTableFactory;
|
||||
/**
|
||||
* @var EventDispatcher|EventDispatcherInterface
|
||||
*/
|
||||
protected $eventDispatcher;
|
||||
protected LabelGenerator $labelGenerator;
|
||||
protected LabelExampleElementsGenerator $barcodeExampleGenerator;
|
||||
|
||||
protected EntityManagerInterface $entityManager;
|
||||
|
||||
public function __construct(TranslatorInterface $translator, UserPasswordHasherInterface $passwordEncoder,
|
||||
AttachmentSubmitHandler $attachmentSubmitHandler,
|
||||
EventCommentHelper $commentHelper, HistoryHelper $historyHelper, TimeTravel $timeTravel,
|
||||
DataTableFactory $dataTableFactory, EventDispatcherInterface $eventDispatcher, LabelExampleElementsGenerator $barcodeExampleGenerator,
|
||||
LabelGenerator $labelGenerator, EntityManagerInterface $entityManager)
|
||||
public function __construct(protected TranslatorInterface $translator, protected UserPasswordHasherInterface $passwordEncoder,
|
||||
protected AttachmentSubmitHandler $attachmentSubmitHandler,
|
||||
protected EventCommentHelper $commentHelper, protected HistoryHelper $historyHelper, protected TimeTravel $timeTravel,
|
||||
protected DataTableFactory $dataTableFactory, EventDispatcherInterface $eventDispatcher, protected LabelExampleElementsGenerator $barcodeExampleGenerator,
|
||||
protected LabelGenerator $labelGenerator, protected EntityManagerInterface $entityManager)
|
||||
{
|
||||
if ('' === $this->entity_class || '' === $this->form_class || '' === $this->twig_template || '' === $this->route_base) {
|
||||
throw new InvalidArgumentException('You have to override the $entity_class, $form_class, $route_base and $twig_template value in your subclasss!');
|
||||
@@ -107,18 +95,7 @@ abstract class BaseAdminController extends AbstractController
|
||||
if ('' === $this->parameter_class || ($this->parameter_class && !is_a($this->parameter_class, AbstractParameter::class, true))) {
|
||||
throw new InvalidArgumentException('You have to override the $parameter_class value with a valid Parameter class in your subclass!');
|
||||
}
|
||||
|
||||
$this->translator = $translator;
|
||||
$this->passwordEncoder = $passwordEncoder;
|
||||
$this->attachmentSubmitHandler = $attachmentSubmitHandler;
|
||||
$this->commentHelper = $commentHelper;
|
||||
$this->historyHelper = $historyHelper;
|
||||
$this->timeTravel = $timeTravel;
|
||||
$this->dataTableFactory = $dataTableFactory;
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
$this->barcodeExampleGenerator = $barcodeExampleGenerator;
|
||||
$this->labelGenerator = $labelGenerator;
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
protected function revertElementIfNeeded(AbstractDBElement $entity, ?string $timestamp): ?DateTime
|
||||
@@ -177,13 +154,13 @@ abstract class BaseAdminController extends AbstractController
|
||||
$form_options = [
|
||||
'attachment_class' => $this->attachment_class,
|
||||
'parameter_class' => $this->parameter_class,
|
||||
'disabled' => null !== $timeTravel_timestamp,
|
||||
'disabled' => $timeTravel_timestamp instanceof \DateTime,
|
||||
];
|
||||
|
||||
//Disable editing of options, if user is not allowed to use twig...
|
||||
if (
|
||||
$entity instanceof LabelProfile
|
||||
&& 'twig' === $entity->getOptions()->getLinesMode()
|
||||
&& LabelProcessMode::TWIG === $entity->getOptions()->getProcessMode()
|
||||
&& !$this->isGranted('@labels.use_twig')
|
||||
) {
|
||||
$form_options['disable_options'] = true;
|
||||
@@ -245,7 +222,7 @@ abstract class BaseAdminController extends AbstractController
|
||||
/** @var AbstractPartsContainingRepository $repo */
|
||||
$repo = $this->entityManager->getRepository($this->entity_class);
|
||||
|
||||
return $this->renderForm($this->twig_template, [
|
||||
return $this->render($this->twig_template, [
|
||||
'entity' => $entity,
|
||||
'form' => $form,
|
||||
'route_base' => $this->route_base,
|
||||
@@ -267,15 +244,9 @@ abstract class BaseAdminController extends AbstractController
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function _new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?AbstractNamedDBElement $entity = null)
|
||||
protected function _new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?AbstractNamedDBElement $entity = null): Response
|
||||
{
|
||||
if (null === $entity) {
|
||||
/** @var AbstractStructuralDBElement|User $new_entity */
|
||||
$new_entity = new $this->entity_class();
|
||||
} else {
|
||||
/** @var AbstractStructuralDBElement|User $new_entity */
|
||||
$new_entity = clone $entity;
|
||||
}
|
||||
$new_entity = $entity instanceof AbstractNamedDBElement ? clone $entity : new $this->entity_class();
|
||||
|
||||
$this->denyAccessUnlessGranted('read', $new_entity);
|
||||
|
||||
@@ -287,42 +258,37 @@ abstract class BaseAdminController extends AbstractController
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
//Perform additional actions
|
||||
if ($this->additionalActionNew($form, $new_entity)) {
|
||||
//Upload passed files
|
||||
$attachments = $form['attachments'];
|
||||
foreach ($attachments as $attachment) {
|
||||
/** @var FormInterface $attachment */
|
||||
$options = [
|
||||
'secure_attachment' => $attachment['secureFile']->getData(),
|
||||
'download_url' => $attachment['downloadURL']->getData(),
|
||||
];
|
||||
//Perform additional actions
|
||||
if ($form->isSubmitted() && $form->isValid() && $this->additionalActionNew($form, $new_entity)) {
|
||||
//Upload passed files
|
||||
$attachments = $form['attachments'];
|
||||
foreach ($attachments as $attachment) {
|
||||
/** @var FormInterface $attachment */
|
||||
$options = [
|
||||
'secure_attachment' => $attachment['secureFile']->getData(),
|
||||
'download_url' => $attachment['downloadURL']->getData(),
|
||||
];
|
||||
|
||||
try {
|
||||
$this->attachmentSubmitHandler->handleFormSubmit(
|
||||
$attachment->getData(),
|
||||
$attachment['file']->getData(),
|
||||
$options
|
||||
);
|
||||
} catch (AttachmentDownloadException $attachmentDownloadException) {
|
||||
$this->addFlash(
|
||||
'error',
|
||||
$this->translator->trans(
|
||||
'attachment.download_failed'
|
||||
).' '.$attachmentDownloadException->getMessage()
|
||||
);
|
||||
}
|
||||
try {
|
||||
$this->attachmentSubmitHandler->handleFormSubmit(
|
||||
$attachment->getData(),
|
||||
$attachment['file']->getData(),
|
||||
$options
|
||||
);
|
||||
} catch (AttachmentDownloadException $attachmentDownloadException) {
|
||||
$this->addFlash(
|
||||
'error',
|
||||
$this->translator->trans(
|
||||
'attachment.download_failed'
|
||||
).' '.$attachmentDownloadException->getMessage()
|
||||
);
|
||||
}
|
||||
|
||||
$this->commentHelper->setMessage($form['log_comment']->getData());
|
||||
|
||||
$em->persist($new_entity);
|
||||
$em->flush();
|
||||
$this->addFlash('success', 'entity.created_flash');
|
||||
|
||||
return $this->redirectToRoute($this->route_base.'_edit', ['id' => $new_entity->getID()]);
|
||||
}
|
||||
$this->commentHelper->setMessage($form['log_comment']->getData());
|
||||
$em->persist($new_entity);
|
||||
$em->flush();
|
||||
$this->addFlash('success', 'entity.created_flash');
|
||||
return $this->redirectToRoute($this->route_base.'_edit', ['id' => $new_entity->getID()]);
|
||||
}
|
||||
|
||||
if ($form->isSubmitted() && !$form->isValid()) {
|
||||
@@ -362,14 +328,13 @@ abstract class BaseAdminController extends AbstractController
|
||||
try {
|
||||
$errors = $importer->importFileAndPersistToDB($file, $options);
|
||||
|
||||
/** @var ConstraintViolationList $error */
|
||||
foreach ($errors as $name => $error) {
|
||||
foreach ($error['violations'] as $violation) {
|
||||
foreach ($error as $violation) {
|
||||
$this->addFlash('error', $name.': '.$violation->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (UnexpectedValueException $e) {
|
||||
catch (UnexpectedValueException) {
|
||||
$this->addFlash('error', 'parts.import.flash.error.invalid_file');
|
||||
}
|
||||
}
|
||||
@@ -402,7 +367,7 @@ abstract class BaseAdminController extends AbstractController
|
||||
}
|
||||
|
||||
ret:
|
||||
return $this->renderForm($this->twig_template, [
|
||||
return $this->render($this->twig_template, [
|
||||
'entity' => $new_entity,
|
||||
'form' => $form,
|
||||
'import_form' => $import_form,
|
||||
@@ -437,7 +402,7 @@ abstract class BaseAdminController extends AbstractController
|
||||
{
|
||||
$this->denyAccessUnlessGranted('delete', $entity);
|
||||
|
||||
if ($this->isCsrfTokenValid('delete'.$entity->getId(), $request->request->get('_token'))) {
|
||||
if ($this->isCsrfTokenValid('delete'.$entity->getID(), $request->request->get('_token'))) {
|
||||
|
||||
$entityManager = $this->entityManager;
|
||||
|
||||
|
||||
@@ -36,8 +36,9 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* @Route("/category")
|
||||
* @see \App\Tests\Controller\AdminPages\CategoryControllerTest
|
||||
*/
|
||||
#[Route(path: '/category')]
|
||||
class CategoryController extends BaseAdminController
|
||||
{
|
||||
protected string $entity_class = Category::class;
|
||||
@@ -47,44 +48,34 @@ class CategoryController extends BaseAdminController
|
||||
protected string $attachment_class = CategoryAttachment::class;
|
||||
protected ?string $parameter_class = CategoryParameter::class;
|
||||
|
||||
/**
|
||||
* @Route("/{id}", name="category_delete", methods={"DELETE"})
|
||||
*/
|
||||
#[Route(path: '/{id}', name: 'category_delete', methods: ['DELETE'])]
|
||||
public function delete(Request $request, Category $entity, StructuralElementRecursionHelper $recursionHelper): RedirectResponse
|
||||
{
|
||||
return $this->_delete($request, $entity, $recursionHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}/edit/{timestamp}", requirements={"id"="\d+"}, name="category_edit")
|
||||
* @Route("/{id}", requirements={"id"="\d+"})
|
||||
*/
|
||||
#[Route(path: '/{id}/edit/{timestamp}', requirements: ['id' => '\d+'], name: 'category_edit')]
|
||||
#[Route(path: '/{id}', requirements: ['id' => '\d+'])]
|
||||
public function edit(Category $entity, Request $request, EntityManagerInterface $em, ?string $timestamp = null): Response
|
||||
{
|
||||
return $this->_edit($entity, $request, $em, $timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/new", name="category_new")
|
||||
* @Route("/{id}/clone", name="category_clone")
|
||||
* @Route("/")
|
||||
*/
|
||||
#[Route(path: '/new', name: 'category_new')]
|
||||
#[Route(path: '/{id}/clone', name: 'category_clone')]
|
||||
#[Route(path: '/')]
|
||||
public function new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?Category $entity = null): Response
|
||||
{
|
||||
return $this->_new($request, $em, $importer, $entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/export", name="category_export_all")
|
||||
*/
|
||||
#[Route(path: '/export', name: 'category_export_all')]
|
||||
public function exportAll(EntityManagerInterface $em, EntityExporter $exporter, Request $request): Response
|
||||
{
|
||||
return $this->_exportAll($em, $exporter, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}/export", name="category_export")
|
||||
*/
|
||||
#[Route(path: '/{id}/export', name: 'category_export')]
|
||||
public function exportEntity(Category $entity, EntityExporter $exporter, Request $request): Response
|
||||
{
|
||||
return $this->_exportEntity($entity, $exporter, $request);
|
||||
|
||||
@@ -52,10 +52,9 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* @Route("/currency")
|
||||
*
|
||||
* Class CurrencyController
|
||||
*/
|
||||
#[Route(path: '/currency')]
|
||||
class CurrencyController extends BaseAdminController
|
||||
{
|
||||
protected string $entity_class = Currency::class;
|
||||
@@ -65,8 +64,6 @@ class CurrencyController extends BaseAdminController
|
||||
protected string $attachment_class = CurrencyAttachment::class;
|
||||
protected ?string $parameter_class = CurrencyParameter::class;
|
||||
|
||||
protected ExchangeRateUpdater $exchangeRateUpdater;
|
||||
|
||||
public function __construct(
|
||||
TranslatorInterface $translator,
|
||||
UserPasswordHasherInterface $passwordEncoder,
|
||||
@@ -79,10 +76,8 @@ class CurrencyController extends BaseAdminController
|
||||
LabelExampleElementsGenerator $barcodeExampleGenerator,
|
||||
LabelGenerator $labelGenerator,
|
||||
EntityManagerInterface $entityManager,
|
||||
ExchangeRateUpdater $exchangeRateUpdater
|
||||
protected ExchangeRateUpdater $exchangeRateUpdater
|
||||
) {
|
||||
$this->exchangeRateUpdater = $exchangeRateUpdater;
|
||||
|
||||
parent::__construct(
|
||||
$translator,
|
||||
$passwordEncoder,
|
||||
@@ -98,9 +93,7 @@ class CurrencyController extends BaseAdminController
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}", name="currency_delete", methods={"DELETE"})
|
||||
*/
|
||||
#[Route(path: '/{id}', name: 'currency_delete', methods: ['DELETE'])]
|
||||
public function delete(Request $request, Currency $entity, StructuralElementRecursionHelper $recursionHelper): RedirectResponse
|
||||
{
|
||||
return $this->_delete($request, $entity, $recursionHelper);
|
||||
@@ -131,36 +124,28 @@ class CurrencyController extends BaseAdminController
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}/edit/{timestamp}", requirements={"id"="\d+"}, name="currency_edit")
|
||||
* @Route("/{id}", requirements={"id"="\d+"})
|
||||
*/
|
||||
#[Route(path: '/{id}/edit/{timestamp}', requirements: ['id' => '\d+'], name: 'currency_edit')]
|
||||
#[Route(path: '/{id}', requirements: ['id' => '\d+'])]
|
||||
public function edit(Currency $entity, Request $request, EntityManagerInterface $em, ?string $timestamp = null): Response
|
||||
{
|
||||
return $this->_edit($entity, $request, $em, $timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/new", name="currency_new")
|
||||
* @Route("/{id}/clone", name="currency_clone")
|
||||
* @Route("/")
|
||||
*/
|
||||
#[Route(path: '/new', name: 'currency_new')]
|
||||
#[Route(path: '/{id}/clone', name: 'currency_clone')]
|
||||
#[Route(path: '/')]
|
||||
public function new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?Currency $entity = null): Response
|
||||
{
|
||||
return $this->_new($request, $em, $importer, $entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/export", name="currency_export_all")
|
||||
*/
|
||||
#[Route(path: '/export', name: 'currency_export_all')]
|
||||
public function exportAll(EntityManagerInterface $em, EntityExporter $exporter, Request $request): Response
|
||||
{
|
||||
return $this->_exportAll($em, $exporter, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}/export", name="currency_export")
|
||||
*/
|
||||
#[Route(path: '/{id}/export', name: 'currency_export')]
|
||||
public function exportEntity(Currency $entity, EntityExporter $exporter, Request $request): Response
|
||||
{
|
||||
return $this->_exportEntity($entity, $exporter, $request);
|
||||
|
||||
@@ -37,8 +37,9 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* @Route("/footprint")
|
||||
* @see \App\Tests\Controller\AdminPages\FootprintControllerTest
|
||||
*/
|
||||
#[Route(path: '/footprint')]
|
||||
class FootprintController extends BaseAdminController
|
||||
{
|
||||
protected string $entity_class = Footprint::class;
|
||||
@@ -48,44 +49,34 @@ class FootprintController extends BaseAdminController
|
||||
protected string $attachment_class = FootprintAttachment::class;
|
||||
protected ?string $parameter_class = FootprintParameter::class;
|
||||
|
||||
/**
|
||||
* @Route("/{id}", name="footprint_delete", methods={"DELETE"})
|
||||
*/
|
||||
#[Route(path: '/{id}', name: 'footprint_delete', methods: ['DELETE'])]
|
||||
public function delete(Request $request, Footprint $entity, StructuralElementRecursionHelper $recursionHelper): RedirectResponse
|
||||
{
|
||||
return $this->_delete($request, $entity, $recursionHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}/edit/{timestamp}", requirements={"id"="\d+"}, name="footprint_edit")
|
||||
* @Route("/{id}", requirements={"id"="\d+"})
|
||||
*/
|
||||
#[Route(path: '/{id}/edit/{timestamp}', requirements: ['id' => '\d+'], name: 'footprint_edit')]
|
||||
#[Route(path: '/{id}', requirements: ['id' => '\d+'])]
|
||||
public function edit(Footprint $entity, Request $request, EntityManagerInterface $em, ?string $timestamp = null): Response
|
||||
{
|
||||
return $this->_edit($entity, $request, $em, $timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/new", name="footprint_new")
|
||||
* @Route("/{id}/clone", name="footprint_clone")
|
||||
* @Route("/")
|
||||
*/
|
||||
#[Route(path: '/new', name: 'footprint_new')]
|
||||
#[Route(path: '/{id}/clone', name: 'footprint_clone')]
|
||||
#[Route(path: '/')]
|
||||
public function new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?Footprint $entity = null): Response
|
||||
{
|
||||
return $this->_new($request, $em, $importer, $entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/export", name="footprint_export_all")
|
||||
*/
|
||||
#[Route(path: '/export', name: 'footprint_export_all')]
|
||||
public function exportAll(EntityManagerInterface $em, EntityExporter $exporter, Request $request): Response
|
||||
{
|
||||
return $this->_exportAll($em, $exporter, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}/export", name="footprint_export")
|
||||
*/
|
||||
#[Route(path: '/{id}/export', name: 'footprint_export')]
|
||||
public function exportEntity(AttachmentType $entity, EntityExporter $exporter, Request $request): Response
|
||||
{
|
||||
return $this->_exportEntity($entity, $exporter, $request);
|
||||
|
||||
@@ -36,8 +36,9 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
/**
|
||||
* @Route("/label_profile")
|
||||
* @see \App\Tests\Controller\AdminPages\LabelProfileControllerTest
|
||||
*/
|
||||
#[Route(path: '/label_profile')]
|
||||
class LabelProfileController extends BaseAdminController
|
||||
{
|
||||
protected string $entity_class = LabelProfile::class;
|
||||
@@ -48,44 +49,34 @@ class LabelProfileController extends BaseAdminController
|
||||
//Just a placeholder
|
||||
protected ?string $parameter_class = null;
|
||||
|
||||
/**
|
||||
* @Route("/{id}", name="label_profile_delete", methods={"DELETE"})
|
||||
*/
|
||||
#[Route(path: '/{id}', name: 'label_profile_delete', methods: ['DELETE'])]
|
||||
public function delete(Request $request, LabelProfile $entity, StructuralElementRecursionHelper $recursionHelper): RedirectResponse
|
||||
{
|
||||
return $this->_delete($request, $entity, $recursionHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}/edit/{timestamp}", requirements={"id"="\d+"}, name="label_profile_edit")
|
||||
* @Route("/{id}", requirements={"id"="\d+"})
|
||||
*/
|
||||
#[Route(path: '/{id}/edit/{timestamp}', requirements: ['id' => '\d+'], name: 'label_profile_edit')]
|
||||
#[Route(path: '/{id}', requirements: ['id' => '\d+'])]
|
||||
public function edit(LabelProfile $entity, Request $request, EntityManagerInterface $em, ?string $timestamp = null): Response
|
||||
{
|
||||
return $this->_edit($entity, $request, $em, $timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/new", name="label_profile_new")
|
||||
* @Route("/{id}/clone", name="label_profile_clone")
|
||||
* @Route("/")
|
||||
*/
|
||||
#[Route(path: '/new', name: 'label_profile_new')]
|
||||
#[Route(path: '/{id}/clone', name: 'label_profile_clone')]
|
||||
#[Route(path: '/')]
|
||||
public function new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?LabelProfile $entity = null): Response
|
||||
{
|
||||
return $this->_new($request, $em, $importer, $entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/export", name="label_profile_export_all")
|
||||
*/
|
||||
#[Route(path: '/export', name: 'label_profile_export_all')]
|
||||
public function exportAll(EntityManagerInterface $em, EntityExporter $exporter, Request $request): Response
|
||||
{
|
||||
return $this->_exportAll($em, $exporter, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}/export", name="label_profile_export")
|
||||
*/
|
||||
#[Route(path: '/{id}/export', name: 'label_profile_export')]
|
||||
public function exportEntity(LabelProfile $entity, EntityExporter $exporter, Request $request): Response
|
||||
{
|
||||
return $this->_exportEntity($entity, $exporter, $request);
|
||||
|
||||
@@ -36,8 +36,9 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* @Route("/manufacturer")
|
||||
* @see \App\Tests\Controller\AdminPages\ManufacturerControllerTest
|
||||
*/
|
||||
#[Route(path: '/manufacturer')]
|
||||
class ManufacturerController extends BaseAdminController
|
||||
{
|
||||
protected string $entity_class = Manufacturer::class;
|
||||
@@ -47,46 +48,34 @@ class ManufacturerController extends BaseAdminController
|
||||
protected string $attachment_class = ManufacturerAttachment::class;
|
||||
protected ?string $parameter_class = ManufacturerParameter::class;
|
||||
|
||||
/**
|
||||
* @Route("/{id}", name="manufacturer_delete", methods={"DELETE"})
|
||||
*
|
||||
* @return RedirectResponse
|
||||
*/
|
||||
#[Route(path: '/{id}', name: 'manufacturer_delete', methods: ['DELETE'])]
|
||||
public function delete(Request $request, Manufacturer $entity, StructuralElementRecursionHelper $recursionHelper): RedirectResponse
|
||||
{
|
||||
return $this->_delete($request, $entity, $recursionHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}/edit/{timestamp}", requirements={"id"="\d+"}, name="manufacturer_edit")
|
||||
* @Route("/{id}", requirements={"id"="\d+"})
|
||||
*/
|
||||
#[Route(path: '/{id}/edit/{timestamp}', requirements: ['id' => '\d+'], name: 'manufacturer_edit')]
|
||||
#[Route(path: '/{id}', requirements: ['id' => '\d+'])]
|
||||
public function edit(Manufacturer $entity, Request $request, EntityManagerInterface $em, ?string $timestamp = null): Response
|
||||
{
|
||||
return $this->_edit($entity, $request, $em, $timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/new", name="manufacturer_new")
|
||||
* @Route("/{id}/clone", name="manufacturer_clone")
|
||||
* @Route("/")
|
||||
*/
|
||||
#[Route(path: '/new', name: 'manufacturer_new')]
|
||||
#[Route(path: '/{id}/clone', name: 'manufacturer_clone')]
|
||||
#[Route(path: '/')]
|
||||
public function new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?Manufacturer $entity = null): Response
|
||||
{
|
||||
return $this->_new($request, $em, $importer, $entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/export", name="manufacturer_export_all")
|
||||
*/
|
||||
#[Route(path: '/export', name: 'manufacturer_export_all')]
|
||||
public function exportAll(EntityManagerInterface $em, EntityExporter $exporter, Request $request): Response
|
||||
{
|
||||
return $this->_exportAll($em, $exporter, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}/export", name="manufacturer_export")
|
||||
*/
|
||||
#[Route(path: '/{id}/export', name: 'manufacturer_export')]
|
||||
public function exportEntity(Manufacturer $entity, EntityExporter $exporter, Request $request): Response
|
||||
{
|
||||
return $this->_exportEntity($entity, $exporter, $request);
|
||||
|
||||
@@ -37,8 +37,9 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* @Route("/measurement_unit")
|
||||
* @see \App\Tests\Controller\AdminPages\MeasurementUnitControllerTest
|
||||
*/
|
||||
#[Route(path: '/measurement_unit')]
|
||||
class MeasurementUnitController extends BaseAdminController
|
||||
{
|
||||
protected string $entity_class = MeasurementUnit::class;
|
||||
@@ -48,44 +49,34 @@ class MeasurementUnitController extends BaseAdminController
|
||||
protected string $attachment_class = MeasurementUnitAttachment::class;
|
||||
protected ?string $parameter_class = MeasurementUnitParameter::class;
|
||||
|
||||
/**
|
||||
* @Route("/{id}", name="measurement_unit_delete", methods={"DELETE"})
|
||||
*/
|
||||
#[Route(path: '/{id}', name: 'measurement_unit_delete', methods: ['DELETE'])]
|
||||
public function delete(Request $request, MeasurementUnit $entity, StructuralElementRecursionHelper $recursionHelper): RedirectResponse
|
||||
{
|
||||
return $this->_delete($request, $entity, $recursionHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}/edit/{timestamp}", requirements={"id"="\d+"}, name="measurement_unit_edit")
|
||||
* @Route("/{id}", requirements={"id"="\d+"})
|
||||
*/
|
||||
#[Route(path: '/{id}/edit/{timestamp}', requirements: ['id' => '\d+'], name: 'measurement_unit_edit')]
|
||||
#[Route(path: '/{id}', requirements: ['id' => '\d+'])]
|
||||
public function edit(MeasurementUnit $entity, Request $request, EntityManagerInterface $em, ?string $timestamp = null): Response
|
||||
{
|
||||
return $this->_edit($entity, $request, $em, $timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/new", name="measurement_unit_new")
|
||||
* @Route("/{id}/clone", name="measurement_unit_clone")
|
||||
* @Route("/")
|
||||
*/
|
||||
#[Route(path: '/new', name: 'measurement_unit_new')]
|
||||
#[Route(path: '/{id}/clone', name: 'measurement_unit_clone')]
|
||||
#[Route(path: '/')]
|
||||
public function new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?MeasurementUnit $entity = null): Response
|
||||
{
|
||||
return $this->_new($request, $em, $importer, $entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/export", name="measurement_unit_export_all")
|
||||
*/
|
||||
#[Route(path: '/export', name: 'measurement_unit_export_all')]
|
||||
public function exportAll(EntityManagerInterface $em, EntityExporter $exporter, Request $request): Response
|
||||
{
|
||||
return $this->_exportAll($em, $exporter, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}/export", name="measurement_unit_export")
|
||||
*/
|
||||
#[Route(path: '/{id}/export', name: 'measurement_unit_export')]
|
||||
public function exportEntity(AttachmentType $entity, EntityExporter $exporter, Request $request): Response
|
||||
{
|
||||
return $this->_exportEntity($entity, $exporter, $request);
|
||||
|
||||
@@ -35,9 +35,7 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* @Route("/project")
|
||||
*/
|
||||
#[Route(path: '/project')]
|
||||
class ProjectAdminController extends BaseAdminController
|
||||
{
|
||||
protected string $entity_class = Project::class;
|
||||
@@ -47,44 +45,34 @@ class ProjectAdminController extends BaseAdminController
|
||||
protected string $attachment_class = ProjectAttachment::class;
|
||||
protected ?string $parameter_class = ProjectParameter::class;
|
||||
|
||||
/**
|
||||
* @Route("/{id}", name="project_delete", methods={"DELETE"})
|
||||
*/
|
||||
#[Route(path: '/{id}', name: 'project_delete', methods: ['DELETE'])]
|
||||
public function delete(Request $request, Project $entity, StructuralElementRecursionHelper $recursionHelper): RedirectResponse
|
||||
{
|
||||
return $this->_delete($request, $entity, $recursionHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}/edit/{timestamp}", requirements={"id"="\d+"}, name="project_edit")
|
||||
* @Route("/{id}/edit", requirements={"id"="\d+"})
|
||||
*/
|
||||
#[Route(path: '/{id}/edit/{timestamp}', requirements: ['id' => '\d+'], name: 'project_edit')]
|
||||
#[Route(path: '/{id}/edit', requirements: ['id' => '\d+'])]
|
||||
public function edit(Project $entity, Request $request, EntityManagerInterface $em, ?string $timestamp = null): Response
|
||||
{
|
||||
return $this->_edit($entity, $request, $em, $timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/new", name="project_new")
|
||||
* @Route("/{id}/clone", name="device_clone")
|
||||
* @Route("/")
|
||||
*/
|
||||
#[Route(path: '/new', name: 'project_new')]
|
||||
#[Route(path: '/{id}/clone', name: 'device_clone')]
|
||||
#[Route(path: '/')]
|
||||
public function new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?Project $entity = null): Response
|
||||
{
|
||||
return $this->_new($request, $em, $importer, $entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/export", name="project_export_all")
|
||||
*/
|
||||
#[Route(path: '/export', name: 'project_export_all')]
|
||||
public function exportAll(EntityManagerInterface $em, EntityExporter $exporter, Request $request): Response
|
||||
{
|
||||
return $this->_exportAll($em, $exporter, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}/export", name="project_export")
|
||||
*/
|
||||
#[Route(path: '/{id}/export', name: 'project_export')]
|
||||
public function exportEntity(Project $entity, EntityExporter $exporter, Request $request): Response
|
||||
{
|
||||
return $this->_exportEntity($entity, $exporter, $request);
|
||||
|
||||
@@ -36,8 +36,9 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* @Route("/store_location")
|
||||
* @see \App\Tests\Controller\AdminPages\StorelocationControllerTest
|
||||
*/
|
||||
#[Route(path: '/store_location')]
|
||||
class StorelocationController extends BaseAdminController
|
||||
{
|
||||
protected string $entity_class = Storelocation::class;
|
||||
@@ -47,44 +48,34 @@ class StorelocationController extends BaseAdminController
|
||||
protected string $attachment_class = StorelocationAttachment::class;
|
||||
protected ?string $parameter_class = StorelocationParameter::class;
|
||||
|
||||
/**
|
||||
* @Route("/{id}", name="store_location_delete", methods={"DELETE"})
|
||||
*/
|
||||
#[Route(path: '/{id}', name: 'store_location_delete', methods: ['DELETE'])]
|
||||
public function delete(Request $request, Storelocation $entity, StructuralElementRecursionHelper $recursionHelper): RedirectResponse
|
||||
{
|
||||
return $this->_delete($request, $entity, $recursionHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}/edit/{timestamp}", requirements={"id"="\d+"}, name="store_location_edit")
|
||||
* @Route("/{id}", requirements={"id"="\d+"})
|
||||
*/
|
||||
#[Route(path: '/{id}/edit/{timestamp}', requirements: ['id' => '\d+'], name: 'store_location_edit')]
|
||||
#[Route(path: '/{id}', requirements: ['id' => '\d+'])]
|
||||
public function edit(Storelocation $entity, Request $request, EntityManagerInterface $em, ?string $timestamp = null): Response
|
||||
{
|
||||
return $this->_edit($entity, $request, $em, $timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/new", name="store_location_new")
|
||||
* @Route("/{id}/clone", name="store_location_clone")
|
||||
* @Route("/")
|
||||
*/
|
||||
#[Route(path: '/new', name: 'store_location_new')]
|
||||
#[Route(path: '/{id}/clone', name: 'store_location_clone')]
|
||||
#[Route(path: '/')]
|
||||
public function new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?Storelocation $entity = null): Response
|
||||
{
|
||||
return $this->_new($request, $em, $importer, $entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/export", name="store_location_export_all")
|
||||
*/
|
||||
#[Route(path: '/export', name: 'store_location_export_all')]
|
||||
public function exportAll(EntityManagerInterface $em, EntityExporter $exporter, Request $request): Response
|
||||
{
|
||||
return $this->_exportAll($em, $exporter, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}/export", name="store_location_export")
|
||||
*/
|
||||
#[Route(path: '/{id}/export', name: 'store_location_export')]
|
||||
public function exportEntity(Storelocation $entity, EntityExporter $exporter, Request $request): Response
|
||||
{
|
||||
return $this->_exportEntity($entity, $exporter, $request);
|
||||
|
||||
@@ -36,8 +36,9 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* @Route("/supplier")
|
||||
* @see \App\Tests\Controller\AdminPages\SupplierControllerTest
|
||||
*/
|
||||
#[Route(path: '/supplier')]
|
||||
class SupplierController extends BaseAdminController
|
||||
{
|
||||
protected string $entity_class = Supplier::class;
|
||||
@@ -47,44 +48,34 @@ class SupplierController extends BaseAdminController
|
||||
protected string $attachment_class = SupplierAttachment::class;
|
||||
protected ?string $parameter_class = SupplierParameter::class;
|
||||
|
||||
/**
|
||||
* @Route("/{id}", name="supplier_delete", methods={"DELETE"})
|
||||
*/
|
||||
#[Route(path: '/{id}', name: 'supplier_delete', methods: ['DELETE'])]
|
||||
public function delete(Request $request, Supplier $entity, StructuralElementRecursionHelper $recursionHelper): RedirectResponse
|
||||
{
|
||||
return $this->_delete($request, $entity, $recursionHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}/edit/{timestamp}", requirements={"id"="\d+"}, name="supplier_edit")
|
||||
* @Route("/{id}", requirements={"id"="\d+"})
|
||||
*/
|
||||
#[Route(path: '/{id}/edit/{timestamp}', requirements: ['id' => '\d+'], name: 'supplier_edit')]
|
||||
#[Route(path: '/{id}', requirements: ['id' => '\d+'])]
|
||||
public function edit(Supplier $entity, Request $request, EntityManagerInterface $em, ?string $timestamp = null): Response
|
||||
{
|
||||
return $this->_edit($entity, $request, $em, $timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/new", name="supplier_new")
|
||||
* @Route("/{id}/clone", name="supplier_clone")
|
||||
* @Route("/")
|
||||
*/
|
||||
#[Route(path: '/new', name: 'supplier_new')]
|
||||
#[Route(path: '/{id}/clone', name: 'supplier_clone')]
|
||||
#[Route(path: '/')]
|
||||
public function new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?Supplier $entity = null): Response
|
||||
{
|
||||
return $this->_new($request, $em, $importer, $entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/export", name="supplier_export_all")
|
||||
*/
|
||||
#[Route(path: '/export', name: 'supplier_export_all')]
|
||||
public function exportAll(EntityManagerInterface $em, EntityExporter $exporter, Request $request): Response
|
||||
{
|
||||
return $this->_exportAll($em, $exporter, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}/export", name="supplier_export")
|
||||
*/
|
||||
#[Route(path: '/{id}/export', name: 'supplier_export')]
|
||||
public function exportEntity(Supplier $entity, EntityExporter $exporter, Request $request): Response
|
||||
{
|
||||
return $this->_exportEntity($entity, $exporter, $request);
|
||||
|
||||
@@ -42,9 +42,8 @@ class AttachmentFileController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* Download the selected attachment.
|
||||
*
|
||||
* @Route("/attachment/{id}/download", name="attachment_download")
|
||||
*/
|
||||
#[Route(path: '/attachment/{id}/download', name: 'attachment_download')]
|
||||
public function download(Attachment $attachment, AttachmentManager $helper): BinaryFileResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('read', $attachment);
|
||||
@@ -72,9 +71,8 @@ class AttachmentFileController extends AbstractController
|
||||
|
||||
/**
|
||||
* View the attachment.
|
||||
*
|
||||
* @Route("/attachment/{id}/view", name="attachment_view")
|
||||
*/
|
||||
#[Route(path: '/attachment/{id}/view', name: 'attachment_view')]
|
||||
public function view(Attachment $attachment, AttachmentManager $helper): BinaryFileResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('read', $attachment);
|
||||
@@ -100,9 +98,7 @@ class AttachmentFileController extends AbstractController
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/attachment/list", name="attachment_list")
|
||||
*/
|
||||
#[Route(path: '/attachment/list', name: 'attachment_list')]
|
||||
public function attachmentsTable(Request $request, DataTableFactory $dataTableFactory, NodesListBuilder $nodesListBuilder): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('@attachments.list_attachments');
|
||||
@@ -124,7 +120,7 @@ class AttachmentFileController extends AbstractController
|
||||
|
||||
return $this->render('attachment_list.html.twig', [
|
||||
'datatable' => $table,
|
||||
'filterForm' => $filterForm->createView(),
|
||||
'filterForm' => $filterForm,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,9 +39,7 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* @Route("/group")
|
||||
*/
|
||||
#[Route(path: '/group')]
|
||||
class GroupController extends BaseAdminController
|
||||
{
|
||||
protected string $entity_class = Group::class;
|
||||
@@ -51,10 +49,8 @@ class GroupController extends BaseAdminController
|
||||
protected string $attachment_class = GroupAttachment::class;
|
||||
protected ?string $parameter_class = GroupParameter::class;
|
||||
|
||||
/**
|
||||
* @Route("/{id}/edit/{timestamp}", requirements={"id"="\d+"}, name="group_edit")
|
||||
* @Route("/{id}/", requirements={"id"="\d+"})
|
||||
*/
|
||||
#[Route(path: '/{id}/edit/{timestamp}', requirements: ['id' => '\d+'], name: 'group_edit')]
|
||||
#[Route(path: '/{id}/', requirements: ['id' => '\d+'])]
|
||||
public function edit(Group $entity, Request $request, EntityManagerInterface $em, PermissionPresetsHelper $permissionPresetsHelper, PermissionSchemaUpdater $permissionSchemaUpdater, ?string $timestamp = null): Response
|
||||
{
|
||||
//Do an upgrade of the permission schema if needed (so the user can see the permissions a user get on next request (even if it was not done yet)
|
||||
@@ -63,7 +59,7 @@ class GroupController extends BaseAdminController
|
||||
//Handle permissions presets
|
||||
if ($request->request->has('permission_preset')) {
|
||||
$this->denyAccessUnlessGranted('edit_permissions', $entity);
|
||||
if ($this->isCsrfTokenValid('group'.$entity->getId(), $request->request->get('_token'))) {
|
||||
if ($this->isCsrfTokenValid('group'.$entity->getID(), $request->request->get('_token'))) {
|
||||
$preset = $request->request->get('permission_preset');
|
||||
|
||||
$permissionPresetsHelper->applyPreset($entity, $preset);
|
||||
@@ -82,35 +78,27 @@ class GroupController extends BaseAdminController
|
||||
return $this->_edit($entity, $request, $em, $timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/new", name="group_new")
|
||||
* @Route("/{id}/clone", name="group_clone")
|
||||
* @Route("/")
|
||||
*/
|
||||
#[Route(path: '/new', name: 'group_new')]
|
||||
#[Route(path: '/{id}/clone', name: 'group_clone')]
|
||||
#[Route(path: '/')]
|
||||
public function new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?Group $entity = null): Response
|
||||
{
|
||||
return $this->_new($request, $em, $importer, $entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}", name="group_delete", methods={"DELETE"})
|
||||
*/
|
||||
#[Route(path: '/{id}', name: 'group_delete', methods: ['DELETE'])]
|
||||
public function delete(Request $request, Group $entity, StructuralElementRecursionHelper $recursionHelper): RedirectResponse
|
||||
{
|
||||
return $this->_delete($request, $entity, $recursionHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/export", name="group_export_all")
|
||||
*/
|
||||
#[Route(path: '/export', name: 'group_export_all')]
|
||||
public function exportAll(EntityManagerInterface $em, EntityExporter $exporter, Request $request): Response
|
||||
{
|
||||
return $this->_exportAll($em, $exporter, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}/export", name="group_export")
|
||||
*/
|
||||
#[Route(path: '/{id}/export', name: 'group_export')]
|
||||
public function exportEntity(Group $entity, EntityExporter $exporter, Request $request): Response
|
||||
{
|
||||
return $this->_exportEntity($entity, $exporter, $request);
|
||||
|
||||
@@ -37,33 +37,31 @@ use Symfony\Contracts\Cache\CacheInterface;
|
||||
|
||||
class HomepageController extends AbstractController
|
||||
{
|
||||
protected CacheInterface $cache;
|
||||
protected KernelInterface $kernel;
|
||||
protected DataTableFactory $dataTable;
|
||||
|
||||
public function __construct(CacheInterface $cache, KernelInterface $kernel, DataTableFactory $dataTable)
|
||||
public function __construct(protected CacheInterface $cache, protected KernelInterface $kernel, protected DataTableFactory $dataTable)
|
||||
{
|
||||
$this->cache = $cache;
|
||||
$this->kernel = $kernel;
|
||||
$this->dataTable = $dataTable;
|
||||
}
|
||||
|
||||
public function getBanner(): string
|
||||
{
|
||||
$banner = $this->getParameter('partdb.banner');
|
||||
if (!is_string($banner)) {
|
||||
throw new \RuntimeException('The parameter "partdb.banner" must be a string.');
|
||||
}
|
||||
if (empty($banner)) {
|
||||
$banner_path = $this->kernel->getProjectDir()
|
||||
.DIRECTORY_SEPARATOR.'config'.DIRECTORY_SEPARATOR.'banner.md';
|
||||
|
||||
return file_get_contents($banner_path);
|
||||
$tmp = file_get_contents($banner_path);
|
||||
if (false === $tmp) {
|
||||
throw new \RuntimeException('The banner file could not be read.');
|
||||
}
|
||||
$banner = $tmp;
|
||||
}
|
||||
|
||||
return $banner;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/", name="homepage")
|
||||
*/
|
||||
#[Route(path: '/', name: 'homepage')]
|
||||
public function homepage(Request $request, GitVersionInfo $versionInfo, EntityManagerInterface $entityManager): Response
|
||||
{
|
||||
if ($this->isGranted('@tools.lastActivity')) {
|
||||
|
||||
@@ -43,7 +43,9 @@ namespace App\Controller;
|
||||
|
||||
use App\Entity\Base\AbstractDBElement;
|
||||
use App\Entity\LabelSystem\LabelOptions;
|
||||
use App\Entity\LabelSystem\LabelProcessMode;
|
||||
use App\Entity\LabelSystem\LabelProfile;
|
||||
use App\Entity\LabelSystem\LabelSupportedElement;
|
||||
use App\Exceptions\TwigModeException;
|
||||
use App\Form\LabelSystem\LabelDialogType;
|
||||
use App\Repository\DBElementRepository;
|
||||
@@ -59,59 +61,39 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* @Route("/label")
|
||||
*/
|
||||
#[Route(path: '/label')]
|
||||
class LabelController extends AbstractController
|
||||
{
|
||||
protected LabelGenerator $labelGenerator;
|
||||
protected EntityManagerInterface $em;
|
||||
protected ElementTypeNameGenerator $elementTypeNameGenerator;
|
||||
protected RangeParser $rangeParser;
|
||||
protected TranslatorInterface $translator;
|
||||
|
||||
public function __construct(LabelGenerator $labelGenerator, EntityManagerInterface $em, ElementTypeNameGenerator $elementTypeNameGenerator,
|
||||
RangeParser $rangeParser, TranslatorInterface $translator)
|
||||
public function __construct(protected LabelGenerator $labelGenerator, protected EntityManagerInterface $em, protected ElementTypeNameGenerator $elementTypeNameGenerator, protected RangeParser $rangeParser, protected TranslatorInterface $translator)
|
||||
{
|
||||
$this->labelGenerator = $labelGenerator;
|
||||
$this->em = $em;
|
||||
$this->elementTypeNameGenerator = $elementTypeNameGenerator;
|
||||
$this->rangeParser = $rangeParser;
|
||||
$this->translator = $translator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/dialog", name="label_dialog")
|
||||
* @Route("/{profile}/dialog", name="label_dialog_profile")
|
||||
*/
|
||||
#[Route(path: '/dialog', name: 'label_dialog')]
|
||||
#[Route(path: '/{profile}/dialog', name: 'label_dialog_profile')]
|
||||
public function generator(Request $request, ?LabelProfile $profile = null): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('@labels.create_labels');
|
||||
|
||||
//If we inherit a LabelProfile, the user need to have access to it...
|
||||
if (null !== $profile) {
|
||||
if ($profile instanceof LabelProfile) {
|
||||
$this->denyAccessUnlessGranted('read', $profile);
|
||||
}
|
||||
|
||||
if ($profile) {
|
||||
$label_options = $profile->getOptions();
|
||||
} else {
|
||||
$label_options = new LabelOptions();
|
||||
}
|
||||
$label_options = $profile instanceof LabelProfile ? $profile->getOptions() : new LabelOptions();
|
||||
|
||||
//We have to disable the options, if twig mode is selected and user is not allowed to use it.
|
||||
$disable_options = 'twig' === $label_options->getLinesMode() && !$this->isGranted('@labels.use_twig');
|
||||
$disable_options = (LabelProcessMode::TWIG === $label_options->getProcessMode()) && !$this->isGranted('@labels.use_twig');
|
||||
|
||||
$form = $this->createForm(LabelDialogType::class, null, [
|
||||
'disable_options' => $disable_options,
|
||||
]);
|
||||
|
||||
//Try to parse given target_type and target_id
|
||||
$target_type = $request->query->get('target_type', null);
|
||||
$target_type = $request->query->getEnum('target_type', LabelSupportedElement::class, null);
|
||||
$target_id = $request->query->get('target_id', null);
|
||||
$generate = $request->query->getBoolean('generate', false);
|
||||
|
||||
if (null === $profile && is_string($target_type)) {
|
||||
if (!$profile instanceof LabelProfile && $target_type instanceof LabelSupportedElement) {
|
||||
$label_options->setSupportedElement($target_type);
|
||||
}
|
||||
if (is_string($target_id)) {
|
||||
@@ -128,10 +110,10 @@ class LabelController extends AbstractController
|
||||
$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() && null !== $profile)) {
|
||||
if (($form->isSubmitted() && $form->isValid()) || ($generate && !$form->isSubmitted() && $profile instanceof LabelProfile)) {
|
||||
$target_id = (string) $form->get('target_id')->getData();
|
||||
$targets = $this->findObjects($form_options->getSupportedElement(), $target_id);
|
||||
if (!empty($targets)) {
|
||||
if ($targets !== []) {
|
||||
try {
|
||||
$pdf_data = $this->labelGenerator->generateLabel($form_options, $targets);
|
||||
$filename = $this->getLabelName($targets[0], $profile);
|
||||
@@ -144,9 +126,14 @@ class LabelController extends AbstractController
|
||||
new FormError($this->translator->trans('label_generator.no_entities_found'))
|
||||
);
|
||||
}
|
||||
|
||||
//When the profile lines are empty, show a notice flash
|
||||
if (trim($form_options->getLines()) === '') {
|
||||
$this->addFlash('notice', 'label_generator.no_lines_given');
|
||||
}
|
||||
}
|
||||
|
||||
return $this->renderForm('label_system/dialog.html.twig', [
|
||||
return $this->render('label_system/dialog.html.twig', [
|
||||
'form' => $form,
|
||||
'pdf_data' => $pdf_data,
|
||||
'filename' => $filename,
|
||||
@@ -162,16 +149,12 @@ class LabelController extends AbstractController
|
||||
return $ret.'.pdf';
|
||||
}
|
||||
|
||||
protected function findObjects(string $type, string $ids): array
|
||||
protected function findObjects(LabelSupportedElement $type, string $ids): array
|
||||
{
|
||||
if (!isset(LabelGenerator::CLASS_SUPPORT_MAPPING[$type])) {
|
||||
throw new InvalidArgumentException('The given type is not known and can not be mapped to a class!');
|
||||
}
|
||||
|
||||
$id_array = $this->rangeParser->parse($ids);
|
||||
|
||||
/** @var DBElementRepository $repo */
|
||||
$repo = $this->em->getRepository(LabelGenerator::CLASS_SUPPORT_MAPPING[$type]);
|
||||
$repo = $this->em->getRepository($type->getEntityClass());
|
||||
|
||||
return $repo->getElementsFromIDArray($id_array);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\DataTables\Column\LogEntryTargetColumn;
|
||||
use App\DataTables\Filters\LogFilter;
|
||||
use App\DataTables\LogDataTable;
|
||||
use App\Entity\Base\AbstractDBElement;
|
||||
@@ -33,6 +34,10 @@ use App\Entity\LogSystem\ElementEditedLogEntry;
|
||||
use App\Form\Filters\LogFilterType;
|
||||
use App\Repository\DBElementRepository;
|
||||
use App\Services\LogSystem\EventUndoHelper;
|
||||
use App\Services\LogSystem\EventUndoMode;
|
||||
use App\Services\LogSystem\LogEntryExtraFormatter;
|
||||
use App\Services\LogSystem\LogLevelHelper;
|
||||
use App\Services\LogSystem\LogTargetHelper;
|
||||
use App\Services\LogSystem\TimeTravel;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
@@ -45,27 +50,17 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* @Route("/log")
|
||||
*/
|
||||
#[Route(path: '/log')]
|
||||
class LogController extends AbstractController
|
||||
{
|
||||
protected EntityManagerInterface $entityManager;
|
||||
protected TimeTravel $timeTravel;
|
||||
protected DBElementRepository $dbRepository;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager, TimeTravel $timeTravel)
|
||||
public function __construct(protected EntityManagerInterface $entityManager, protected TimeTravel $timeTravel)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
$this->timeTravel = $timeTravel;
|
||||
$this->dbRepository = $entityManager->getRepository(AbstractDBElement::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/", name="log_view")
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
#[Route(path: '/', name: 'log_view')]
|
||||
public function showLogs(Request $request, DataTableFactory $dataTable): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('@system.show_logs');
|
||||
@@ -89,26 +84,66 @@ class LogController extends AbstractController
|
||||
|
||||
return $this->render('log_system/log_list.html.twig', [
|
||||
'datatable' => $table,
|
||||
'filterForm' => $filterForm->createView(),
|
||||
'filterForm' => $filterForm,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/undo", name="log_undo", methods={"POST"})
|
||||
*/
|
||||
#[Route(path: '/{id}/details', name: 'log_details')]
|
||||
public function logDetails(AbstractLogEntry $logEntry, LogEntryExtraFormatter $logEntryExtraFormatter,
|
||||
LogLevelHelper $logLevelHelper, LogTargetHelper $logTargetHelper, EntityManagerInterface $entityManager): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('show_details', $logEntry);
|
||||
|
||||
$extra_html = $logEntryExtraFormatter->format($logEntry);
|
||||
$target_html = $logTargetHelper->formatTarget($logEntry);
|
||||
|
||||
$repo = $entityManager->getRepository(AbstractLogEntry::class);
|
||||
$target_element = $repo->getTargetElement($logEntry);
|
||||
|
||||
return $this->render('log_system/details/log_details.html.twig', [
|
||||
'log_entry' => $logEntry,
|
||||
'target_element' => $target_element,
|
||||
'extra_html' => $extra_html,
|
||||
'target_html' => $target_html,
|
||||
'log_level_helper' => $logLevelHelper,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/delete', name: 'log_delete', methods: ['DELETE'])]
|
||||
public function deleteLogEntry(Request $request, AbstractLogEntry $logEntry, EntityManagerInterface $entityManager): RedirectResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('delete', $logEntry);
|
||||
|
||||
if ($this->isCsrfTokenValid('delete'.$logEntry->getID(), $request->request->get('_token'))) {
|
||||
//Remove part
|
||||
$entityManager->remove($logEntry);
|
||||
//Flush changes
|
||||
$entityManager->flush();
|
||||
$this->addFlash('success', 'log.delete.success');
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('homepage');
|
||||
}
|
||||
|
||||
|
||||
#[Route(path: '/undo', name: 'log_undo', methods: ['POST'])]
|
||||
public function undoRevertLog(Request $request, EventUndoHelper $eventUndoHelper): RedirectResponse
|
||||
{
|
||||
$mode = EventUndoHelper::MODE_UNDO;
|
||||
$id = $request->request->get('undo');
|
||||
$mode = EventUndoMode::UNDO;
|
||||
$id = $request->request->getInt('undo');
|
||||
|
||||
//If no undo value was set check if a revert was set
|
||||
if (null === $id) {
|
||||
$id = $request->get('revert');
|
||||
$mode = EventUndoHelper::MODE_REVERT;
|
||||
if (0 === $id) {
|
||||
$id = $request->request->getInt('revert');
|
||||
$mode = EventUndoMode::REVERT;
|
||||
}
|
||||
|
||||
if (0 === $id) {
|
||||
throw new InvalidArgumentException('No log entry ID was given!');
|
||||
}
|
||||
|
||||
$log_element = $this->entityManager->find(AbstractLogEntry::class, $id);
|
||||
if (null === $log_element) {
|
||||
if (!$log_element instanceof AbstractLogEntry) {
|
||||
throw new InvalidArgumentException('No log entry with the given ID is existing!');
|
||||
}
|
||||
|
||||
@@ -117,9 +152,9 @@ class LogController extends AbstractController
|
||||
$eventUndoHelper->setMode($mode);
|
||||
$eventUndoHelper->setUndoneEvent($log_element);
|
||||
|
||||
if (EventUndoHelper::MODE_UNDO === $mode) {
|
||||
if (EventUndoMode::UNDO === $mode) {
|
||||
$this->undoLog($log_element);
|
||||
} elseif (EventUndoHelper::MODE_REVERT === $mode) {
|
||||
} elseif (EventUndoMode::REVERT === $mode) {
|
||||
$this->revertLog($log_element);
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ use Doctrine\ORM\EntityManagerInterface;
|
||||
use Exception;
|
||||
use Omines\DataTablesBundle\DataTableFactory;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
|
||||
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
@@ -59,29 +60,19 @@ use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
use function Symfony\Component\Translation\t;
|
||||
|
||||
/**
|
||||
* @Route("/part")
|
||||
*/
|
||||
#[Route(path: '/part')]
|
||||
class PartController extends AbstractController
|
||||
{
|
||||
protected PricedetailHelper $pricedetailHelper;
|
||||
protected PartPreviewGenerator $partPreviewGenerator;
|
||||
protected EventCommentHelper $commentHelper;
|
||||
|
||||
public function __construct(PricedetailHelper $pricedetailHelper,
|
||||
PartPreviewGenerator $partPreviewGenerator, EventCommentHelper $commentHelper)
|
||||
public function __construct(protected PricedetailHelper $pricedetailHelper, protected PartPreviewGenerator $partPreviewGenerator, protected EventCommentHelper $commentHelper)
|
||||
{
|
||||
$this->pricedetailHelper = $pricedetailHelper;
|
||||
$this->partPreviewGenerator = $partPreviewGenerator;
|
||||
$this->commentHelper = $commentHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}/info/{timestamp}", name="part_info")
|
||||
* @Route("/{id}", requirements={"id"="\d+"})
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
#[Route(path: '/{id}/info/{timestamp}', name: 'part_info')]
|
||||
#[Route(path: '/{id}', requirements: ['id' => '\d+'])]
|
||||
public function show(Part $part, Request $request, TimeTravel $timeTravel, HistoryHelper $historyHelper,
|
||||
DataTableFactory $dataTable, ParameterExtractor $parameterExtractor, PartLotWithdrawAddHelper $withdrawAddHelper, ?string $timestamp = null): Response
|
||||
{
|
||||
@@ -129,9 +120,7 @@ class PartController extends AbstractController
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}/edit", name="part_edit")
|
||||
*/
|
||||
#[Route(path: '/{id}/edit', name: 'part_edit')]
|
||||
public function edit(Part $part, Request $request, EntityManagerInterface $em, TranslatorInterface $translator,
|
||||
AttachmentSubmitHandler $attachmentSubmitHandler): Response
|
||||
{
|
||||
@@ -182,21 +171,19 @@ class PartController extends AbstractController
|
||||
$this->addFlash('error', 'part.edited_flash.invalid');
|
||||
}
|
||||
|
||||
return $this->renderForm('parts/edit/edit_part_info.html.twig',
|
||||
return $this->render('parts/edit/edit_part_info.html.twig',
|
||||
[
|
||||
'part' => $part,
|
||||
'form' => $form,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}/delete", name="part_delete", methods={"DELETE"})
|
||||
*/
|
||||
#[Route(path: '/{id}/delete', name: 'part_delete', methods: ['DELETE'])]
|
||||
public function delete(Request $request, Part $part, EntityManagerInterface $entityManager): RedirectResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('delete', $part);
|
||||
|
||||
if ($this->isCsrfTokenValid('delete'.$part->getId(), $request->request->get('_token'))) {
|
||||
if ($this->isCsrfTokenValid('delete'.$part->getID(), $request->request->get('_token'))) {
|
||||
|
||||
$this->commentHelper->setMessage($request->request->get('log_comment', null));
|
||||
|
||||
@@ -212,23 +199,22 @@ class PartController extends AbstractController
|
||||
return $this->redirectToRoute('homepage');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/new", name="part_new")
|
||||
* @Route("/{id}/clone", name="part_clone")
|
||||
* @Route("/new_build_part/{project_id}", name="part_new_build_part")
|
||||
* @ParamConverter("part", options={"id" = "id"})
|
||||
* @ParamConverter("project", options={"id" = "project_id"})
|
||||
*/
|
||||
#[Route(path: '/new', name: 'part_new')]
|
||||
#[Route(path: '/{id}/clone', name: 'part_clone')]
|
||||
#[Route(path: '/new_build_part/{project_id}', name: 'part_new_build_part')]
|
||||
public function new(Request $request, EntityManagerInterface $em, TranslatorInterface $translator,
|
||||
AttachmentSubmitHandler $attachmentSubmitHandler, ProjectBuildPartHelper $projectBuildPartHelper,
|
||||
?Part $part = null, ?Project $project = null): Response
|
||||
#[MapEntity(mapping: ['id' => 'id'])] ?Part $part = null,
|
||||
#[MapEntity(mapping: ['id' => 'project_id'])] ?Project $project = null): Response
|
||||
{
|
||||
|
||||
if ($part) { //Clone part
|
||||
if ($part instanceof Part) {
|
||||
//Clone part
|
||||
$new_part = clone $part;
|
||||
} else if ($project) { //Initialize a new part for a build part from the given project
|
||||
} elseif ($project instanceof Project) {
|
||||
//Initialize a new part for a build part from the given project
|
||||
//Ensure that the project has not already a build part
|
||||
if ($project->getBuildPart() !== null) {
|
||||
if ($project->getBuildPart() instanceof Part) {
|
||||
$this->addFlash('error', 'part.new_build_part.error.build_part_already_exists');
|
||||
return $this->redirectToRoute('part_edit', ['id' => $project->getBuildPart()->getID()]);
|
||||
}
|
||||
@@ -241,7 +227,7 @@ class PartController extends AbstractController
|
||||
|
||||
$cid = $request->get('category', null);
|
||||
$category = $cid ? $em->find(Category::class, $cid) : null;
|
||||
if (null !== $category && null === $new_part->getCategory()) {
|
||||
if ($category instanceof Category && !$new_part->getCategory() instanceof Category) {
|
||||
$new_part->setCategory($category);
|
||||
$new_part->setDescription($category->getDefaultDescription());
|
||||
$new_part->setComment($category->getDefaultComment());
|
||||
@@ -249,19 +235,19 @@ class PartController extends AbstractController
|
||||
|
||||
$fid = $request->get('footprint', null);
|
||||
$footprint = $fid ? $em->find(Footprint::class, $fid) : null;
|
||||
if (null !== $footprint && null === $new_part->getFootprint()) {
|
||||
if ($footprint instanceof Footprint && !$new_part->getFootprint() instanceof Footprint) {
|
||||
$new_part->setFootprint($footprint);
|
||||
}
|
||||
|
||||
$mid = $request->get('manufacturer', null);
|
||||
$manufacturer = $mid ? $em->find(Manufacturer::class, $mid) : null;
|
||||
if (null !== $manufacturer && null === $new_part->getManufacturer()) {
|
||||
if ($manufacturer instanceof Manufacturer && !$new_part->getManufacturer() instanceof Manufacturer) {
|
||||
$new_part->setManufacturer($manufacturer);
|
||||
}
|
||||
|
||||
$store_id = $request->get('storelocation', null);
|
||||
$storelocation = $store_id ? $em->find(Storelocation::class, $store_id) : null;
|
||||
if (null !== $storelocation && $new_part->getPartLots()->isEmpty()) {
|
||||
if ($storelocation instanceof Storelocation && $new_part->getPartLots()->isEmpty()) {
|
||||
$partLot = new PartLot();
|
||||
$partLot->setStorageLocation($storelocation);
|
||||
$partLot->setInstockUnknown(true);
|
||||
@@ -270,7 +256,7 @@ class PartController extends AbstractController
|
||||
|
||||
$supplier_id = $request->get('supplier', null);
|
||||
$supplier = $supplier_id ? $em->find(Supplier::class, $supplier_id) : null;
|
||||
if (null !== $supplier && $new_part->getOrderdetails()->isEmpty()) {
|
||||
if ($supplier instanceof Supplier && $new_part->getOrderdetails()->isEmpty()) {
|
||||
$orderdetail = new Orderdetail();
|
||||
$orderdetail->setSupplier($supplier);
|
||||
$new_part->addOrderdetail($orderdetail);
|
||||
@@ -328,22 +314,20 @@ class PartController extends AbstractController
|
||||
$this->addFlash('error', 'part.created_flash.invalid');
|
||||
}
|
||||
|
||||
return $this->renderForm('parts/edit/new_part.html.twig',
|
||||
return $this->render('parts/edit/new_part.html.twig',
|
||||
[
|
||||
'part' => $new_part,
|
||||
'form' => $form,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}/add_withdraw", name="part_add_withdraw", methods={"POST"})
|
||||
*/
|
||||
#[Route(path: '/{id}/add_withdraw', name: 'part_add_withdraw', methods: ['POST'])]
|
||||
public function withdrawAddHandler(Part $part, Request $request, EntityManagerInterface $em, PartLotWithdrawAddHelper $withdrawAddHelper): Response
|
||||
{
|
||||
if ($this->isCsrfTokenValid('part_withraw' . $part->getID(), $request->request->get('_csfr'))) {
|
||||
//Retrieve partlot from the request
|
||||
$partLot = $em->find(PartLot::class, $request->request->get('lot_id'));
|
||||
if($partLot === null) {
|
||||
if(!$partLot instanceof PartLot) {
|
||||
throw new \RuntimeException('Part lot not found!');
|
||||
}
|
||||
//Ensure that the partlot belongs to the part
|
||||
@@ -383,7 +367,7 @@ class PartController extends AbstractController
|
||||
default:
|
||||
throw new \RuntimeException("Unknown action!");
|
||||
}
|
||||
} catch (AccessDeniedException $exception) {
|
||||
} catch (AccessDeniedException) {
|
||||
$this->addFlash('error', t('part.withdraw.access_denied'));
|
||||
goto err;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
@@ -17,7 +20,6 @@
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Parts\Part;
|
||||
@@ -36,23 +38,11 @@ use UnexpectedValueException;
|
||||
|
||||
class PartImportExportController extends AbstractController
|
||||
{
|
||||
private PartsTableActionHandler $partsTableActionHandler;
|
||||
private EntityImporter $entityImporter;
|
||||
private EventCommentHelper $commentHelper;
|
||||
|
||||
public function __construct(PartsTableActionHandler $partsTableActionHandler,
|
||||
EntityImporter $entityImporter, EventCommentHelper $commentHelper)
|
||||
public function __construct(private readonly PartsTableActionHandler $partsTableActionHandler, private readonly EntityImporter $entityImporter, private readonly EventCommentHelper $commentHelper)
|
||||
{
|
||||
$this->partsTableActionHandler = $partsTableActionHandler;
|
||||
$this->entityImporter = $entityImporter;
|
||||
$this->commentHelper = $commentHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/parts/import", name="parts_import")
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
#[Route(path: '/parts/import', name: 'parts_import')]
|
||||
public function importParts(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('@parts.import');
|
||||
@@ -109,23 +99,20 @@ class PartImportExportController extends AbstractController
|
||||
|
||||
|
||||
ret:
|
||||
return $this->renderForm('parts/import/parts_import.html.twig', [
|
||||
return $this->render('parts/import/parts_import.html.twig', [
|
||||
'import_form' => $import_form,
|
||||
'imported_entities' => $entities ?? [],
|
||||
'import_errors' => $errors ?? [],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/parts/export", name="parts_export", methods={"GET"})
|
||||
* @return Response
|
||||
*/
|
||||
#[Route(path: '/parts/export', name: 'parts_export', methods: ['GET'])]
|
||||
public function exportParts(Request $request, EntityExporter $entityExporter): Response
|
||||
{
|
||||
$ids = $request->query->get('ids', '');
|
||||
$parts = $this->partsTableActionHandler->idStringToArray($ids);
|
||||
|
||||
if (empty($parts)) {
|
||||
if ($parts === []) {
|
||||
throw new \RuntimeException('No parts found!');
|
||||
}
|
||||
|
||||
@@ -136,4 +123,4 @@ class PartImportExportController extends AbstractController
|
||||
|
||||
return $entityExporter->exportEntityFromRequest($parts, $request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\DataTables\ErrorDataTable;
|
||||
use App\DataTables\Filters\PartFilter;
|
||||
use App\DataTables\Filters\PartSearchFilter;
|
||||
use App\DataTables\PartsDataTable;
|
||||
@@ -30,9 +31,11 @@ use App\Entity\Parts\Footprint;
|
||||
use App\Entity\Parts\Manufacturer;
|
||||
use App\Entity\Parts\Storelocation;
|
||||
use App\Entity\Parts\Supplier;
|
||||
use App\Exceptions\InvalidRegexException;
|
||||
use App\Form\Filters\PartFilterType;
|
||||
use App\Services\Parts\PartsTableActionHandler;
|
||||
use App\Services\Trees\NodesListBuilder;
|
||||
use Doctrine\DBAL\Exception\DriverException;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Omines\DataTablesBundle\DataTableFactory;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
@@ -41,23 +44,15 @@ use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class PartListsController extends AbstractController
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
private NodesListBuilder $nodesListBuilder;
|
||||
private DataTableFactory $dataTableFactory;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager, NodesListBuilder $nodesListBuilder, DataTableFactory $dataTableFactory)
|
||||
public function __construct(private readonly EntityManagerInterface $entityManager, private readonly NodesListBuilder $nodesListBuilder, private readonly DataTableFactory $dataTableFactory, private readonly TranslatorInterface $translator)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
$this->nodesListBuilder = $nodesListBuilder;
|
||||
$this->dataTableFactory = $dataTableFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/table/action", name="table_action", methods={"POST"})
|
||||
*/
|
||||
#[Route(path: '/table/action', name: 'table_action', methods: ['POST'])]
|
||||
public function tableAction(Request $request, PartsTableActionHandler $actionHandler): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('@parts.edit');
|
||||
@@ -95,8 +90,6 @@ class PartListsController extends AbstractController
|
||||
|
||||
/**
|
||||
* Disable the given form interface after creation of the form by removing and reattaching the form.
|
||||
* @param FormInterface $form
|
||||
* @return void
|
||||
*/
|
||||
private function disableFormFieldAfterCreation(FormInterface $form, bool $disabled = true): void
|
||||
{
|
||||
@@ -104,12 +97,12 @@ class PartListsController extends AbstractController
|
||||
$attrs['disabled'] = $disabled;
|
||||
|
||||
$parent = $form->getParent();
|
||||
if ($parent === null) {
|
||||
if (!$parent instanceof FormInterface) {
|
||||
throw new \RuntimeException('This function can only be used on form fields that are children of another form!');
|
||||
}
|
||||
|
||||
$parent->remove($form->getName());
|
||||
$parent->add($form->getName(), get_class($form->getConfig()->getType()->getInnerType()), $attrs);
|
||||
$parent->add($form->getName(), $form->getConfig()->getType()->getInnerType()::class, $attrs);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,7 +113,6 @@ class PartListsController extends AbstractController
|
||||
* @param callable|null $form_changer A function that is called with the form object as parameter. This function can be used to customize the form
|
||||
* @param array $additonal_template_vars Any additional template variables that should be passed to the template
|
||||
* @param array $additional_table_vars Any additional variables that should be passed to the table creation
|
||||
* @return Response
|
||||
*/
|
||||
protected function showListWithFilter(Request $request, string $template, ?callable $filter_changer = null, ?callable $form_changer = null, array $additonal_template_vars = [], array $additional_table_vars = []): Response
|
||||
{
|
||||
@@ -144,7 +136,21 @@ class PartListsController extends AbstractController
|
||||
->handleRequest($request);
|
||||
|
||||
if ($table->isCallback()) {
|
||||
return $table->getResponse();
|
||||
try {
|
||||
try {
|
||||
return $table->getResponse();
|
||||
} catch (DriverException $driverException) {
|
||||
if ($driverException->getCode() === 1139) {
|
||||
//Convert the driver exception to InvalidRegexException so it has the same hanlder as for SQLite
|
||||
throw InvalidRegexException::fromDriverException($driverException);
|
||||
} else {
|
||||
throw $driverException;
|
||||
}
|
||||
}
|
||||
} catch (InvalidRegexException $exception) {
|
||||
$errors = $this->translator->trans('part.table.invalid_regex').': '.$exception->getReason();
|
||||
return ErrorDataTable::errorTable($this->dataTableFactory, $request, $errors);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render($template, array_merge([
|
||||
@@ -153,11 +159,7 @@ class PartListsController extends AbstractController
|
||||
], $additonal_template_vars));
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/category/{id}/parts", name="part_list_category")
|
||||
*
|
||||
* @return JsonResponse|Response
|
||||
*/
|
||||
#[Route(path: '/category/{id}/parts', name: 'part_list_category')]
|
||||
public function showCategory(Category $category, Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('@categories.read');
|
||||
@@ -165,7 +167,7 @@ class PartListsController extends AbstractController
|
||||
return $this->showListWithFilter($request,
|
||||
'parts/lists/category_list.html.twig',
|
||||
function (PartFilter $filter) use ($category) {
|
||||
$filter->getCategory()->setOperator('INCLUDING_CHILDREN')->setValue($category);
|
||||
$filter->category->setOperator('INCLUDING_CHILDREN')->setValue($category);
|
||||
}, function (FormInterface $filterForm) {
|
||||
$this->disableFormFieldAfterCreation($filterForm->get('category')->get('value'));
|
||||
}, [
|
||||
@@ -175,11 +177,7 @@ class PartListsController extends AbstractController
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/footprint/{id}/parts", name="part_list_footprint")
|
||||
*
|
||||
* @return JsonResponse|Response
|
||||
*/
|
||||
#[Route(path: '/footprint/{id}/parts', name: 'part_list_footprint')]
|
||||
public function showFootprint(Footprint $footprint, Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('@footprints.read');
|
||||
@@ -187,7 +185,7 @@ class PartListsController extends AbstractController
|
||||
return $this->showListWithFilter($request,
|
||||
'parts/lists/footprint_list.html.twig',
|
||||
function (PartFilter $filter) use ($footprint) {
|
||||
$filter->getFootprint()->setOperator('INCLUDING_CHILDREN')->setValue($footprint);
|
||||
$filter->footprint->setOperator('INCLUDING_CHILDREN')->setValue($footprint);
|
||||
}, function (FormInterface $filterForm) {
|
||||
$this->disableFormFieldAfterCreation($filterForm->get('footprint')->get('value'));
|
||||
}, [
|
||||
@@ -197,11 +195,7 @@ class PartListsController extends AbstractController
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/manufacturer/{id}/parts", name="part_list_manufacturer")
|
||||
*
|
||||
* @return JsonResponse|Response
|
||||
*/
|
||||
#[Route(path: '/manufacturer/{id}/parts', name: 'part_list_manufacturer')]
|
||||
public function showManufacturer(Manufacturer $manufacturer, Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('@manufacturers.read');
|
||||
@@ -209,7 +203,7 @@ class PartListsController extends AbstractController
|
||||
return $this->showListWithFilter($request,
|
||||
'parts/lists/manufacturer_list.html.twig',
|
||||
function (PartFilter $filter) use ($manufacturer) {
|
||||
$filter->getManufacturer()->setOperator('INCLUDING_CHILDREN')->setValue($manufacturer);
|
||||
$filter->manufacturer->setOperator('INCLUDING_CHILDREN')->setValue($manufacturer);
|
||||
}, function (FormInterface $filterForm) {
|
||||
$this->disableFormFieldAfterCreation($filterForm->get('manufacturer')->get('value'));
|
||||
}, [
|
||||
@@ -219,11 +213,7 @@ class PartListsController extends AbstractController
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/store_location/{id}/parts", name="part_list_store_location")
|
||||
*
|
||||
* @return JsonResponse|Response
|
||||
*/
|
||||
#[Route(path: '/store_location/{id}/parts', name: 'part_list_store_location')]
|
||||
public function showStorelocation(Storelocation $storelocation, Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('@storelocations.read');
|
||||
@@ -231,7 +221,7 @@ class PartListsController extends AbstractController
|
||||
return $this->showListWithFilter($request,
|
||||
'parts/lists/store_location_list.html.twig',
|
||||
function (PartFilter $filter) use ($storelocation) {
|
||||
$filter->getStorelocation()->setOperator('INCLUDING_CHILDREN')->setValue($storelocation);
|
||||
$filter->storelocation->setOperator('INCLUDING_CHILDREN')->setValue($storelocation);
|
||||
}, function (FormInterface $filterForm) {
|
||||
$this->disableFormFieldAfterCreation($filterForm->get('storelocation')->get('value'));
|
||||
}, [
|
||||
@@ -241,11 +231,7 @@ class PartListsController extends AbstractController
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/supplier/{id}/parts", name="part_list_supplier")
|
||||
*
|
||||
* @return JsonResponse|Response
|
||||
*/
|
||||
#[Route(path: '/supplier/{id}/parts', name: 'part_list_supplier')]
|
||||
public function showSupplier(Supplier $supplier, Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('@suppliers.read');
|
||||
@@ -253,7 +239,7 @@ class PartListsController extends AbstractController
|
||||
return $this->showListWithFilter($request,
|
||||
'parts/lists/supplier_list.html.twig',
|
||||
function (PartFilter $filter) use ($supplier) {
|
||||
$filter->getSupplier()->setOperator('INCLUDING_CHILDREN')->setValue($supplier);
|
||||
$filter->supplier->setOperator('INCLUDING_CHILDREN')->setValue($supplier);
|
||||
}, function (FormInterface $filterForm) {
|
||||
$this->disableFormFieldAfterCreation($filterForm->get('supplier')->get('value'));
|
||||
}, [
|
||||
@@ -263,11 +249,7 @@ class PartListsController extends AbstractController
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/parts/by_tag/{tag}", name="part_list_tags", requirements={"tag": ".*"})
|
||||
*
|
||||
* @return JsonResponse|Response
|
||||
*/
|
||||
#[Route(path: '/parts/by_tag/{tag}', name: 'part_list_tags', requirements: ['tag' => '.*'])]
|
||||
public function showTag(string $tag, Request $request): Response
|
||||
{
|
||||
$tag = trim($tag);
|
||||
@@ -275,7 +257,7 @@ class PartListsController extends AbstractController
|
||||
return $this->showListWithFilter($request,
|
||||
'parts/lists/tags_list.html.twig',
|
||||
function (PartFilter $filter) use ($tag) {
|
||||
$filter->getTags()->setOperator('ANY')->setValue($tag);
|
||||
$filter->tags->setOperator('ANY')->setValue($tag);
|
||||
}, function (FormInterface $filterForm) {
|
||||
$this->disableFormFieldAfterCreation($filterForm->get('tags')->get('value'));
|
||||
}, [
|
||||
@@ -288,30 +270,27 @@ class PartListsController extends AbstractController
|
||||
{
|
||||
$filter = new PartSearchFilter($request->query->get('keyword', ''));
|
||||
|
||||
$filter->setName($request->query->getBoolean('name', true));
|
||||
$filter->setCategory($request->query->getBoolean('category', true));
|
||||
$filter->setDescription($request->query->getBoolean('description', true));
|
||||
$filter->setMpn($request->query->getBoolean('mpn', true));
|
||||
$filter->setTags($request->query->getBoolean('tags', true));
|
||||
$filter->setStorelocation($request->query->getBoolean('storelocation', true));
|
||||
$filter->setComment($request->query->getBoolean('comment', true));
|
||||
$filter->setIPN($request->query->getBoolean('ipn', true));
|
||||
$filter->setOrdernr($request->query->getBoolean('ordernr', true));
|
||||
$filter->setSupplier($request->query->getBoolean('supplier', false));
|
||||
$filter->setManufacturer($request->query->getBoolean('manufacturer', false));
|
||||
$filter->setFootprint($request->query->getBoolean('footprint', false));
|
||||
//As an unchecked checkbox is not set in the query, the default value for all bools have to be false (which is the default argument value)!
|
||||
$filter->setName($request->query->getBoolean('name'));
|
||||
$filter->setCategory($request->query->getBoolean('category'));
|
||||
$filter->setDescription($request->query->getBoolean('description'));
|
||||
$filter->setMpn($request->query->getBoolean('mpn'));
|
||||
$filter->setTags($request->query->getBoolean('tags'));
|
||||
$filter->setStorelocation($request->query->getBoolean('storelocation'));
|
||||
$filter->setComment($request->query->getBoolean('comment'));
|
||||
$filter->setIPN($request->query->getBoolean('ipn'));
|
||||
$filter->setOrdernr($request->query->getBoolean('ordernr'));
|
||||
$filter->setSupplier($request->query->getBoolean('supplier'));
|
||||
$filter->setManufacturer($request->query->getBoolean('manufacturer'));
|
||||
$filter->setFootprint($request->query->getBoolean('footprint'));
|
||||
|
||||
|
||||
$filter->setRegex($request->query->getBoolean('regex', false));
|
||||
$filter->setRegex($request->query->getBoolean('regex'));
|
||||
|
||||
return $filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/parts/search", name="parts_search")
|
||||
*
|
||||
* @return JsonResponse|Response
|
||||
*/
|
||||
#[Route(path: '/parts/search', name: 'parts_search')]
|
||||
public function showSearch(Request $request, DataTableFactory $dataTable): Response
|
||||
{
|
||||
$searchFilter = $this->searchRequestToFilter($request);
|
||||
@@ -330,11 +309,7 @@ class PartListsController extends AbstractController
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/parts", name="parts_show_all")
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
#[Route(path: '/parts', name: 'parts_show_all')]
|
||||
public function showAll(Request $request): Response
|
||||
{
|
||||
return $this->showListWithFilter($request,'parts/lists/all_list.html.twig');
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
@@ -17,28 +20,32 @@
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\DataTables\ProjectBomEntriesDataTable;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Entity\ProjectSystem\Project;
|
||||
use App\Entity\ProjectSystem\ProjectBOMEntry;
|
||||
use App\Form\ProjectSystem\ProjectAddPartsType;
|
||||
use App\Form\ProjectSystem\ProjectBOMEntryCollectionType;
|
||||
use App\Form\ProjectSystem\ProjectBuildType;
|
||||
use App\Form\Type\StructuralEntityType;
|
||||
use App\Helpers\Projects\ProjectBuildRequest;
|
||||
use App\Services\ImportExportSystem\BOMImporter;
|
||||
use App\Services\ProjectSystem\ProjectBuildHelper;
|
||||
use App\Validator\Constraints\UniqueObjectCollection;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use League\Csv\SyntaxError;
|
||||
use Omines\DataTablesBundle\DataTableFactory;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntityValidator;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FileType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
@@ -47,21 +54,14 @@ use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
use function Symfony\Component\Translation\t;
|
||||
|
||||
/**
|
||||
* @Route("/project")
|
||||
*/
|
||||
#[Route(path: '/project')]
|
||||
class ProjectController extends AbstractController
|
||||
{
|
||||
private DataTableFactory $dataTableFactory;
|
||||
|
||||
public function __construct(DataTableFactory $dataTableFactory)
|
||||
public function __construct(private readonly DataTableFactory $dataTableFactory)
|
||||
{
|
||||
$this->dataTableFactory = $dataTableFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}/info", name="project_info", requirements={"id"="\d+"})
|
||||
*/
|
||||
#[Route(path: '/{id}/info', name: 'project_info', requirements: ['id' => '\d+'])]
|
||||
public function info(Project $project, Request $request, ProjectBuildHelper $buildHelper): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('read', $project);
|
||||
@@ -80,9 +80,7 @@ class ProjectController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}/build", name="project_build", requirements={"id"="\d+"})
|
||||
*/
|
||||
#[Route(path: '/{id}/build', name: 'project_build', requirements: ['id' => '\d+'])]
|
||||
public function build(Project $project, Request $request, ProjectBuildHelper $buildHelper, EntityManagerInterface $entityManager): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('read', $project);
|
||||
@@ -117,7 +115,7 @@ class ProjectController extends AbstractController
|
||||
$this->addFlash('error', 'project.build.flash.invalid_input');
|
||||
}
|
||||
|
||||
return $this->renderForm('projects/build/build.html.twig', [
|
||||
return $this->render('projects/build/build.html.twig', [
|
||||
'buildHelper' => $buildHelper,
|
||||
'project' => $project,
|
||||
'build_request' => $projectBuildRequest,
|
||||
@@ -126,9 +124,7 @@ class ProjectController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}/import_bom", name="project_import_bom", requirements={"id"="\d+"})
|
||||
*/
|
||||
#[Route(path: '/{id}/import_bom', name: 'project_import_bom', requirements: ['id' => '\d+'])]
|
||||
public function importBOM(Request $request, EntityManagerInterface $entityManager, Project $project,
|
||||
BOMImporter $BOMImporter, ValidatorInterface $validator): Response
|
||||
{
|
||||
@@ -185,54 +181,39 @@ class ProjectController extends AbstractController
|
||||
return $this->redirectToRoute('project_edit', ['id' => $project->getID()]);
|
||||
}
|
||||
|
||||
if (count ($errors) > 0) {
|
||||
$this->addFlash('error', t('project.bom_import.flash.invalid_entries'));
|
||||
}
|
||||
} catch (\UnexpectedValueException $e) {
|
||||
$this->addFlash('error', t('project.bom_import.flash.invalid_file', ['%message%' => $e->getMessage()]));
|
||||
} catch (SyntaxError $e) {
|
||||
//When we get here, there were validation errors
|
||||
$this->addFlash('error', t('project.bom_import.flash.invalid_entries'));
|
||||
|
||||
} catch (\UnexpectedValueException|SyntaxError $e) {
|
||||
$this->addFlash('error', t('project.bom_import.flash.invalid_file', ['%message%' => $e->getMessage()]));
|
||||
}
|
||||
}
|
||||
|
||||
return $this->renderForm('projects/import_bom.html.twig', [
|
||||
return $this->render('projects/import_bom.html.twig', [
|
||||
'project' => $project,
|
||||
'form' => $form,
|
||||
'errors' => $errors ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/add_parts", name="project_add_parts_no_id")
|
||||
* @Route("/{id}/add_parts", name="project_add_parts", requirements={"id"="\d+"})
|
||||
* @param Request $request
|
||||
* @param Project|null $project
|
||||
*/
|
||||
#[Route(path: '/add_parts', name: 'project_add_parts_no_id')]
|
||||
#[Route(path: '/{id}/add_parts', name: 'project_add_parts', requirements: ['id' => '\d+'])]
|
||||
public function addPart(Request $request, EntityManagerInterface $entityManager, ?Project $project): Response
|
||||
{
|
||||
if($project) {
|
||||
if($project instanceof Project) {
|
||||
$this->denyAccessUnlessGranted('edit', $project);
|
||||
} else {
|
||||
$this->denyAccessUnlessGranted('@projects.edit');
|
||||
}
|
||||
|
||||
$builder = $this->createFormBuilder();
|
||||
$builder->add('project', StructuralEntityType::class, [
|
||||
'class' => Project::class,
|
||||
'required' => true,
|
||||
'disabled' => $project !== null, //If a project is given, disable the field
|
||||
'data' => $project,
|
||||
'constraints' => [
|
||||
new NotNull()
|
||||
]
|
||||
$form = $this->createForm(ProjectAddPartsType::class, null, [
|
||||
'project' => $project,
|
||||
]);
|
||||
$builder->add('bom_entries', ProjectBOMEntryCollectionType::class);
|
||||
$builder->add('submit', SubmitType::class, ['label' => 'save']);
|
||||
$form = $builder->getForm();
|
||||
|
||||
|
||||
//Preset the BOM entries with the selected parts, when the form was not submitted yet
|
||||
$preset_data = new ArrayCollection();
|
||||
foreach (explode(',', $request->get('parts', '')) as $part_id) {
|
||||
foreach (explode(',', (string) $request->get('parts', '')) as $part_id) {
|
||||
$part = $entityManager->getRepository(Part::class)->find($part_id);
|
||||
if (null !== $part) {
|
||||
//If there is already a BOM entry for this part, we use this one (we edit it then)
|
||||
@@ -264,8 +245,11 @@ class ProjectController extends AbstractController
|
||||
foreach ($bom_entries as $bom_entry){
|
||||
$target_project->addBOMEntry($bom_entry);
|
||||
}
|
||||
|
||||
|
||||
$entityManager->flush();
|
||||
|
||||
|
||||
//If a redirect query parameter is set, redirect to this page
|
||||
if ($request->query->get('_redirect')) {
|
||||
return $this->redirect($request->query->get('_redirect'));
|
||||
@@ -274,9 +258,9 @@ class ProjectController extends AbstractController
|
||||
return $this->redirectToRoute('project_info', ['id' => $target_project->getID()]);
|
||||
}
|
||||
|
||||
return $this->renderForm('projects/add_parts.html.twig', [
|
||||
return $this->render('projects/add_parts.html.twig', [
|
||||
'project' => $project,
|
||||
'form' => $form,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user