Merge branch 'darkmode-migration'

This commit is contained in:
Jan Böhmer 2023-06-26 23:02:12 +02:00
commit 6fd79688b0
552 changed files with 11575 additions and 13425 deletions

View file

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

View file

@ -1 +1 @@
1.4.2
1.5.0-dev

3
assets/bootstrap.js vendored
View file

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

View file

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

View file

@ -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,
});

View file

@ -97,7 +97,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 +123,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});

View file

@ -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} */

View file

@ -99,7 +99,7 @@ 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;

View file

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

View file

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

View file

@ -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; */

View file

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

View file

@ -18,6 +18,24 @@
*/
.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);
}

View file

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

View file

@ -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'});
});

View file

@ -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)
),
};

View file

@ -2,7 +2,7 @@
"type": "project",
"license": "AGPL-3.0-or-later",
"require": {
"php": "^7.4 || ^8.0",
"php": "^8.1",
"ext-ctype": "*",
"ext-dom": "*",
"ext-gd": "*",
@ -11,9 +11,9 @@
"ext-json": "*",
"ext-mbstring": "*",
"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",
@ -24,54 +24,53 @@
"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",
"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",
"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-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",
@ -79,28 +78,30 @@
"twig/inky-extra": "^3.0",
"twig/intl-extra": "^3.0",
"twig/markdown-extra": "^3.0",
"web-auth/webauthn-symfony-bundle": "^3.3",
"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 +112,7 @@
"*": "dist"
},
"platform": {
"php": "7.4.0"
"php": "8.1.0"
},
"sort-packages": true,
"allow-plugins": {
@ -143,7 +144,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 +152,7 @@
"extra": {
"symfony": {
"allow-contrib": false,
"require": "5.4.*"
"require": "6.3.*"
}
}
},
"repositories": [
]
}

5213
composer.lock generated

File diff suppressed because it is too large Load diff

View file

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

View file

@ -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,6 @@ 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],
];

View file

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

View file

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

View file

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

View 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

View file

@ -1,2 +0,0 @@
framework:
lock: '%env(LOCK_DSN)%'

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

View file

@ -1,4 +1,4 @@
# 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:
@ -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
# 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
# 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

View file

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

View file

@ -1,3 +0,0 @@
sensio_framework_extra:
router:
annotations: false

View file

@ -1,4 +1,2 @@
framework:
test: true
session:
storage_id: session.storage.mock_file

4
config/packages/uid.yaml Normal file
View file

@ -0,0 +1,4 @@
framework:
uid:
default_uuid_version: 7
time_based_uuid_version: 7

View file

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

View file

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

View file

@ -1,7 +1,3 @@
#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:

View file

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

View file

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

View file

@ -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:
@ -88,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:
@ -97,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%'
@ -183,7 +177,7 @@ services:
App\EventSubscriber\UserSystem\SetUserTimezoneSubscriber:
arguments:
$timezone: '%partdb.timezone%'
$default_timezone: '%partdb.timezone%'
App\Controller\SecurityController:
arguments:
@ -265,7 +259,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
@ -289,3 +283,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' }

View file

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

View file

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

View 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)');
}
}

View file

@ -28,45 +28,44 @@
"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",
"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",
@ -80,7 +79,9 @@
"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",

View file

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

View file

@ -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
View 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,
]);
};

View file

@ -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,29 +41,20 @@ 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.'.
$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.');
}
@ -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;
}
/**

View file

@ -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,14 +67,11 @@ 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)) {
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');

View file

@ -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.');
}
}

View file

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

View file

@ -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,15 +138,13 @@ class ShowEventLogCommand extends Command
$target_class = $this->elementTypeNameGenerator->getLocalizedTypeLabel($entry->getTargetClass());
}
if ($entry->getUser()) {
if ($entry->getUser() instanceof User) {
$user = $entry->getUser()->getFullName(true);
} else {
if ($entry->isCLIEntry()) {
} elseif ($entry->isCLIEntry()) {
$user = $entry->getCLIUsername() . ' [CLI]';
} else {
$user = $entry->getUsername() . ' [deleted]';
}
}
$row = [
$entry->getID(),

View file

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

View file

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

View file

@ -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();

View file

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

View file

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

View file

@ -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!');

View file

@ -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',
]);
}

View file

@ -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)</>';
}

View file

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

View file

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

View file

@ -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,9 +258,8 @@ abstract class BaseAdminController extends AbstractController
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
//Perform additional actions
if ($this->additionalActionNew($form, $new_entity)) {
if ($form->isSubmitted() && $form->isValid() && $this->additionalActionNew($form, $new_entity)) {
//Upload passed files
$attachments = $form['attachments'];
foreach ($attachments as $attachment) {
@ -314,16 +284,12 @@ abstract class BaseAdminController extends AbstractController
);
}
}
$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()) {
$this->addFlash('error', 'entity.created_flash.invalid');
@ -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;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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,
]);
}
}

View file

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

View file

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

View file

@ -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);
@ -146,7 +128,7 @@ class LabelController extends AbstractController
}
}
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 +144,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);
}

View file

@ -34,6 +34,7 @@ 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;
@ -49,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');
@ -93,17 +84,12 @@ class LogController extends AbstractController
return $this->render('log_system/log_list.html.twig', [
'datatable' => $table,
'filterForm' => $filterForm->createView(),
'filterForm' => $filterForm,
]);
}
/**
* @Route("/{id}/details", name="log_details")
* @param Request $request
* @param AbstractLogEntry $logEntry
* @return Response
*/
public function logDetails(Request $request, AbstractLogEntry $logEntry, LogEntryExtraFormatter $logEntryExtraFormatter,
#[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);
@ -123,14 +109,12 @@ class LogController extends AbstractController
]);
}
/**
* @Route("/{id}/delete", name="log_delete", methods={"DELETE"})
*/
#[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'))) {
if ($this->isCsrfTokenValid('delete'.$logEntry->getID(), $request->request->get('_token'))) {
//Remove part
$entityManager->remove($logEntry);
//Flush changes
@ -142,22 +126,24 @@ class LogController extends AbstractController
}
/**
* @Route("/undo", name="log_undo", methods={"POST"})
*/
#[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!');
}
@ -166,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);
}

View file

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

View file

@ -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!');
}

View file

@ -48,23 +48,11 @@ use Symfony\Contracts\Translation\TranslatorInterface;
class PartListsController extends AbstractController
{
private EntityManagerInterface $entityManager;
private NodesListBuilder $nodesListBuilder;
private DataTableFactory $dataTableFactory;
private TranslatorInterface $translator;
public function __construct(EntityManagerInterface $entityManager, NodesListBuilder $nodesListBuilder, DataTableFactory $dataTableFactory, TranslatorInterface $translator)
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;
$this->translator = $translator;
}
/**
* @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');
@ -102,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
{
@ -111,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);
}
/**
@ -127,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
{
@ -174,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');
@ -186,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'));
}, [
@ -196,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');
@ -208,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'));
}, [
@ -218,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');
@ -230,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'));
}, [
@ -240,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');
@ -252,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'));
}, [
@ -262,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');
@ -274,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'));
}, [
@ -284,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);
@ -296,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'));
}, [
@ -329,11 +290,7 @@ class PartListsController extends AbstractController
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);
@ -352,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');

View file

@ -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\DataTables\ProjectBomEntriesDataTable;
@ -47,21 +49,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 +75,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 +110,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 +119,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,32 +176,26 @@ class ProjectController extends AbstractController
return $this->redirectToRoute('project_edit', ['id' => $project->getID()]);
}
if (count ($errors) > 0) {
//When we get here, there were validation errors
$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) {
} 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');
@ -220,7 +205,7 @@ class ProjectController extends AbstractController
$builder->add('project', StructuralEntityType::class, [
'class' => Project::class,
'required' => true,
'disabled' => $project !== null, //If a project is given, disable the field
'disabled' => $project instanceof Project, //If a project is given, disable the field
'data' => $project,
'constraints' => [
new NotNull()
@ -232,7 +217,7 @@ class ProjectController extends AbstractController
//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)
@ -274,7 +259,7 @@ 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,
]);

View file

@ -30,17 +30,13 @@ use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @see \App\Tests\Controller\RedirectControllerTest
*/
class RedirectController extends AbstractController
{
protected string $default_locale;
protected TranslatorInterface $translator;
protected bool $enforce_index_php;
public function __construct(string $default_locale, TranslatorInterface $translator, bool $enforce_index_php)
public function __construct(protected string $default_locale, protected TranslatorInterface $translator, protected bool $enforce_index_php)
{
$this->default_locale = $default_locale;
$this->translator = $translator;
$this->enforce_index_php = $enforce_index_php;
}
/**
@ -54,7 +50,7 @@ class RedirectController extends AbstractController
//Check if a user has set a preferred language setting:
$user = $this->getUser();
if (($user instanceof User) && !empty($user->getLanguage())) {
if (($user instanceof User) && ($user->getLanguage() !== null && $user->getLanguage() !== '')) {
$locale = $user->getLanguage();
}
@ -62,7 +58,7 @@ class RedirectController extends AbstractController
//If either mod_rewrite is not enabled or the index.php version is enforced, add index.php to the string
if (($this->enforce_index_php || !$this->checkIfModRewriteAvailable())
&& false === strpos($new_url, 'index.php')) {
&& !str_contains((string) $new_url, 'index.php')) {
//Like Request::getUriForPath only with index.php
$new_url = $request->getSchemeAndHttpHost().$request->getBaseUrl().'/index.php/'.$locale.$request->getPathInfo();
}
@ -87,6 +83,6 @@ class RedirectController extends AbstractController
}
//Check if the mod_rewrite module is loaded
return in_array('mod_rewrite', apache_get_modules(), false);
return in_array('mod_rewrite', apache_get_modules(), true);
}
}

View file

@ -51,23 +51,14 @@ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/scan")
*/
#[Route(path: '/scan')]
class ScanController extends AbstractController
{
protected BarcodeRedirector $barcodeParser;
protected BarcodeNormalizer $barcodeNormalizer;
public function __construct(BarcodeRedirector $barcodeParser, BarcodeNormalizer $barcodeNormalizer)
public function __construct(protected BarcodeRedirector $barcodeParser, protected BarcodeNormalizer $barcodeNormalizer)
{
$this->barcodeParser = $barcodeParser;
$this->barcodeNormalizer = $barcodeNormalizer;
}
/**
* @Route("", name="scan_dialog")
*/
#[Route(path: '', name: 'scan_dialog')]
public function dialog(Request $request): Response
{
$this->denyAccessUnlessGranted('@tools.label_scanner');
@ -83,15 +74,15 @@ class ScanController extends AbstractController
try {
return $this->redirect($this->barcodeParser->getRedirectURL($type, $id));
} catch (EntityNotFoundException $exception) {
} catch (EntityNotFoundException) {
$this->addFlash('success', 'scan.qr_not_found');
}
} catch (InvalidArgumentException $exception) {
} catch (InvalidArgumentException) {
$this->addFlash('error', 'scan.format_unknown');
}
}
return $this->renderForm('label_system/scanner/scanner.html.twig', [
return $this->render('label_system/scanner/scanner.html.twig', [
'form' => $form,
]);
}
@ -105,7 +96,7 @@ class ScanController extends AbstractController
$this->addFlash('success', 'scan.qr_success');
return $this->redirect($this->barcodeParser->getRedirectURL($type, $id));
} catch (EntityNotFoundException $exception) {
} catch (EntityNotFoundException) {
$this->addFlash('success', 'scan.qr_not_found');
return $this->redirectToRoute('homepage');

View file

@ -48,18 +48,11 @@ use Symfony\Contracts\Translation\TranslatorInterface;
class SecurityController extends AbstractController
{
protected TranslatorInterface $translator;
protected bool $allow_email_pw_reset;
public function __construct(TranslatorInterface $translator, bool $allow_email_pw_reset)
public function __construct(protected TranslatorInterface $translator, protected bool $allow_email_pw_reset)
{
$this->translator = $translator;
$this->allow_email_pw_reset = $allow_email_pw_reset;
}
/**
* @Route("/login", name="login", methods={"GET", "POST"})
*/
#[Route(path: '/login', name: 'login', methods: ['GET', 'POST'])]
public function login(AuthenticationUtils $authenticationUtils): Response
{
// get the login error if there is one
@ -75,11 +68,10 @@ class SecurityController extends AbstractController
}
/**
* @Route("/pw_reset/request", name="pw_reset_request")
*
* @return RedirectResponse|Response
*/
public function requestPwReset(PasswordResetManager $passwordReset, Request $request)
#[Route(path: '/pw_reset/request', name: 'pw_reset_request')]
public function requestPwReset(PasswordResetManager $passwordReset, Request $request): RedirectResponse|Response
{
if (!$this->allow_email_pw_reset) {
throw new AccessDeniedHttpException('The password reset via email is disabled!');
@ -113,17 +105,16 @@ class SecurityController extends AbstractController
return $this->redirectToRoute('login');
}
return $this->renderForm('security/pw_reset_request.html.twig', [
return $this->render('security/pw_reset_request.html.twig', [
'form' => $form,
]);
}
/**
* @Route("/pw_reset/new_pw/{user}/{token}", name="pw_reset_new_pw")
*
* @return RedirectResponse|Response
*/
public function pwResetNewPw(PasswordResetManager $passwordReset, Request $request, EntityManagerInterface $em, EventDispatcherInterface $eventDispatcher, ?string $user = null, ?string $token = null)
#[Route(path: '/pw_reset/new_pw/{user}/{token}', name: 'pw_reset_new_pw')]
public function pwResetNewPw(PasswordResetManager $passwordReset, Request $request, EntityManagerInterface $em, EventDispatcherInterface $eventDispatcher, ?string $user = null, ?string $token = null): RedirectResponse|Response
{
if (!$this->allow_email_pw_reset) {
throw new AccessDeniedHttpException('The password reset via email is disabled!');
@ -187,15 +178,13 @@ class SecurityController extends AbstractController
}
}
return $this->renderForm('security/pw_reset_new_pw.html.twig', [
return $this->render('security/pw_reset_new_pw.html.twig', [
'form' => $form,
]);
}
/**
* @Route("/logout", name="logout")
*/
public function logout(): void
#[Route(path: '/logout', name: 'logout')]
public function logout(): never
{
throw new RuntimeException('Will be intercepted before getting here');
}

View file

@ -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\Base\AbstractNamedDBElement;
@ -37,66 +39,46 @@ use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @Route("/select_api")
*
* This endpoint is used by the select2 library to dynamically load data (used in the multiselect action helper in parts lists)
*/
#[Route(path: '/select_api')]
class SelectAPIController extends AbstractController
{
private NodesListBuilder $nodesListBuilder;
private TranslatorInterface $translator;
private StructuralEntityChoiceHelper $choiceHelper;
public function __construct(NodesListBuilder $nodesListBuilder, TranslatorInterface $translator, StructuralEntityChoiceHelper $choiceHelper)
public function __construct(private readonly NodesListBuilder $nodesListBuilder, private readonly TranslatorInterface $translator, private readonly StructuralEntityChoiceHelper $choiceHelper)
{
$this->nodesListBuilder = $nodesListBuilder;
$this->translator = $translator;
$this->choiceHelper = $choiceHelper;
}
/**
* @Route("/category", name="select_category")
*/
#[Route(path: '/category', name: 'select_category')]
public function category(): Response
{
return $this->getResponseForClass(Category::class);
}
/**
* @Route("/footprint", name="select_footprint")
*/
#[Route(path: '/footprint', name: 'select_footprint')]
public function footprint(): Response
{
return $this->getResponseForClass(Footprint::class, true);
}
/**
* @Route("/manufacturer", name="select_manufacturer")
*/
#[Route(path: '/manufacturer', name: 'select_manufacturer')]
public function manufacturer(): Response
{
return $this->getResponseForClass(Manufacturer::class, true);
}
/**
* @Route("/measurement_unit", name="select_measurement_unit")
*/
#[Route(path: '/measurement_unit', name: 'select_measurement_unit')]
public function measurement_unit(): Response
{
return $this->getResponseForClass(MeasurementUnit::class, true);
}
/**
* @Route("/project", name="select_project")
*/
#[Route(path: '/project', name: 'select_project')]
public function projects(): Response
{
return $this->getResponseForClass(Project::class, false);
}
/**
* @Route("/export_level", name="select_export_level")
*/
#[Route(path: '/export_level', name: 'select_export_level')]
public function exportLevel(): Response
{
$entries = [
@ -105,18 +87,13 @@ class SelectAPIController extends AbstractController
3 => $this->translator->trans('export.level.full'),
];
return $this->json(array_map(static function ($key, $value) {
return [
return $this->json(array_map(static fn($key, $value) => [
'text' => $value,
'value' => $key,
];
}, array_keys($entries), $entries));
], array_keys($entries), $entries));
}
/**
* @Route("/label_profiles", name="select_label_profiles")
* @return Response
*/
#[Route(path: '/label_profiles', name: 'select_label_profiles')]
public function labelProfiles(EntityManagerInterface $entityManager): Response
{
$this->denyAccessUnlessGranted('@labels.create_labels');
@ -134,10 +111,7 @@ class SelectAPIController extends AbstractController
return $this->json($nodes);
}
/**
* @Route("/label_profiles_lot", name="select_label_profiles_lot")
* @return Response
*/
#[Route(path: '/label_profiles_lot', name: 'select_label_profiles_lot')]
public function labelProfilesLot(EntityManagerInterface $entityManager): Response
{
$this->denyAccessUnlessGranted('@labels.create_labels');
@ -198,7 +172,7 @@ class SelectAPIController extends AbstractController
//Remove the data-* prefix for each key
$data = array_combine(
array_map(static function ($key) {
if (strpos($key, 'data-') === 0) {
if (str_starts_with($key, 'data-')) {
return substr($key, 5);
}
return $key;

View file

@ -48,9 +48,7 @@ use Symfony\Component\Routing\Annotation\Route;
class StatisticsController extends AbstractController
{
/**
* @Route("/statistics", name="statistics_view")
*/
#[Route(path: '/statistics', name: 'statistics_view')]
public function showStatistics(StatisticsHelper $helper): Response
{
$this->denyAccessUnlessGranted('@tools.statistics');

View file

@ -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\Services\Attachments\AttachmentPathResolver;
@ -32,14 +34,10 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGenerator;
/**
* @Route("/tools")
*/
#[Route(path: '/tools')]
class ToolsController extends AbstractController
{
/**
* @Route("/reel_calc", name="tools_reel_calculator")
*/
#[Route(path: '/reel_calc', name: 'tools_reel_calculator')]
public function reelCalculator(): Response
{
$this->denyAccessUnlessGranted('@tools.reel_calculator');
@ -47,9 +45,7 @@ class ToolsController extends AbstractController
return $this->render('tools/reel_calculator/reel_calculator.html.twig');
}
/**
* @Route("/server_infos", name="tools_server_infos")
*/
#[Route(path: '/server_infos', name: 'tools_server_infos')]
public function systemInfos(GitVersionInfo $versionInfo, DBInfoHelper $DBInfoHelper,
AttachmentSubmitHandler $attachmentSubmitHandler): Response
{
@ -65,7 +61,7 @@ class ToolsController extends AbstractController
'default_theme' => $this->getParameter('partdb.global_theme'),
'enabled_locales' => $this->getParameter('partdb.locale_menu'),
'demo_mode' => $this->getParameter('partdb.demo_mode'),
'gpdr_compliance' => $this->getParameter('partdb.gpdr_compliance'),
'gpdr_compliance' => $this->getParameter('partdb.gdpr_compliance'),
'use_gravatar' => $this->getParameter('partdb.users.use_gravatar'),
'email_password_reset' => $this->getParameter('partdb.users.email_pw_reset'),
'enviroment' => $this->getParameter('kernel.environment'),
@ -83,7 +79,7 @@ class ToolsController extends AbstractController
'php_version' => PHP_VERSION,
'php_uname' => php_uname('a'),
'php_sapi' => PHP_SAPI,
'php_extensions' => array_merge(get_loaded_extensions()),
'php_extensions' => [...get_loaded_extensions()],
'php_opcache_enabled' => ini_get('opcache.enable'),
'php_upload_max_filesize' => ini_get('upload_max_filesize'),
'php_post_max_size' => ini_get('post_max_size'),
@ -97,33 +93,23 @@ class ToolsController extends AbstractController
]);
}
/**
* @Route("/builtin_footprints", name="tools_builtin_footprints_viewer")
* @return Response
*/
#[Route(path: '/builtin_footprints', name: 'tools_builtin_footprints_viewer')]
public function builtInFootprintsViewer(BuiltinAttachmentsFinder $builtinAttachmentsFinder, AttachmentURLGenerator $urlGenerator): Response
{
$this->denyAccessUnlessGranted('@tools.builtin_footprints_viewer');
$grouped_footprints = $builtinAttachmentsFinder->getListOfFootprintsGroupedByFolder();
$grouped_footprints = array_map(function($group) use ($urlGenerator) {
return array_map(function($placeholder_filepath) use ($urlGenerator) {
return [
'filename' => basename($placeholder_filepath),
$grouped_footprints = array_map(fn($group) => array_map(fn($placeholder_filepath) => [
'filename' => basename((string) $placeholder_filepath),
'assets_path' => $urlGenerator->placeholderPathToAssetPath($placeholder_filepath),
];
}, $group);
}, $grouped_footprints);
], $group), $grouped_footprints);
return $this->render('tools/builtin_footprints_viewer/builtin_footprints_viewer.html.twig', [
'grouped_footprints' => $grouped_footprints,
]);
}
/**
* @Route("/ic_logos", name="tools_ic_logos")
* @return Response
*/
#[Route(path: '/ic_logos', name: 'tools_ic_logos')]
public function icLogos(): Response
{
$this->denyAccessUnlessGranted('@tools.ic_logos');

View file

@ -22,6 +22,7 @@ declare(strict_types=1);
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use App\Entity\ProjectSystem\Project;
use App\Entity\Parts\Category;
use App\Entity\Parts\Footprint;
@ -36,21 +37,15 @@ use Symfony\Component\Routing\Annotation\Route;
/**
* This controller has the purpose to provide the data for all treeviews.
*
* @Route("/tree")
*/
#[Route(path: '/tree')]
class TreeController extends AbstractController
{
protected TreeViewGenerator $treeGenerator;
public function __construct(TreeViewGenerator $treeGenerator)
public function __construct(protected TreeViewGenerator $treeGenerator)
{
$this->treeGenerator = $treeGenerator;
}
/**
* @Route("/tools", name="tree_tools")
*/
#[Route(path: '/tools', name: 'tree_tools')]
public function tools(ToolsTreeBuilder $builder): JsonResponse
{
$tree = $builder->getTree();
@ -58,90 +53,78 @@ class TreeController extends AbstractController
return new JsonResponse($tree);
}
/**
* @Route("/category/{id}", name="tree_category")
* @Route("/categories", name="tree_category_root")
*/
#[Route(path: '/category/{id}', name: 'tree_category')]
#[Route(path: '/categories', name: 'tree_category_root')]
public function categoryTree(?Category $category = null): JsonResponse
{
if ($this->isGranted('@parts.read') && $this->isGranted('@categories.read')) {
$tree = $this->treeGenerator->getTreeView(Category::class, $category, 'list_parts_root');
} else {
return new JsonResponse("Access denied", 403);
return new JsonResponse("Access denied", Response::HTTP_FORBIDDEN);
}
return new JsonResponse($tree);
}
/**
* @Route("/footprint/{id}", name="tree_footprint")
* @Route("/footprints", name="tree_footprint_root")
*/
#[Route(path: '/footprint/{id}', name: 'tree_footprint')]
#[Route(path: '/footprints', name: 'tree_footprint_root')]
public function footprintTree(?Footprint $footprint = null): JsonResponse
{
if ($this->isGranted('@parts.read') && $this->isGranted('@footprints.read')) {
$tree = $this->treeGenerator->getTreeView(Footprint::class, $footprint, 'list_parts_root');
} else {
return new JsonResponse("Access denied", 403);
return new JsonResponse("Access denied", Response::HTTP_FORBIDDEN);
}
return new JsonResponse($tree);
}
/**
* @Route("/location/{id}", name="tree_location")
* @Route("/locations", name="tree_location_root")
*/
#[Route(path: '/location/{id}', name: 'tree_location')]
#[Route(path: '/locations', name: 'tree_location_root')]
public function locationTree(?Storelocation $location = null): JsonResponse
{
if ($this->isGranted('@parts.read') && $this->isGranted('@storelocations.read')) {
$tree = $this->treeGenerator->getTreeView(Storelocation::class, $location, 'list_parts_root');
} else {
return new JsonResponse("Access denied", 403);
return new JsonResponse("Access denied", Response::HTTP_FORBIDDEN);
}
return new JsonResponse($tree);
}
/**
* @Route("/manufacturer/{id}", name="tree_manufacturer")
* @Route("/manufacturers", name="tree_manufacturer_root")
*/
#[Route(path: '/manufacturer/{id}', name: 'tree_manufacturer')]
#[Route(path: '/manufacturers', name: 'tree_manufacturer_root')]
public function manufacturerTree(?Manufacturer $manufacturer = null): JsonResponse
{
if ($this->isGranted('@parts.read') && $this->isGranted('@manufacturers.read')) {
$tree = $this->treeGenerator->getTreeView(Manufacturer::class, $manufacturer, 'list_parts_root');
} else {
return new JsonResponse("Access denied", 403);
return new JsonResponse("Access denied", Response::HTTP_FORBIDDEN);
}
return new JsonResponse($tree);
}
/**
* @Route("/supplier/{id}", name="tree_supplier")
* @Route("/suppliers", name="tree_supplier_root")
*/
#[Route(path: '/supplier/{id}', name: 'tree_supplier')]
#[Route(path: '/suppliers', name: 'tree_supplier_root')]
public function supplierTree(?Supplier $supplier = null): JsonResponse
{
if ($this->isGranted('@parts.read') && $this->isGranted('@suppliers.read')) {
$tree = $this->treeGenerator->getTreeView(Supplier::class, $supplier, 'list_parts_root');
} else {
return new JsonResponse("Access denied", 403);
return new JsonResponse("Access denied", Response::HTTP_FORBIDDEN);
}
return new JsonResponse($tree);
}
/**
* @Route("/device/{id}", name="tree_device")
* @Route("/devices", name="tree_device_root")
*/
#[Route(path: '/device/{id}', name: 'tree_device')]
#[Route(path: '/devices', name: 'tree_device_root')]
public function deviceTree(?Project $device = null): JsonResponse
{
if ($this->isGranted('@projects.read')) {
$tree = $this->treeGenerator->getTreeView(Project::class, $device, 'devices');
} else {
return new JsonResponse("Access denied", 403);
return new JsonResponse("Access denied", Response::HTTP_FORBIDDEN);
}
return new JsonResponse($tree);

View file

@ -22,6 +22,10 @@ declare(strict_types=1);
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use App\Entity\Attachments\Attachment;
use App\Entity\Parts\Category;
use App\Entity\Parts\Footprint;
use App\Entity\Parameters\AttachmentTypeParameter;
use App\Entity\Parameters\CategoryParameter;
use App\Entity\Parameters\ProjectParameter;
@ -51,23 +55,15 @@ use Symfony\Component\Serializer\Serializer;
/**
* In this controller the endpoints for the typeaheads are collected.
*
* @Route("/typeahead")
*/
#[Route(path: '/typeahead')]
class TypeaheadController extends AbstractController
{
protected AttachmentURLGenerator $urlGenerator;
protected Packages $assets;
public function __construct(AttachmentURLGenerator $URLGenerator, Packages $assets)
public function __construct(protected AttachmentURLGenerator $urlGenerator, protected Packages $assets)
{
$this->urlGenerator = $URLGenerator;
$this->assets = $assets;
}
/**
* @Route("/builtInResources/search", name="typeahead_builtInRessources")
*/
#[Route(path: '/builtInResources/search', name: 'typeahead_builtInRessources')]
public function builtInResources(Request $request, BuiltinAttachmentsFinder $finder): JsonResponse
{
$query = $request->get('query');
@ -91,51 +87,32 @@ class TypeaheadController extends AbstractController
$serializer = new Serializer($normalizers, $encoders);
$data = $serializer->serialize($result, 'json');
return new JsonResponse($data, 200, [], true);
return new JsonResponse($data, Response::HTTP_OK, [], true);
}
/**
* This function map the parameter type to the class, so we can access its repository
* @param string $type
* @return class-string
*/
private function typeToParameterClass(string $type): string
{
switch ($type) {
case 'category':
return CategoryParameter::class;
case 'part':
return PartParameter::class;
case 'device':
return ProjectParameter::class;
case 'footprint':
return FootprintParameter::class;
case 'manufacturer':
return ManufacturerParameter::class;
case 'storelocation':
return StorelocationParameter::class;
case 'supplier':
return SupplierParameter::class;
case 'attachment_type':
return AttachmentTypeParameter::class;
case 'group':
return GroupParameter::class;
case 'measurement_unit':
return MeasurementUnitParameter::class;
case 'currency':
return Currency::class;
default:
throw new \InvalidArgumentException('Invalid parameter type: '.$type);
}
return match ($type) {
'category' => CategoryParameter::class,
'part' => PartParameter::class,
'device' => ProjectParameter::class,
'footprint' => FootprintParameter::class,
'manufacturer' => ManufacturerParameter::class,
'storelocation' => StorelocationParameter::class,
'supplier' => SupplierParameter::class,
'attachment_type' => AttachmentTypeParameter::class,
'group' => GroupParameter::class,
'measurement_unit' => MeasurementUnitParameter::class,
'currency' => Currency::class,
default => throw new \InvalidArgumentException('Invalid parameter type: '.$type),
};
}
/**
* @Route("/parts/search/{query}", name="typeahead_parts")
* @param string $query
* @param EntityManagerInterface $entityManager
* @return JsonResponse
*/
#[Route(path: '/parts/search/{query}', name: 'typeahead_parts')]
public function parts(EntityManagerInterface $entityManager, PartPreviewGenerator $previewGenerator,
AttachmentURLGenerator $attachmentURLGenerator, string $query = ""): JsonResponse
{
@ -149,7 +126,7 @@ class TypeaheadController extends AbstractController
foreach ($parts as $part) {
//Determine the picture to show:
$preview_attachment = $previewGenerator->getTablePreviewAttachment($part);
if($preview_attachment !== null) {
if($preview_attachment instanceof Attachment) {
$preview_url = $attachmentURLGenerator->getThumbnailURL($preview_attachment, 'thumbnail_sm');
} else {
$preview_url = '';
@ -159,8 +136,8 @@ class TypeaheadController extends AbstractController
$data[] = [
'id' => $part->getID(),
'name' => $part->getName(),
'category' => $part->getCategory() ? $part->getCategory()->getName() : 'Unknown',
'footprint' => $part->getFootprint() ? $part->getFootprint()->getName() : '',
'category' => $part->getCategory() instanceof Category ? $part->getCategory()->getName() : 'Unknown',
'footprint' => $part->getFootprint() instanceof Footprint ? $part->getFootprint()->getName() : '',
'description' => mb_strimwidth($part->getDescription(), 0, 127, '...'),
'image' => $preview_url,
];
@ -169,11 +146,7 @@ class TypeaheadController extends AbstractController
return new JsonResponse($data);
}
/**
* @Route("/parameters/{type}/search/{query}", name="typeahead_parameters", requirements={"type" = ".+"})
* @param string $query
* @return JsonResponse
*/
#[Route(path: '/parameters/{type}/search/{query}', name: 'typeahead_parameters', requirements: ['type' => '.+'])]
public function parameters(string $type, EntityManagerInterface $entityManager, string $query = ""): JsonResponse
{
$class = $this->typeToParameterClass($type);
@ -190,9 +163,7 @@ class TypeaheadController extends AbstractController
return new JsonResponse($data);
}
/**
* @Route("/tags/search/{query}", name="typeahead_tags", requirements={"query"= ".+"})
*/
#[Route(path: '/tags/search/{query}', name: 'typeahead_tags', requirements: ['query' => '.+'])]
public function tags(string $query, TagFinder $finder): JsonResponse
{
$this->denyAccessUnlessGranted('@parts.read');
@ -209,6 +180,6 @@ class TypeaheadController extends AbstractController
$serializer = new Serializer($normalizers, $encoders);
$data = $serializer->serialize($array, 'json');
return new JsonResponse($data, 200, [], true);
return new JsonResponse($data, Response::HTTP_OK, [], true);
}
}

View file

@ -22,6 +22,7 @@ declare(strict_types=1);
namespace App\Controller;
use App\Controller\AdminPages\BaseAdminController;
use App\DataTables\LogDataTable;
use App\Entity\Attachments\UserAttachment;
use App\Entity\Base\AbstractNamedDBElement;
@ -46,11 +47,8 @@ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/user")
* Class UserController
*/
class UserController extends AdminPages\BaseAdminController
#[Route(path: '/user')]
class UserController extends BaseAdminController
{
protected string $entity_class = User::class;
protected string $twig_template = 'admin/user_admin.html.twig';
@ -76,11 +74,11 @@ class UserController extends AdminPages\BaseAdminController
}
/**
* @Route("/{id}/edit/{timestamp}", requirements={"id"="\d+"}, name="user_edit")
* @Route("/{id}/", requirements={"id"="\d+"})
*
* @throws Exception
*/
#[Route(path: '/{id}/edit/{timestamp}', requirements: ['id' => '\d+'], name: 'user_edit')]
#[Route(path: '/{id}/', requirements: ['id' => '\d+'])]
public function edit(User $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)
@ -90,7 +88,7 @@ class UserController extends AdminPages\BaseAdminController
if ($request->request->has('reset_2fa')) {
//Check if the admin has the needed permissions
$this->denyAccessUnlessGranted('set_password', $entity);
if ($this->isCsrfTokenValid('reset_2fa'.$entity->getId(), $request->request->get('_token'))) {
if ($this->isCsrfTokenValid('reset_2fa'.$entity->getID(), $request->request->get('_token'))) {
//Disable Google authenticator
$entity->setGoogleAuthenticatorSecret(null);
$entity->setBackupCodes([]);
@ -98,7 +96,7 @@ class UserController extends AdminPages\BaseAdminController
foreach ($entity->getLegacyU2FKeys() as $key) {
$em->remove($key);
}
foreach ($entity->getWebAuthnKeys() as $key) {
foreach ($entity->getWebauthnKeys() as $key) {
$em->remove($key);
}
//Invalidate trusted devices
@ -117,7 +115,7 @@ class UserController extends AdminPages\BaseAdminController
//Handle permissions presets
if ($request->request->has('permission_preset')) {
$this->denyAccessUnlessGranted('edit_permissions', $entity);
if ($this->isCsrfTokenValid('reset_2fa'.$entity->getId(), $request->request->get('_token'))) {
if ($this->isCsrfTokenValid('reset_2fa'.$entity->getID(), $request->request->get('_token'))) {
$preset = $request->request->get('permission_preset');
$permissionPresetsHelper->applyPreset($entity, $preset);
@ -148,19 +146,15 @@ class UserController extends AdminPages\BaseAdminController
return true;
}
/**
* @Route("/new", name="user_new")
* @Route("/{id}/clone", name="user_clone")
* @Route("/")
*/
#[Route(path: '/new', name: 'user_new')]
#[Route(path: '/{id}/clone', name: 'user_clone')]
#[Route(path: '/')]
public function new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?User $entity = null): Response
{
return $this->_new($request, $em, $importer, $entity);
}
/**
* @Route("/{id}", name="user_delete", methods={"DELETE"}, requirements={"id"="\d+"})
*/
#[Route(path: '/{id}', name: 'user_delete', methods: ['DELETE'], requirements: ['id' => '\d+'])]
public function delete(Request $request, User $entity, StructuralElementRecursionHelper $recursionHelper): RedirectResponse
{
if (User::ID_ANONYMOUS === $entity->getID()) {
@ -170,30 +164,24 @@ class UserController extends AdminPages\BaseAdminController
return $this->_delete($request, $entity, $recursionHelper);
}
/**
* @Route("/export", name="user_export_all")
*/
#[Route(path: '/export', name: 'user_export_all')]
public function exportAll(EntityManagerInterface $em, EntityExporter $exporter, Request $request): Response
{
return $this->_exportAll($em, $exporter, $request);
}
/**
* @Route("/{id}/export", name="user_export")
*/
#[Route(path: '/{id}/export', name: 'user_export')]
public function exportEntity(User $entity, EntityExporter $exporter, Request $request): Response
{
return $this->_exportEntity($entity, $exporter, $request);
}
/**
* @Route("/info", name="user_info_self")
* @Route("/{id}/info", name="user_info")
*/
#[Route(path: '/info', name: 'user_info_self')]
#[Route(path: '/{id}/info', name: 'user_info')]
public function userInfo(?User $user, Packages $packages, Request $request, DataTableFactory $dataTableFactory): Response
{
//If no user id was passed, then we show info about the current user
if (null === $user) {
if (!$user instanceof User) {
$tmp = $this->getUser();
if (!$tmp instanceof User) {
throw new InvalidArgumentException('Userinfo only works for database users!');
@ -229,7 +217,7 @@ class UserController extends AdminPages\BaseAdminController
'data' => $user,
]);
return $this->renderForm('users/user_info.html.twig', [
return $this->render('users/user_info.html.twig', [
'user' => $user,
'form' => $builder->getForm(),
'datatable' => $table ?? null,

View file

@ -22,6 +22,7 @@ declare(strict_types=1);
namespace App\Controller;
use App\Entity\Attachments\Attachment;
use App\Entity\UserSystem\U2FKey;
use App\Entity\UserSystem\User;
use App\Entity\UserSystem\WebauthnKey;
@ -51,28 +52,21 @@ use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Validator\Constraints\UserPassword;
use Symfony\Component\Validator\Constraints\Length;
/**
* @Route("/user")
*/
#[Route(path: '/user')]
class UserSettingsController extends AbstractController
{
protected bool $demo_mode;
/**
* @var EventDispatcher|EventDispatcherInterface
*/
protected $eventDispatcher;
public function __construct(bool $demo_mode, EventDispatcherInterface $eventDispatcher)
public function __construct(protected bool $demo_mode, EventDispatcherInterface $eventDispatcher)
{
$this->demo_mode = $demo_mode;
$this->eventDispatcher = $eventDispatcher;
}
/**
* @Route("/2fa_backup_codes", name="show_backup_codes")
*/
public function showBackupCodes()
#[Route(path: '/2fa_backup_codes', name: 'show_backup_codes')]
public function showBackupCodes(): Response
{
$user = $this->getUser();
@ -80,14 +74,14 @@ class UserSettingsController extends AbstractController
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
if (!$user instanceof User) {
return new RuntimeException('This controller only works only for Part-DB User objects!');
throw new RuntimeException('This controller only works only for Part-DB User objects!');
}
if ($user->isSamlUser()) {
throw new RuntimeException('You can not remove U2F keys from SAML users!');
}
if (empty($user->getBackupCodes())) {
if ($user->getBackupCodes() === []) {
$this->addFlash('error', 'tfa_backup.no_codes_enabled');
throw new RuntimeException('You do not have any backup codes enabled, therefore you can not view them!');
@ -98,9 +92,7 @@ class UserSettingsController extends AbstractController
]);
}
/**
* @Route("/u2f_delete", name="u2f_delete", methods={"DELETE"})
*/
#[Route(path: '/u2f_delete', name: 'u2f_delete', methods: ['DELETE'])]
public function removeU2FToken(Request $request, EntityManagerInterface $entityManager, BackupCodeManager $backupCodeManager): RedirectResponse
{
if ($this->demo_mode) {
@ -120,56 +112,50 @@ class UserSettingsController extends AbstractController
throw new RuntimeException('You can not remove U2F keys from SAML users!');
}
if ($this->isCsrfTokenValid('delete'.$user->getId(), $request->request->get('_token'))) {
if ($this->isCsrfTokenValid('delete'.$user->getID(), $request->request->get('_token'))) {
//Handle U2F key removal
if ($request->request->has('key_id')) {
$key_id = $request->request->get('key_id');
$key_repo = $entityManager->getRepository(U2FKey::class);
/** @var U2FKey|null $u2f */
$u2f = $key_repo->find($key_id);
if (null === $u2f) {
if (!$u2f instanceof U2FKey) {
$this->addFlash('danger', 'tfa_u2f.u2f_delete.not_existing');
return $this->redirectToRoute('user_settings');
}
//User can only delete its own U2F keys
if ($u2f->getUser() !== $user) {
$this->addFlash('danger', 'tfa_u2f.u2f_delete.access_denied');
return $this->redirectToRoute('user_settings');
}
$backupCodeManager->disableBackupCodesIfUnused($user);
$entityManager->remove($u2f);
$entityManager->flush();
$this->addFlash('success', 'tfa.u2f.u2f_delete.success');
$security_event = new SecurityEvent($user);
$this->eventDispatcher->dispatch($security_event, SecurityEvents::U2F_REMOVED);
} else if ($request->request->has('webauthn_key_id')) {
} elseif ($request->request->has('webauthn_key_id')) {
$key_id = $request->request->get('webauthn_key_id');
$key_repo = $entityManager->getRepository(WebauthnKey::class);
/** @var WebauthnKey|null $key */
$key = $key_repo->find($key_id);
if (null === $key) {
if (!$key instanceof WebauthnKey) {
$this->addFlash('error', 'tfa_u2f.u2f_delete.not_existing');
return $this->redirectToRoute('user_settings');
}
//User can only delete its own U2F keys
if ($key->getUser() !== $user) {
$this->addFlash('error', 'tfa_u2f.u2f_delete.access_denied');
return $this->redirectToRoute('user_settings');
}
$backupCodeManager->disableBackupCodesIfUnused($user);
$entityManager->remove($key);
$entityManager->flush();
$this->addFlash('success', 'tfa.u2f.u2f_delete.success');
$security_event = new SecurityEvent($user);
$this->eventDispatcher->dispatch($security_event, SecurityEvents::U2F_REMOVED);
}
@ -180,12 +166,8 @@ class UserSettingsController extends AbstractController
return $this->redirectToRoute('user_settings');
}
/**
* @Route("/invalidate_trustedDevices", name="tfa_trustedDevices_invalidate", methods={"DELETE"})
*
* @return RuntimeException|RedirectResponse
*/
public function resetTrustedDevices(Request $request, EntityManagerInterface $entityManager)
#[Route(path: '/invalidate_trustedDevices', name: 'tfa_trustedDevices_invalidate', methods: ['DELETE'])]
public function resetTrustedDevices(Request $request, EntityManagerInterface $entityManager): \RuntimeException|RedirectResponse
{
if ($this->demo_mode) {
throw new RuntimeException('You can not do 2FA things in demo mode');
@ -204,7 +186,7 @@ class UserSettingsController extends AbstractController
throw new RuntimeException('You can not remove U2F keys from SAML users!');
}
if ($this->isCsrfTokenValid('devices_reset'.$user->getId(), $request->request->get('_token'))) {
if ($this->isCsrfTokenValid('devices_reset'.$user->getID(), $request->request->get('_token'))) {
$user->invalidateTrustedDeviceTokens();
$entityManager->flush();
$this->addFlash('success', 'tfa_trustedDevice.invalidate.success');
@ -219,11 +201,10 @@ class UserSettingsController extends AbstractController
}
/**
* @Route("/settings", name="user_settings")
*
* @return RedirectResponse|Response
*/
public function userSettings(Request $request, EntityManagerInterface $em, UserPasswordHasherInterface $passwordEncoder, GoogleAuthenticator $googleAuthenticator, BackupCodeManager $backupCodeManager, FormFactoryInterface $formFactory, UserAvatarHelper $avatarHelper)
#[Route(path: '/settings', name: 'user_settings')]
public function userSettings(Request $request, EntityManagerInterface $em, UserPasswordHasherInterface $passwordEncoder, GoogleAuthenticator $googleAuthenticator, BackupCodeManager $backupCodeManager, FormFactoryInterface $formFactory, UserAvatarHelper $avatarHelper): RedirectResponse|Response
{
/** @var User $user */
$user = $this->getUser();
@ -262,14 +243,12 @@ class UserSettingsController extends AbstractController
}
/** @var Form $form We need a form implementation for the next calls */
if ($form->getClickedButton() && 'remove_avatar' === $form->getClickedButton()->getName()) {
//Remove the avatar attachment from the user if requested
if ($user->getMasterPictureAttachment() !== null) {
if ($form->getClickedButton() && 'remove_avatar' === $form->getClickedButton()->getName() && $user->getMasterPictureAttachment() instanceof Attachment) {
$em->remove($user->getMasterPictureAttachment());
$user->setMasterPictureAttachment(null);
$page_need_reload = true;
}
}
$em->flush();
$this->addFlash('success', 'user.settings.saved_flash');
@ -346,7 +325,7 @@ class UserSettingsController extends AbstractController
'disabled' => $this->demo_mode || $user->isSamlUser(),
]);
$google_enabled = $user->isGoogleAuthenticatorEnabled();
if (!$google_enabled && !$form->isSubmitted()) {
if (!$google_enabled && !$google_form->isSubmitted()) {
$user->setGoogleAuthenticatorSecret($googleAuthenticator->generateSecret());
$google_form->get('googleAuthenticatorSecret')->setData($user->getGoogleAuthenticatorSecret());
}
@ -382,7 +361,7 @@ class UserSettingsController extends AbstractController
'attr' => [
'class' => 'btn-danger',
],
'disabled' => empty($user->getBackupCodes()),
'disabled' => $user->getBackupCodes() === [],
])->getForm();
$backup_form->handleRequest($request);
@ -397,7 +376,7 @@ class UserSettingsController extends AbstractController
* Output both forms
*****************************/
return $this->renderForm('users/user_settings.html.twig', [
return $this->render('users/user_settings.html.twig', [
'user' => $user,
'settings_form' => $form,
'pw_form' => $pw_form,

View file

@ -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\UserSystem\User;
@ -27,23 +29,19 @@ use Jbtronics\TFAWebauthn\Services\TFAWebauthnRegistrationHelper;
use RuntimeException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use function Symfony\Component\Translation\t;
class WebauthnKeyRegistrationController extends AbstractController
{
private bool $demo_mode;
public function __construct(bool $demo_mode)
public function __construct(private readonly bool $demo_mode)
{
$this->demo_mode = $demo_mode;
}
/**
* @Route("/webauthn/register", name="webauthn_register")
*/
public function register(Request $request, TFAWebauthnRegistrationHelper $registrationHelper, EntityManagerInterface $em)
#[Route(path: '/webauthn/register', name: 'webauthn_register')]
public function register(Request $request, TFAWebauthnRegistrationHelper $registrationHelper, EntityManagerInterface $em): Response
{
//When user change its settings, he should be logged in fully.
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
@ -75,14 +73,19 @@ class WebauthnKeyRegistrationController extends AbstractController
//Check the response
try {
$new_key = $registrationHelper->checkRegistrationResponse($webauthnResponse);
} catch (\Exception $exception) {
} catch (\Exception) {
$this->addFlash('error', t('tfa_u2f.add_key.registration_error'));
return $this->redirectToRoute('webauthn_register');
}
$user = $this->getUser();
if (!$user instanceof User) {
throw new RuntimeException('This controller only works only for Part-DB User objects!');
}
$keyEntity = WebauthnKey::fromRegistration($new_key);
$keyEntity->setName($keyName);
$keyEntity->setUser($this->getUser());
$keyEntity->setUser($user);
$em->persist($keyEntity);
$em->flush();

View file

@ -1,37 +0,0 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\DataFixtures;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager): void
{
// $product = new Product();
// $manager->persist($product);
$manager->flush();
}
}

View file

@ -31,18 +31,17 @@ use App\Entity\Parts\Manufacturer;
use App\Entity\Parts\MeasurementUnit;
use App\Entity\Parts\Storelocation;
use App\Entity\Parts\Supplier;
use App\Entity\UserSystem\User;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\ObjectManager;
use InvalidArgumentException;
class DataStructureFixtures extends Fixture
class DataStructureFixtures extends Fixture implements DependentFixtureInterface
{
protected EntityManagerInterface $em;
public function __construct(EntityManagerInterface $entityManager)
public function __construct(protected EntityManagerInterface $em)
{
$this->em = $entityManager;
}
/**
@ -52,7 +51,7 @@ class DataStructureFixtures extends Fixture
{
//Reset autoincrement
$types = [AttachmentType::class, Project::class, Category::class, Footprint::class, Manufacturer::class,
MeasurementUnit::class, Storelocation::class, Supplier::class, ];
MeasurementUnit::class, Storelocation::class, Supplier::class,];
foreach ($types as $type) {
$this->createNodesForClass($type, $manager);
@ -109,4 +108,11 @@ class DataStructureFixtures extends Fixture
$manager->persist($node2_1);
$manager->persist($node1_1_1);
}
public function getDependencies(): array
{
return [
UserFixtures::class
];
}
}

View file

@ -30,18 +30,12 @@ use Doctrine\Persistence\ObjectManager;
class GroupFixtures extends Fixture
{
public const ADMINS = 'group-admin';
public const USERS = 'group-users';
public const READONLY = 'group-readonly';
final public const ADMINS = 'group-admin';
final public const USERS = 'group-users';
final public const READONLY = 'group-readonly';
private PermissionPresetsHelper $permission_presets;
private PermissionManager $permissionManager;
public function __construct(PermissionPresetsHelper $permissionPresetsHelper, PermissionManager $permissionManager)
public function __construct(private readonly PermissionPresetsHelper $permission_presets, private readonly PermissionManager $permissionManager)
{
$this->permission_presets = $permissionPresetsHelper;
$this->permissionManager = $permissionManager;
}
public function load(ObjectManager $manager): void

View file

@ -41,19 +41,19 @@ declare(strict_types=1);
namespace App\DataFixtures;
use App\Entity\LabelSystem\BarcodeType;
use App\Entity\LabelSystem\LabelOptions;
use App\Entity\LabelSystem\LabelProcessMode;
use App\Entity\LabelSystem\LabelProfile;
use App\Entity\LabelSystem\LabelSupportedElement;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\ObjectManager;
class LabelProfileFixtures extends Fixture
{
protected EntityManagerInterface $em;
public function __construct(EntityManagerInterface $entityManager)
public function __construct(protected EntityManagerInterface $em)
{
$this->em = $entityManager;
}
public function load(ObjectManager $manager): void
@ -65,8 +65,8 @@ class LabelProfileFixtures extends Fixture
$option1 = new LabelOptions();
$option1->setLines("[[NAME]]\n[[DESCRIPION]]");
$option1->setBarcodeType('none');
$option1->setSupportedElement('part');
$option1->setBarcodeType(BarcodeType::NONE);
$option1->setSupportedElement(LabelSupportedElement::PART);
$profile1->setOptions($option1);
$manager->persist($profile1);
@ -77,8 +77,8 @@ class LabelProfileFixtures extends Fixture
$option2 = new LabelOptions();
$option2->setLines("[[NAME]]\n[[DESCRIPION]]");
$option2->setBarcodeType('qr');
$option2->setSupportedElement('part');
$option2->setBarcodeType(BarcodeType::QR);
$option2->setSupportedElement(LabelSupportedElement::PART);
$profile2->setOptions($option2);
$manager->persist($profile2);
@ -89,8 +89,8 @@ class LabelProfileFixtures extends Fixture
$option3 = new LabelOptions();
$option3->setLines("[[NAME]]\n[[DESCRIPION]]");
$option3->setBarcodeType('code128');
$option3->setSupportedElement('part_lot');
$option3->setBarcodeType(BarcodeType::CODE128);
$option3->setSupportedElement(LabelSupportedElement::PART_LOT);
$profile3->setOptions($option3);
$manager->persist($profile3);
@ -101,13 +101,20 @@ class LabelProfileFixtures extends Fixture
$option4 = new LabelOptions();
$option4->setLines('{{ element.name }}');
$option4->setBarcodeType('code39');
$option4->setSupportedElement('part');
$option4->setLinesMode('twig');
$option4->setBarcodeType(BarcodeType::CODE39);
$option4->setSupportedElement(LabelSupportedElement::PART);
$option4->setProcessMode(LabelProcessMode::TWIG);
$profile4->setOptions($option4);
$manager->persist($profile4);
$manager->flush();
}
public function getDependencies(): array
{
return [
PartFixtures::class,
];
}
}

View file

@ -55,16 +55,14 @@ use App\Entity\PriceInformations\Pricedetail;
use Brick\Math\BigDecimal;
use DateTime;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\ObjectManager;
class PartFixtures extends Fixture
class PartFixtures extends Fixture implements DependentFixtureInterface
{
protected EntityManagerInterface $em;
public function __construct(EntityManagerInterface $entityManager)
public function __construct(protected EntityManagerInterface $em)
{
$this->em = $entityManager;
}
public function load(ObjectManager $manager): void
@ -135,4 +133,11 @@ class PartFixtures extends Fixture
$manager->persist($part);
$manager->flush();
}
public function getDependencies(): array
{
return [
DataStructureFixtures::class
];
}
}

View file

@ -24,19 +24,15 @@ namespace App\DataFixtures;
use App\Entity\UserSystem\User;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\ObjectManager;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
class UserFixtures extends Fixture
class UserFixtures extends Fixture implements DependentFixtureInterface
{
protected UserPasswordHasherInterface $encoder;
protected EntityManagerInterface $em;
public function __construct(UserPasswordHasherInterface $encoder, EntityManagerInterface $entityManager)
public function __construct(protected UserPasswordHasherInterface $encoder, protected EntityManagerInterface $em)
{
$this->em = $entityManager;
$this->encoder = $encoder;
}
public function load(ObjectManager $manager): void
@ -71,4 +67,11 @@ class UserFixtures extends Fixture
$manager->flush();
}
public function getDependencies(): array
{
return [
GroupFixtures::class,
];
}
}

View file

@ -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\DataTables\Adapters;
use Doctrine\ORM\QueryBuilder;
@ -37,7 +39,7 @@ use Omines\DataTablesBundle\Adapter\Doctrine\FetchJoinORMAdapter;
*/
class CustomFetchJoinORMAdapter extends FetchJoinORMAdapter
{
public function getCount(QueryBuilder $queryBuilder, $identifier): ?int
public function getCount(QueryBuilder $queryBuilder, $identifier): int
{
$qb_without_group_by = clone $queryBuilder;
@ -48,6 +50,6 @@ class CustomFetchJoinORMAdapter extends FetchJoinORMAdapter
$paginator = new Paginator($qb_without_group_by);
return $paginator->count();
return $paginator->count() ?? 0;
}
}

View file

@ -42,27 +42,14 @@ use Symfony\Contracts\Translation\TranslatorInterface;
final class AttachmentDataTable implements DataTableTypeInterface
{
private TranslatorInterface $translator;
private EntityURLGenerator $entityURLGenerator;
private AttachmentManager $attachmentHelper;
private ElementTypeNameGenerator $elementTypeNameGenerator;
private AttachmentURLGenerator $attachmentURLGenerator;
public function __construct(TranslatorInterface $translator, EntityURLGenerator $entityURLGenerator,
AttachmentManager $attachmentHelper, AttachmentURLGenerator $attachmentURLGenerator,
ElementTypeNameGenerator $elementTypeNameGenerator)
public function __construct(private readonly TranslatorInterface $translator, private readonly EntityURLGenerator $entityURLGenerator, private readonly AttachmentManager $attachmentHelper, private readonly AttachmentURLGenerator $attachmentURLGenerator, private readonly ElementTypeNameGenerator $elementTypeNameGenerator)
{
$this->translator = $translator;
$this->entityURLGenerator = $entityURLGenerator;
$this->attachmentHelper = $attachmentHelper;
$this->elementTypeNameGenerator = $elementTypeNameGenerator;
$this->attachmentURLGenerator = $attachmentURLGenerator;
}
public function configure(DataTable $dataTable, array $options): void
{
$dataTable->add('dont_matter', RowClassColumn::class, [
'render' => function ($value, Attachment $context) {
'render' => function ($value, Attachment $context): string {
//Mark attachments with missing files yellow
if(!$this->attachmentHelper->isFileExisting($context)){
return 'table-warning';
@ -75,7 +62,7 @@ final class AttachmentDataTable implements DataTableTypeInterface
$dataTable->add('picture', TextColumn::class, [
'label' => '',
'className' => 'no-colvis',
'render' => function ($value, Attachment $context) {
'render' => function ($value, Attachment $context): string {
if ($context->isPicture()
&& !$context->isExternal()
&& $this->attachmentHelper->isFileExisting($context)) {
@ -125,25 +112,21 @@ final class AttachmentDataTable implements DataTableTypeInterface
$dataTable->add('attachment_type', TextColumn::class, [
'label' => 'attachment.table.type',
'field' => 'attachment_type.name',
'render' => function ($value, Attachment $context) {
return sprintf(
'render' => fn($value, Attachment $context): string => sprintf(
'<a href="%s">%s</a>',
$this->entityURLGenerator->editURL($context->getAttachmentType()),
htmlspecialchars($value)
);
},
htmlspecialchars((string) $value)
),
]);
$dataTable->add('element', TextColumn::class, [
'label' => 'attachment.table.element',
//'propertyPath' => 'element.name',
'render' => function ($value, Attachment $context) {
return sprintf(
'render' => fn($value, Attachment $context): string => sprintf(
'<a href="%s">%s</a>',
$this->entityURLGenerator->infoURL($context->getElement()),
$this->elementTypeNameGenerator->getTypeNameCombination($context->getElement(), true)
);
},
),
]);
$dataTable->add('filename', TextColumn::class, [

View file

@ -31,13 +31,8 @@ use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
class EntityColumn extends AbstractColumn
{
protected EntityURLGenerator $urlGenerator;
protected PropertyAccessorInterface $accessor;
public function __construct(EntityURLGenerator $URLGenerator, PropertyAccessorInterface $accessor)
public function __construct(protected EntityURLGenerator $urlGenerator, protected PropertyAccessorInterface $accessor)
{
$this->urlGenerator = $URLGenerator;
$this->accessor = $accessor;
}
/**
@ -46,24 +41,24 @@ class EntityColumn extends AbstractColumn
* @param mixed $value The single value of the column
* @return mixed
*/
public function normalize($value)
public function normalize($value): mixed
{
/** @var AbstractNamedDBElement $value */
return $value;
}
public function configureOptions(OptionsResolver $resolver): self
/**
* @return $this
*/
public function configureOptions(OptionsResolver $resolver): static
{
parent::configureOptions($resolver);
$resolver->setRequired('property');
$resolver->setDefault('field', static function (Options $option) {
return $option['property'].'.name';
});
$resolver->setDefault('field', static fn(Options $option): string => $option['property'].'.name');
$resolver->setDefault('render', function (Options $options) {
return function ($value, $context) use ($options) {
$resolver->setDefault('render', fn(Options $options) => function ($value, $context) use ($options): string {
if ($this->accessor->isReadable($context, $options['property'])) {
$entity = $this->accessor->getValue($context, $options['property']);
} else {
@ -72,7 +67,7 @@ class EntityColumn extends AbstractColumn
/** @var AbstractNamedDBElement|null $entity */
if (null !== $entity) {
if ($entity instanceof AbstractNamedDBElement) {
if (null !== $entity->getID()) {
return sprintf(
'<a href="%s">%s</a>',
@ -85,7 +80,6 @@ class EntityColumn extends AbstractColumn
}
return '';
};
});
return $this;

View file

@ -0,0 +1,64 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\DataTables\Column;
use Omines\DataTablesBundle\Column\AbstractColumn;
use Symfony\Component\OptionsResolver\OptionsResolver;
use UnitEnum;
/**
* @template T of UnitEnum
*/
class EnumColumn extends AbstractColumn
{
/**
* @phpstan-return T
*/
public function normalize($value): UnitEnum
{
if (is_a($value, $this->getEnumClass())) {
return $value;
}
//@phpstan-ignore-next-line
return ($this->getEnumClass())::from($value);
}
protected function configureOptions(OptionsResolver $resolver): static
{
parent::configureOptions($resolver);
$resolver->setRequired('class');
$resolver->setAllowedTypes('class', 'string');
$resolver->addAllowedValues('class', enum_exists(...));
return $this;
}
/**
* @return class-string<T>
*/
public function getEnumClass(): string
{
return $this->options['class'];
}
}

View file

@ -50,12 +50,15 @@ class IconLinkColumn extends AbstractColumn
* @param $value
* @return mixed
*/
public function normalize($value)
public function normalize($value): mixed
{
return $value;
}
public function configureOptions(OptionsResolver $resolver): self
/**
* @return $this
*/
public function configureOptions(OptionsResolver $resolver): static
{
parent::configureOptions($resolver);
$resolver->setDefaults([

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