diff --git a/.docker/symfony.conf b/.docker/symfony.conf index 60597dd6..2f8e7f66 100644 --- a/.docker/symfony.conf +++ b/.docker/symfony.conf @@ -26,7 +26,7 @@ # Pass the configuration from the docker env to the PHP environment (here you should list all .env options) PassEnv APP_ENV APP_DEBUG APP_SECRET - PassEnv DATABASE_URL + PassEnv DATABASE_URL ENFORCE_CHANGE_COMMENTS_FOR PassEnv DEFAULT_LANG DEFAULT_TIMEZONE BASE_CURRENCY INSTANCE_NAME ALLOW_ATTACHMENT_DOWNLOADS USE_GRAVATAR MAX_ATTACHMENT_FILE_SIZE DEFAULT_URI PassEnv MAILER_DSN ALLOW_EMAIL_PW_RESET EMAIL_SENDER_EMAIL EMAIL_SENDER_NAME PassEnv HISTORY_SAVE_CHANGED_FIELDS HISTORY_SAVE_CHANGED_DATA HISTORY_SAVE_REMOVED_DATA diff --git a/.env b/.env index 43cffcd1..7db81e46 100644 --- a/.env +++ b/.env @@ -39,6 +39,11 @@ MAX_ATTACHMENT_FILE_SIZE="100M" # This must end with a slash! DEFAULT_URI="https://partdb.changeme.invalid/" +# With this option you can configure, where users are enforced to give a change reason, which will be logged +# This is a comma separated list of values, see documentation for available values +# Leave this empty, to make all change reasons optional +ENFORCE_CHANGE_COMMENTS_FOR="" + ################################################################################### # Email settings ################################################################################### diff --git a/assets/js/error_handler.js b/assets/js/error_handler.js index 579f4298..7f047af9 100644 --- a/assets/js/error_handler.js +++ b/assets/js/error_handler.js @@ -155,6 +155,11 @@ class ErrorHandlerHelper { return; } + //Skip 404 errors, on admin pages (as this causes a popup on deletion in firefox) + if (response.status == 404 && event.target.id === 'admin-content-frame') { + return; + } + if(!response.ok) { response.text().then(responseHTML => { diff --git a/assets/js/tab_remember.js b/assets/js/tab_remember.js index 7405fcfa..9ecd71c5 100644 --- a/assets/js/tab_remember.js +++ b/assets/js/tab_remember.js @@ -19,7 +19,7 @@ "use strict"; -import {Tab} from "bootstrap"; +import {Tab, Dropdown} from "bootstrap"; import tab from "bootstrap/js/src/tab"; /** @@ -63,6 +63,16 @@ class TabRememberHelper { */ onInvalid(event) { this.revealElementOnTab(event.target); + this.revealElementInDropdown(event.target); + } + + revealElementInDropdown(element) { + let dropdown = element.closest('.dropdown-menu'); + + if(dropdown) { + let bs_dropdown = Dropdown.getOrCreateInstance(dropdown); + bs_dropdown.show(); + } } revealElementOnTab(element) { diff --git a/composer.lock b/composer.lock index 0ccc8933..db97b11f 100644 --- a/composer.lock +++ b/composer.lock @@ -4771,16 +4771,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.16.1", + "version": "1.18.1", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "e27e92d939e2e3636f0a1f0afaba59692c0bf571" + "reference": "22dcdfd725ddf99583bfe398fc624ad6c5004a0f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/e27e92d939e2e3636f0a1f0afaba59692c0bf571", - "reference": "e27e92d939e2e3636f0a1f0afaba59692c0bf571", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/22dcdfd725ddf99583bfe398fc624ad6c5004a0f", + "reference": "22dcdfd725ddf99583bfe398fc624ad6c5004a0f", "shasum": "" }, "require": { @@ -4810,9 +4810,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.16.1" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.18.1" }, - "time": "2023-02-07T18:11:17+00:00" + "time": "2023-04-07T11:51:11+00:00" }, { "name": "psr/cache", @@ -5070,25 +5070,25 @@ }, { "name": "psr/http-message", - "version": "1.0.1", + "version": "1.1", "source": { "type": "git", "url": "https://github.com/php-fig/http-message.git", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": "^7.2 || ^8.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.1.x-dev" } }, "autoload": { @@ -5117,9 +5117,9 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-message/tree/master" + "source": "https://github.com/php-fig/http-message/tree/1.1" }, - "time": "2016-08-06T14:39:51+00:00" + "time": "2023-04-04T09:50:52+00:00" }, { "name": "psr/link", @@ -14120,20 +14120,21 @@ }, { "name": "doctrine/data-fixtures", - "version": "1.6.3", + "version": "1.6.5", "source": { "type": "git", "url": "https://github.com/doctrine/data-fixtures.git", - "reference": "c27821d038e64f1bfc852a94064d65d2a75ad01f" + "reference": "e6b97f557942ea17564bbc30ae3ebc9bd2209363" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/data-fixtures/zipball/c27821d038e64f1bfc852a94064d65d2a75ad01f", - "reference": "c27821d038e64f1bfc852a94064d65d2a75ad01f", + "url": "https://api.github.com/repos/doctrine/data-fixtures/zipball/e6b97f557942ea17564bbc30ae3ebc9bd2209363", + "reference": "e6b97f557942ea17564bbc30ae3ebc9bd2209363", "shasum": "" }, "require": { - "doctrine/persistence": "^1.3.3|^2.0|^3.0", + "doctrine/deprecations": "^0.5.3 || ^1.0", + "doctrine/persistence": "^1.3.3 || ^2.0 || ^3.0", "php": "^7.2 || ^8.0" }, "conflict": { @@ -14142,16 +14143,15 @@ "doctrine/phpcr-odm": "<1.3.0" }, "require-dev": { - "doctrine/coding-standard": "^10.0", + "doctrine/coding-standard": "^11.0", "doctrine/dbal": "^2.13 || ^3.0", - "doctrine/deprecations": "^1.0", "doctrine/mongodb-odm": "^1.3.0 || ^2.0.0", "doctrine/orm": "^2.12", "ext-sqlite3": "*", "phpstan/phpstan": "^1.5", - "phpunit/phpunit": "^8.5 || ^9.5", + "phpunit/phpunit": "^8.5 || ^9.5 || ^10.0", "symfony/cache": "^5.0 || ^6.0", - "vimeo/psalm": "^4.10" + "vimeo/psalm": "^4.10 || ^5.9" }, "suggest": { "alcaeus/mongo-php-adapter": "For using MongoDB ODM 1.3 with PHP 7 (deprecated)", @@ -14162,7 +14162,7 @@ "type": "library", "autoload": { "psr-4": { - "Doctrine\\Common\\DataFixtures\\": "lib/Doctrine/Common/DataFixtures" + "Doctrine\\Common\\DataFixtures\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -14182,7 +14182,7 @@ ], "support": { "issues": "https://github.com/doctrine/data-fixtures/issues", - "source": "https://github.com/doctrine/data-fixtures/tree/1.6.3" + "source": "https://github.com/doctrine/data-fixtures/tree/1.6.5" }, "funding": [ { @@ -14198,7 +14198,7 @@ "type": "tidelift" } ], - "time": "2023-01-07T15:10:22+00:00" + "time": "2023-04-03T14:58:58+00:00" }, { "name": "doctrine/doctrine-fixtures-bundle", @@ -14607,16 +14607,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.10.9", + "version": "1.10.11", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "9b13dafe3d66693d20fe5729c3dde1d31bb64703" + "reference": "8aa62e6ea8b58ffb650e02940e55a788cbc3fe21" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9b13dafe3d66693d20fe5729c3dde1d31bb64703", - "reference": "9b13dafe3d66693d20fe5729c3dde1d31bb64703", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/8aa62e6ea8b58ffb650e02940e55a788cbc3fe21", + "reference": "8aa62e6ea8b58ffb650e02940e55a788cbc3fe21", "shasum": "" }, "require": { @@ -14665,7 +14665,7 @@ "type": "tidelift" } ], - "time": "2023-03-30T08:58:01+00:00" + "time": "2023-04-04T19:17:42+00:00" }, { "name": "phpstan/phpstan-doctrine", @@ -14739,16 +14739,16 @@ }, { "name": "phpstan/phpstan-symfony", - "version": "1.2.24", + "version": "1.2.25", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-symfony.git", - "reference": "db81b1861aac7cc2e66115cb33b4d1ea2a73d096" + "reference": "1da7bf450c6b351fec08ca0aa97298473d4f6ab3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/db81b1861aac7cc2e66115cb33b4d1ea2a73d096", - "reference": "db81b1861aac7cc2e66115cb33b4d1ea2a73d096", + "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/1da7bf450c6b351fec08ca0aa97298473d4f6ab3", + "reference": "1da7bf450c6b351fec08ca0aa97298473d4f6ab3", "shasum": "" }, "require": { @@ -14804,9 +14804,9 @@ "description": "Symfony Framework extensions and rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-symfony/issues", - "source": "https://github.com/phpstan/phpstan-symfony/tree/1.2.24" + "source": "https://github.com/phpstan/phpstan-symfony/tree/1.2.25" }, - "time": "2023-03-30T08:38:10+00:00" + "time": "2023-04-05T12:16:20+00:00" }, { "name": "psalm/plugin-symfony", @@ -14879,12 +14879,12 @@ "source": { "type": "git", "url": "https://github.com/Roave/SecurityAdvisories.git", - "reference": "945aadf4c174c61973464b4000f4d7529aeb84a0" + "reference": "6efa800243b92a3601e0101b0333d45df35832a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/945aadf4c174c61973464b4000f4d7529aeb84a0", - "reference": "945aadf4c174c61973464b4000f4d7529aeb84a0", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/6efa800243b92a3601e0101b0333d45df35832a4", + "reference": "6efa800243b92a3601e0101b0333d45df35832a4", "shasum": "" }, "conflict": { @@ -14902,6 +14902,7 @@ "amphp/http-client": ">=4,<4.4", "anchorcms/anchor-cms": "<=0.12.7", "andreapollastri/cipi": "<=3.1.15", + "andrewhaine/silverstripe-form-capture": ">=0.2,<=0.2.3|>=1,<=1.0.1|>=2,<=2.2.4", "apereo/phpcas": "<1.6", "api-platform/core": ">=2.2,<2.2.10|>=2.3,<2.3.6|>=2.6,<2.7.10|>=3,<3.0.12|>=3.1,<3.1.3", "appwrite/server-ce": "<0.11.1|>=0.12,<0.12.2", @@ -14920,6 +14921,7 @@ "barzahlen/barzahlen-php": "<2.0.1", "baserproject/basercms": "<4.7.5", "bassjobsen/bootstrap-3-typeahead": ">4.0.2", + "bigfork/silverstripe-form-capture": ">=3,<=3.1", "billz/raspap-webgui": "<=2.6.6", "bk2k/bootstrap-package": ">=7.1,<7.1.2|>=8,<8.0.8|>=9,<9.0.4|>=9.1,<9.1.3|>=10,<10.0.10|>=11,<11.0.3", "bmarshall511/wordpress_zero_spam": "<5.2.13", @@ -15003,7 +15005,7 @@ "ezsystems/ezplatform-graphql": ">=1-rc.1,<1.0.13|>=2-beta.1,<2.3.12", "ezsystems/ezplatform-kernel": "<1.2.5.1|>=1.3,<1.3.26", "ezsystems/ezplatform-rest": ">=1.2,<=1.2.2|>=1.3,<1.3.8", - "ezsystems/ezplatform-richtext": ">=2.3,<=2.3.7", + "ezsystems/ezplatform-richtext": ">=2.3,<2.3.7.1", "ezsystems/ezplatform-user": ">=1,<1.0.1", "ezsystems/ezpublish-kernel": "<6.13.8.2|>=7,<7.5.30", "ezsystems/ezpublish-legacy": "<=2017.12.7.3|>=2018.6,<=2019.3.5.1", @@ -15051,7 +15053,7 @@ "gos/web-socket-bundle": "<1.10.4|>=2,<2.6.1|>=3,<3.3", "gree/jose": "<2.2.1", "gregwar/rst": "<1.0.3", - "grumpydictator/firefly-iii": "<5.8", + "grumpydictator/firefly-iii": "<6", "guzzlehttp/guzzle": "<6.5.8|>=7,<7.4.5", "guzzlehttp/psr7": "<1.8.4|>=2,<2.1.1", "harvesthq/chosen": "<1.8.7", @@ -15096,7 +15098,7 @@ "kimai/kimai": "<1.1", "kitodo/presentation": "<3.1.2", "klaviyo/magento2-extension": ">=1,<3", - "knplabs/knp-snappy": "<=1.4.1", + "knplabs/knp-snappy": "<1.4.2", "krayin/laravel-crm": "<1.2.2", "kreait/firebase-php": ">=3.2,<3.8.1", "la-haute-societe/tcpdf": "<6.2.22", @@ -15136,7 +15138,7 @@ "melisplatform/melis-front": "<5.0.1", "mezzio/mezzio-swoole": "<3.7|>=4,<4.3", "mgallegos/laravel-jqgrid": "<=1.3", - "microweber/microweber": "<=1.3.2", + "microweber/microweber": "<1.3.3", "miniorange/miniorange-saml": "<1.4.3", "mittwald/typo3_forum": "<1.2.1", "mobiledetect/mobiledetectlib": "<2.8.32", @@ -15204,6 +15206,7 @@ "phpxmlrpc/extras": "<0.6.1", "phpxmlrpc/phpxmlrpc": "<4.9.2", "pimcore/data-hub": "<1.2.4", + "pimcore/perspective-editor": "<1.5.1", "pimcore/pimcore": "<10.5.20", "pixelfed/pixelfed": "<=0.11.4", "pocketmine/bedrock-protocol": "<8.0.2", @@ -15230,6 +15233,7 @@ "rainlab/debugbar-plugin": "<3.1", "rankmath/seo-by-rank-math": "<=1.0.95", "react/http": ">=0.7,<1.7", + "really-simple-plugins/complianz-gdpr": "<6.4.2", "remdex/livehelperchat": "<3.99", "rmccue/requests": ">=1.6,<1.8", "robrichards/xmlseclibs": "<3.0.4", @@ -15339,7 +15343,7 @@ "thelia/thelia": ">=2.1-beta.1,<2.1.3", "theonedemon/phpwhois": "<=4.2.5", "thinkcmf/thinkcmf": "<=5.1.7", - "thorsten/phpmyfaq": "<3.1.11", + "thorsten/phpmyfaq": "<3.1.12", "tinymce/tinymce": "<5.10.7|>=6,<6.3.1", "tinymighty/wiki-seo": "<1.2.2", "titon/framework": ">=0,<9.9.99", @@ -15467,7 +15471,7 @@ "type": "tidelift" } ], - "time": "2023-03-30T21:04:19+00:00" + "time": "2023-04-06T17:04:19+00:00" }, { "name": "sebastian/diff", diff --git a/config/parameters.yaml b/config/parameters.yaml index 773b61e8..a7a23db3 100644 --- a/config/parameters.yaml +++ b/config/parameters.yaml @@ -12,6 +12,7 @@ parameters: partdb.default_currency: '%env(string:BASE_CURRENCY)%' # The currency that is used inside the DB (and is assumed when no currency is set). This can not be changed later, so be sure to set it the currency used in your country partdb.global_theme: '' # The theme to use globally (see public/build/themes/ for choices, use name without .css). Set to '' for default bootstrap theme partdb.locale_menu: ['en', 'de', 'fr', 'ru', 'ja'] # The languages that are shown in user drop down menu + partdb.enforce_change_comments_for: '%env(csv:ENFORCE_CHANGE_COMMENTS_FOR)%' # The actions for which a change comment is required (e.g. "part_edit", "part_create", etc.). If this is empty, change comments are not required at all. partdb.default_uri: '%env(string:DEFAULT_URI)%' # The default URI to use for the Part-DB instance (e.g. https://part-db.example.com/). This is used for generating links in emails @@ -105,6 +106,8 @@ parameters: env(USE_GRAVATAR): '0' env(MAX_ATTACHMENT_FILE_SIZE): '100M' + env(ENFORCE_CHANGE_COMMENTS_FOR): '' + env(ERROR_PAGE_ADMIN_EMAIL): '' env(ERROR_PAGE_SHOW_HELP): 1 diff --git a/config/services.yaml b/config/services.yaml index 961f6258..b075684a 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -102,6 +102,10 @@ services: 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%' + #################################################################################################################### # Attachment system #################################################################################################################### diff --git a/docs/configuration.md b/docs/configuration.md index 01121efe..1132f6a7 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -28,6 +28,14 @@ The following configuration options can only be changed by the server administra * `USE_GRAVATAR`: Set to `1` to use [gravatar.com](gravatar.com) images for user avatars (as long as they have not set their own picture). The users browsers have to download the pictures from a third-party (gravatars) server, so this might be a privacy risk. * `MAX_ATTACHMENT_FILE_SIZE`: The maximum file size (in bytes) for attachments. You can use the suffix `K`, `M` or `G` to specify the size in kilobytes, megabytes or gigabytes. By default `100M` (100 megabytes). Please note that this only the limit of Part-DB. You still need to configure the php.ini `upload_max_filesize` and `post_max_size` to allow bigger files to be uploaded. * `DEFAULT_URI`: The default URI base to use for the Part-DB, when no URL can be determined from the browser request. This should be the primary URL/Domain, which is used to access Part-DB. This value is used to create correct links in emails and other places, where the URL is needed. It is also used, when SAML is enabled.s If you are using a reverse proxy, you should set this to the URL of the reverse proxy (e.g. `https://part-db.example.com`). **This value must end with a slash**. +* `ENFORCE_CHANGE_COMMENTS_FOR`: With this option you can configure, where users are enforced to give a change reason, which will be written to the log. This is a comma separated list of values (e.g. `part_edit,part_delete`). Leave empty to make change comments optional everywhere. Possible values are: + * `part_edit`: Edit operation of a existing part + * `part_delete`: Delete operation of a existing part + * `part_create`: Creation of a new part + * `part_stock_operation`: Stock operation on a part (therefore withdraw, add or move stock) + * `datastructure_edit`: Edit operation of a existing datastructure (e.g. category, manufacturer, ...) + * `datastructure_delete`: Delete operation of a existing datastructure (e.g. category, manufacturer, ...) + * `datastructure_create`: Creation of a new datastructure (e.g. category, manufacturer, ...) ### E-Mail settings * `MAILER_DSN`: You can configure the mail provider which should be used for email delivery (see https://symfony.com/doc/current/components/mailer.html for full documentation). If you just want to use an SMTP mail account, you can use the following syntax `MAILER_DSN=smtp://user:password@smtp.mailserver.invalid:587` diff --git a/docs/usage/tips_tricks.md b/docs/usage/tips_tricks.md index 8f5d99f5..81a31ec1 100644 --- a/docs/usage/tips_tricks.md +++ b/docs/usage/tips_tricks.md @@ -64,6 +64,12 @@ If you call this command regularly (e.g. with a cronjob), you can keep the excha Please note that if you use a base currency, which is not the Euro, you have to configure an exchange rate API, as the free API used by default only supports the Euro as base currency. +## Enforce log comments +On almost any editing operation it is possible to add a comment describing, what or why you changed something. +This comment will be written to change log and can be viewed later. +If you want to enforce your users to add comments to certain operations, you can do this by setting the `ENFORCE_CHANGE_COMMENTS_FOR` option. +See the configuration reference for more information. + ## Personal stocks and stock locations For makerspaces and universities with a lot of users, where each user can have his own stock, which only he should be able to access, you can assign the user as "owner" of a part lot. This way, only him is allowed to add or remove parts from this lot. \ No newline at end of file diff --git a/migrations/Version20230402170923.php b/migrations/Version20230402170923.php index 2bb7acf3..016a10d0 100644 --- a/migrations/Version20230402170923.php +++ b/migrations/Version20230402170923.php @@ -27,7 +27,7 @@ final class Version20230402170923 extends AbstractMultiPlatformMigration $this->addSql('ALTER TABLE storelocations ADD id_owner INT DEFAULT NULL, ADD part_owner_must_match TINYINT(1) DEFAULT 0 NOT NULL'); $this->addSql('ALTER TABLE storelocations ADD CONSTRAINT FK_751702021E5A74C FOREIGN KEY (id_owner) REFERENCES `users` (id) ON DELETE SET NULL'); $this->addSql('CREATE INDEX IDX_751702021E5A74C ON storelocations (id_owner)'); - $this->addSql('ALTER TABLE users ADD about_me LONGTEXT NOT NULL'); + $this->addSql('ALTER TABLE `users` ADD about_me LONGTEXT NOT NULL'); $this->addSql('ALTER TABLE projects CHANGE description description LONGTEXT NOT NULL'); } diff --git a/migrations/Version20230408170059.php b/migrations/Version20230408170059.php new file mode 100644 index 00000000..9f686c38 --- /dev/null +++ b/migrations/Version20230408170059.php @@ -0,0 +1,52 @@ +addSql('ALTER TABLE `users` ADD show_email_on_profile TINYINT(1) DEFAULT 0 NOT NULL'); + } + + public function mySQLDown(Schema $schema): void + { + $this->addSql('ALTER TABLE `users` DROP show_email_on_profile'); + } + + public function sqLiteUp(Schema $schema): void + { + $this->addSql('ALTER TABLE users ADD COLUMN show_email_on_profile BOOLEAN DEFAULT 0 NOT NULL'); + } + + public function sqLiteDown(Schema $schema): void + { + $this->addSql('CREATE TEMPORARY TABLE __temp__users AS SELECT id, group_id, currency_id, id_preview_attachment, disabled, config_theme, pw_reset_token, config_instock_comment_a, config_instock_comment_w, about_me, trusted_device_cookie_version, backup_codes, google_authenticator_secret, config_timezone, config_language, email, department, last_name, first_name, need_pw_change, password, name, settings, backup_codes_generation_date, pw_reset_expires, saml_user, last_modified, datetime_added, permissions_data FROM "users"'); + $this->addSql('DROP TABLE "users"'); + $this->addSql('CREATE TABLE "users" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, group_id INTEGER DEFAULT NULL, currency_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, disabled BOOLEAN NOT NULL, config_theme VARCHAR(255) DEFAULT NULL, pw_reset_token VARCHAR(255) DEFAULT NULL, config_instock_comment_a CLOB NOT NULL, config_instock_comment_w CLOB NOT NULL, about_me CLOB DEFAULT \'\' NOT NULL, trusted_device_cookie_version INTEGER NOT NULL, backup_codes CLOB NOT NULL --(DC2Type:json) + , google_authenticator_secret VARCHAR(255) DEFAULT NULL, config_timezone VARCHAR(255) DEFAULT NULL, config_language VARCHAR(255) DEFAULT NULL, email VARCHAR(255) DEFAULT NULL, department VARCHAR(255) DEFAULT NULL, last_name VARCHAR(255) DEFAULT NULL, first_name VARCHAR(255) DEFAULT NULL, need_pw_change BOOLEAN NOT NULL, password VARCHAR(255) DEFAULT NULL, name VARCHAR(180) NOT NULL, settings CLOB NOT NULL --(DC2Type:json) + , backup_codes_generation_date DATETIME DEFAULT NULL, pw_reset_expires DATETIME DEFAULT NULL, saml_user BOOLEAN NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, permissions_data CLOB DEFAULT \'[]\' NOT NULL --(DC2Type:json) + , CONSTRAINT FK_1483A5E9FE54D947 FOREIGN KEY (group_id) REFERENCES "groups" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_1483A5E938248176 FOREIGN KEY (currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_1483A5E9EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO "users" (id, group_id, currency_id, id_preview_attachment, disabled, config_theme, pw_reset_token, config_instock_comment_a, config_instock_comment_w, about_me, trusted_device_cookie_version, backup_codes, google_authenticator_secret, config_timezone, config_language, email, department, last_name, first_name, need_pw_change, password, name, settings, backup_codes_generation_date, pw_reset_expires, saml_user, last_modified, datetime_added, permissions_data) SELECT id, group_id, currency_id, id_preview_attachment, disabled, config_theme, pw_reset_token, config_instock_comment_a, config_instock_comment_w, about_me, trusted_device_cookie_version, backup_codes, google_authenticator_secret, config_timezone, config_language, email, department, last_name, first_name, need_pw_change, password, name, settings, backup_codes_generation_date, pw_reset_expires, saml_user, last_modified, datetime_added, permissions_data FROM __temp__users'); + $this->addSql('DROP TABLE __temp__users'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_1483A5E95E237E06 ON "users" (name)'); + $this->addSql('CREATE INDEX IDX_1483A5E9FE54D947 ON "users" (group_id)'); + $this->addSql('CREATE INDEX IDX_1483A5E938248176 ON "users" (currency_id)'); + $this->addSql('CREATE INDEX IDX_1483A5E9EA7100A1 ON "users" (id_preview_attachment)'); + $this->addSql('CREATE INDEX user_idx_username ON "users" (name)'); + } +} diff --git a/src/Command/Logs/ShowEventLogCommand.php b/src/Command/Logs/ShowEventLogCommand.php index d3ac5c7f..7eef7a2d 100644 --- a/src/Command/Logs/ShowEventLogCommand.php +++ b/src/Command/Logs/ShowEventLogCommand.php @@ -147,11 +147,21 @@ class ShowEventLogCommand extends Command $target_class = $this->elementTypeNameGenerator->getLocalizedTypeLabel($entry->getTargetClass()); } + if ($entry->getUser()) { + $user = $entry->getUser()->getFullName(true); + } else { + if ($entry->isCLIEntry()) { + $user = $entry->getCLIUsername() . ' [CLI]'; + } else { + $user = $entry->getUsername() . ' [deleted]'; + } + } + $row = [ $entry->getID(), $entry->getTimestamp()->format('Y-m-d H:i:s'), $entry->getType(), - $entry->getUser()->getFullName(true), + $user, $target_class, $target_name, ]; diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 6dad4159..9949b8c7 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -202,21 +202,24 @@ class UserController extends AdminPages\BaseAdminController $user = $tmp; } else { //Else we must check, if the current user is allowed to access $user - $this->denyAccessUnlessGranted('read', $user); + $this->denyAccessUnlessGranted('info', $user); } - $table = $this->dataTableFactory->createFromType( - LogDataTable::class, - [ - 'filter_elements' => $user, - 'mode' => 'element_history', - ], - ['pageLength' => 10] - ) - ->handleRequest($request); + //Only show the history table, if the user is the current user + if ($user === $this->getUser()) { + $table = $this->dataTableFactory->createFromType( + LogDataTable::class, + [ + 'filter_elements' => $user, + 'mode' => 'element_history', + ], + ['pageLength' => 10] + ) + ->handleRequest($request); - if ($table->isCallback()) { - return $table->getResponse(); + if ($table->isCallback()) { + return $table->getResponse(); + } } //Show permissions to user @@ -230,7 +233,7 @@ class UserController extends AdminPages\BaseAdminController return $this->renderForm('users/user_info.html.twig', [ 'user' => $user, 'form' => $builder->getForm(), - 'datatable' => $table, + 'datatable' => $table ?? null, ]); } } diff --git a/src/DataTables/Filters/Constraints/Part/LessThanDesiredConstraint.php b/src/DataTables/Filters/Constraints/Part/LessThanDesiredConstraint.php new file mode 100644 index 00000000..34d5c157 --- /dev/null +++ b/src/DataTables/Filters/Constraints/Part/LessThanDesiredConstraint.php @@ -0,0 +1,47 @@ +. + */ + +namespace App\DataTables\Filters\Constraints\Part; + +use App\DataTables\Filters\Constraints\BooleanConstraint; +use Doctrine\ORM\QueryBuilder; + +class LessThanDesiredConstraint extends BooleanConstraint +{ + public function __construct(string $property = null, string $identifier = null, ?bool $default_value = null) + { + parent::__construct($property ?? 'amountSum', $identifier, $default_value); + } + + public function apply(QueryBuilder $queryBuilder): void + { + //Do not apply a filter if value is null (filter is set to ignore) + if(!$this->isEnabled()) { + return; + } + + //If value is true, we want to filter for parts with stock < desired stock + if ($this->value) { + $queryBuilder->andHaving('amountSum < part.minamount'); + } else { + $queryBuilder->andHaving('amountSum >= part.minamount'); + } + } +} \ No newline at end of file diff --git a/src/DataTables/Filters/PartFilter.php b/src/DataTables/Filters/PartFilter.php index dabf005e..14ab1a9c 100644 --- a/src/DataTables/Filters/PartFilter.php +++ b/src/DataTables/Filters/PartFilter.php @@ -26,6 +26,7 @@ use App\DataTables\Filters\Constraints\DateTimeConstraint; use App\DataTables\Filters\Constraints\EntityConstraint; use App\DataTables\Filters\Constraints\IntConstraint; use App\DataTables\Filters\Constraints\NumberConstraint; +use App\DataTables\Filters\Constraints\Part\LessThanDesiredConstraint; use App\DataTables\Filters\Constraints\Part\ParameterConstraint; use App\DataTables\Filters\Constraints\Part\TagsConstraint; use App\DataTables\Filters\Constraints\TextConstraint; @@ -36,6 +37,8 @@ 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 App\Form\Filters\Constraints\UserEntityConstraintType; use App\Services\Trees\NodesListBuilder; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\QueryBuilder; @@ -68,10 +71,14 @@ class PartFilter implements FilterInterface protected EntityConstraint $storelocation; protected IntConstraint $lotCount; protected IntConstraint $amountSum; + protected LessThanDesiredConstraint $lessThanDesired; + protected BooleanConstraint $lotNeedsRefill; protected TextConstraint $lotDescription; protected BooleanConstraint $lotUnknownAmount; protected DateTimeConstraint $lotExpirationDate; + protected EntityConstraint $lotOwner; + protected EntityConstraint $measurementUnit; protected TextConstraint $manufacturer_product_url; protected TextConstraint $manufacturer_product_number; @@ -108,12 +115,14 @@ class PartFilter implements FilterInterface //We have to use Having here, as we use an alias column which is not supported on the where clause and would result in an error $this->amountSum = (new IntConstraint('amountSum'))->useHaving(); $this->lotCount = new IntConstraint('COUNT(partLots)'); + $this->lessThanDesired = new LessThanDesiredConstraint(); $this->storelocation = new EntityConstraint($nodesListBuilder, Storelocation::class, 'partLots.storage_location'); $this->lotNeedsRefill = new BooleanConstraint('partLots.needs_refill'); $this->lotUnknownAmount = new BooleanConstraint('partLots.instock_unknown'); $this->lotExpirationDate = new DateTimeConstraint('partLots.expiration_date'); $this->lotDescription = new TextConstraint('partLots.description'); + $this->lotOwner = new EntityConstraint($nodesListBuilder, User::class, 'partLots.owner'); $this->manufacturer = new EntityConstraint($nodesListBuilder, Manufacturer::class, 'part.manufacturer'); $this->manufacturer_product_number = new TextConstraint('part.manufacturer_product_number'); @@ -280,6 +289,14 @@ class PartFilter implements FilterInterface return $this->lotCount; } + /** + * @return EntityConstraint + */ + public function getLotOwner(): EntityConstraint + { + return $this->lotOwner; + } + /** * @return TagsConstraint */ @@ -383,7 +400,13 @@ class PartFilter implements FilterInterface return $this->obsolete; } - + /** + * @return LessThanDesiredConstraint + */ + public function getLessThanDesired(): LessThanDesiredConstraint + { + return $this->lessThanDesired; + } } diff --git a/src/DataTables/LogDataTable.php b/src/DataTables/LogDataTable.php index 5d23446a..85ab3113 100644 --- a/src/DataTables/LogDataTable.php +++ b/src/DataTables/LogDataTable.php @@ -226,6 +226,14 @@ class LogDataTable implements DataTableTypeInterface //If user was deleted, show the info from the username field if ($user === null) { + if ($context->isCLIEntry()) { + return sprintf('%s [%s]', + htmlentities($context->getCLIUsername()), + $this->translator->trans('log.cli_user') + ); + } + + //Else we just deal with a deleted user return sprintf( '@%s [%s]', htmlentities($context->getUsername()), diff --git a/src/Entity/LogSystem/AbstractLogEntry.php b/src/Entity/LogSystem/AbstractLogEntry.php index e2dca513..5b6a728b 100644 --- a/src/Entity/LogSystem/AbstractLogEntry.php +++ b/src/Entity/LogSystem/AbstractLogEntry.php @@ -216,6 +216,41 @@ abstract class AbstractLogEntry extends AbstractDBElement return $this; } + /** + * Returns true if this log entry was created by a CLI command, false otherwise. + * @return bool + */ + public function isCLIEntry(): bool + { + return strpos($this->username, '!!!CLI ') === 0; + } + + /** + * Marks this log entry as a CLI entry, and set the username of the CLI user. + * This removes the association to a user object in database, as CLI users are not really related to logged in + * Part-DB users. + * @param string $cli_username + * @return $this + */ + public function setCLIUsername(string $cli_username): self + { + $this->user = null; + $this->username = '!!!CLI ' . $cli_username; + return $this; + } + + /** + * Retrieves the username of the CLI user that caused the event. + * @return string|null The username of the CLI user, or null if this log entry was not created by a CLI command. + */ + public function getCLIUsername(): ?string + { + if ($this->isCLIEntry()) { + return substr($this->username, 7); + } + return null; + } + /** * Retuns the username of the user that caused the event (useful if the user was deleted). * diff --git a/src/Entity/Parts/PartLot.php b/src/Entity/Parts/PartLot.php index e3f0fcef..32db4828 100644 --- a/src/Entity/Parts/PartLot.php +++ b/src/Entity/Parts/PartLot.php @@ -343,6 +343,13 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named */ public function validate(ExecutionContextInterface $context, $payload) { + //Ensure that the owner is not the anonymous user + if ($this->getOwner() && $this->getOwner()->isAnonymousUser()) { + $context->buildViolation('validator.part_lot.owner_must_not_be_anonymous') + ->atPath('owner') + ->addViolation(); + } + //When the storage location sets the owner must match, the part lot owner must match the storage location owner if ($this->getStorageLocation() && $this->getStorageLocation()->isPartOwnerMustMatch() && $this->getStorageLocation()->getOwner() && $this->getOwner()) { diff --git a/src/Entity/Parts/Storelocation.php b/src/Entity/Parts/Storelocation.php index 6ea009e2..0d90f96a 100644 --- a/src/Entity/Parts/Storelocation.php +++ b/src/Entity/Parts/Storelocation.php @@ -94,6 +94,7 @@ class Storelocation extends AbstractPartsContainingDBElement * @var User|null The owner of this storage location * @ORM\ManyToOne(targetEntity="App\Entity\UserSystem\User") * @ORM\JoinColumn(name="id_owner", referencedColumnName="id", nullable=true, onDelete="SET NULL") + * @Assert\Expression("this.getOwner() == null or this.getOwner().isAnonymousUser() === false", message="validator.part_lot.owner_must_not_be_anonymous") */ protected ?User $owner = null; diff --git a/src/Entity/UserSystem/User.php b/src/Entity/UserSystem/User.php index a468d355..57ac1f43 100644 --- a/src/Entity/UserSystem/User.php +++ b/src/Entity/UserSystem/User.php @@ -168,6 +168,12 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe */ protected ?string $email = ''; + /** + * @var bool True if the user wants to show his email address on his (public) profile + * @ORM\Column(type="boolean", options={"default": false}) + */ + protected bool $show_email_on_profile = false; + /** * @var string|null The department the user is working * @ORM\Column(type="string", length=255, nullable=true) @@ -632,6 +638,28 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe return $this; } + /** + * Gets whether the email address of the user is shown on the public profile page. + * @return bool + */ + public function isShowEmailOnProfile(): bool + { + return $this->show_email_on_profile; + } + + /** + * Sets whether the email address of the user is shown on the public profile page. + * @param bool $show_email_on_profile + * @return User + */ + public function setShowEmailOnProfile(bool $show_email_on_profile): User + { + $this->show_email_on_profile = $show_email_on_profile; + return $this; + } + + + /** * Returns the about me text of the user. * @return string diff --git a/src/Form/AdminPages/AttachmentTypeAdminForm.php b/src/Form/AdminPages/AttachmentTypeAdminForm.php index 75174279..57ba6fed 100644 --- a/src/Form/AdminPages/AttachmentTypeAdminForm.php +++ b/src/Form/AdminPages/AttachmentTypeAdminForm.php @@ -24,6 +24,7 @@ namespace App\Form\AdminPages; use App\Entity\Base\AbstractNamedDBElement; use App\Services\Attachments\FileTypeFilterTools; +use App\Services\LogSystem\EventCommentNeededHelper; use Symfony\Component\Form\CallbackTransformer; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; @@ -33,10 +34,10 @@ class AttachmentTypeAdminForm extends BaseEntityAdminForm { protected FileTypeFilterTools $filterTools; - public function __construct(Security $security, FileTypeFilterTools $filterTools) + public function __construct(Security $security, FileTypeFilterTools $filterTools, EventCommentNeededHelper $eventCommentNeededHelper) { $this->filterTools = $filterTools; - parent::__construct($security); + parent::__construct($security, $eventCommentNeededHelper); } protected function additionalFormElements(FormBuilderInterface $builder, array $options, AbstractNamedDBElement $entity): void diff --git a/src/Form/AdminPages/BaseEntityAdminForm.php b/src/Form/AdminPages/BaseEntityAdminForm.php index a28e0211..1a95a119 100644 --- a/src/Form/AdminPages/BaseEntityAdminForm.php +++ b/src/Form/AdminPages/BaseEntityAdminForm.php @@ -31,6 +31,7 @@ use App\Form\ParameterType; use App\Form\Type\MasterPictureAttachmentType; use App\Form\Type\RichTextEditorType; use App\Form\Type\StructuralEntityType; +use App\Services\LogSystem\EventCommentNeededHelper; use FOS\CKEditorBundle\Form\Type\CKEditorType; use function get_class; use Symfony\Component\Form\AbstractType; @@ -46,10 +47,12 @@ use Symfony\Component\Security\Core\Security; class BaseEntityAdminForm extends AbstractType { protected Security $security; + protected EventCommentNeededHelper $eventCommentNeededHelper; - public function __construct(Security $security) + public function __construct(Security $security, EventCommentNeededHelper $eventCommentNeededHelper) { $this->security = $security; + $this->eventCommentNeededHelper = $eventCommentNeededHelper; } public function configureOptions(OptionsResolver $resolver): void @@ -141,7 +144,7 @@ class BaseEntityAdminForm extends AbstractType $builder->add('log_comment', TextType::class, [ 'label' => 'edit.log_comment', 'mapped' => false, - 'required' => false, + 'required' => $this->eventCommentNeededHelper->isCommentNeeded($is_new ? 'datastructure_create': 'datastructure_edit'), 'empty_data' => null, ]); diff --git a/src/Form/AdminPages/CurrencyAdminForm.php b/src/Form/AdminPages/CurrencyAdminForm.php index 19123465..754d7c66 100644 --- a/src/Form/AdminPages/CurrencyAdminForm.php +++ b/src/Form/AdminPages/CurrencyAdminForm.php @@ -24,6 +24,7 @@ namespace App\Form\AdminPages; use App\Entity\Base\AbstractNamedDBElement; use App\Form\Type\BigDecimalMoneyType; +use App\Services\LogSystem\EventCommentNeededHelper; use Symfony\Component\Form\Extension\Core\Type\CurrencyType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\FormBuilderInterface; @@ -33,9 +34,9 @@ class CurrencyAdminForm extends BaseEntityAdminForm { private string $default_currency; - public function __construct(Security $security, string $default_currency) + public function __construct(Security $security, EventCommentNeededHelper $eventCommentNeededHelper, string $default_currency) { - parent::__construct($security); + parent::__construct($security, $eventCommentNeededHelper); $this->default_currency = $default_currency; } diff --git a/src/Form/AdminPages/SupplierForm.php b/src/Form/AdminPages/SupplierForm.php index 95cecfd3..db798db8 100644 --- a/src/Form/AdminPages/SupplierForm.php +++ b/src/Form/AdminPages/SupplierForm.php @@ -26,6 +26,7 @@ use App\Entity\Base\AbstractNamedDBElement; use App\Entity\PriceInformations\Currency; use App\Form\Type\BigDecimalMoneyType; use App\Form\Type\StructuralEntityType; +use App\Services\LogSystem\EventCommentNeededHelper; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Security\Core\Security; @@ -33,9 +34,9 @@ class SupplierForm extends CompanyForm { protected string $default_currency; - public function __construct(Security $security, string $default_currency) + public function __construct(Security $security, EventCommentNeededHelper $eventCommentNeededHelper, string $default_currency) { - parent::__construct($security); + parent::__construct($security, $eventCommentNeededHelper); $this->default_currency = $default_currency; } diff --git a/src/Form/Filters/Constraints/BooleanConstraintType.php b/src/Form/Filters/Constraints/BooleanConstraintType.php index e04e88d3..ebc5ce09 100644 --- a/src/Form/Filters/Constraints/BooleanConstraintType.php +++ b/src/Form/Filters/Constraints/BooleanConstraintType.php @@ -24,6 +24,8 @@ use App\DataTables\Filters\Constraints\BooleanConstraint; use App\Form\Type\TriStateCheckboxType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; use Symfony\Component\OptionsResolver\OptionsResolver; class BooleanConstraintType extends AbstractType @@ -43,4 +45,10 @@ class BooleanConstraintType extends AbstractType 'required' => false, ]); } + + public function finishView(FormView $view, FormInterface $form, array $options) + { + //Remove the label from the compound form, as the checkbox already has a label + $view->vars['label'] = false; + } } \ No newline at end of file diff --git a/src/Form/Filters/PartFilterType.php b/src/Form/Filters/PartFilterType.php index 347948a2..249b0c1c 100644 --- a/src/Form/Filters/PartFilterType.php +++ b/src/Form/Filters/PartFilterType.php @@ -36,6 +36,7 @@ use App\Form\Filters\Constraints\ParameterConstraintType; use App\Form\Filters\Constraints\StructuralEntityConstraintType; use App\Form\Filters\Constraints\TagsConstraintType; use App\Form\Filters\Constraints\TextConstraintType; +use App\Form\Filters\Constraints\UserEntityConstraintType; use Svg\Tag\Text; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\CollectionType; @@ -206,6 +207,10 @@ class PartFilterType extends AbstractType 'min' => 0, ]); + $builder->add('lessThanDesired', BooleanConstraintType::class, [ + 'label' => 'part.filter.lessThanDesired' + ]); + $builder->add('lotNeedsRefill', BooleanConstraintType::class, [ 'label' => 'part.filter.lotNeedsRefill' ]); @@ -223,6 +228,10 @@ class PartFilterType extends AbstractType 'label' => 'part.filter.lotDescription', ]); + $builder->add('lotOwner', UserEntityConstraintType::class, [ + 'label' => 'part.filter.lotOwner', + ]); + /** * Attachments count */ diff --git a/src/Form/Part/PartBaseType.php b/src/Form/Part/PartBaseType.php index 3951a2ac..ef9bd60c 100644 --- a/src/Form/Part/PartBaseType.php +++ b/src/Form/Part/PartBaseType.php @@ -37,6 +37,7 @@ use App\Form\Type\RichTextEditorType; use App\Form\Type\SIUnitType; use App\Form\Type\StructuralEntityType; use App\Form\WorkaroundCollectionType; +use App\Services\LogSystem\EventCommentNeededHelper; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; @@ -54,17 +55,20 @@ class PartBaseType extends AbstractType { protected Security $security; protected UrlGeneratorInterface $urlGenerator; + protected EventCommentNeededHelper $event_comment_needed_helper; - public function __construct(Security $security, UrlGeneratorInterface $urlGenerator) + public function __construct(Security $security, UrlGeneratorInterface $urlGenerator, EventCommentNeededHelper $event_comment_needed_helper) { $this->security = $security; $this->urlGenerator = $urlGenerator; + $this->event_comment_needed_helper = $event_comment_needed_helper; } public function buildForm(FormBuilderInterface $builder, array $options): void { /** @var Part $part */ $part = $builder->getData(); + $new_part = null === $part->getID(); $status_choices = [ 'm_status.unknown' => '', @@ -250,7 +254,7 @@ class PartBaseType extends AbstractType $builder->add('log_comment', TextType::class, [ 'label' => 'edit.log_comment', 'mapped' => false, - 'required' => false, + 'required' => $this->event_comment_needed_helper->isCommentNeeded($new_part ? 'part_create' : 'part_edit'), 'empty_data' => null, ]); diff --git a/src/Form/UserAdminForm.php b/src/Form/UserAdminForm.php index 9f97d8f3..ce9cab04 100644 --- a/src/Form/UserAdminForm.php +++ b/src/Form/UserAdminForm.php @@ -117,7 +117,11 @@ class UserAdminForm extends AbstractType 'required' => false, 'disabled' => !$this->security->isGranted('edit_infos', $entity), ]) - + ->add('showEmailOnProfile', CheckboxType::class, [ + 'required' => false, + 'label' => 'user.show_email_on_profile.label', + 'disabled' => !$this->security->isGranted('edit_infos', $entity), + ]) ->add('department', TextType::class, [ 'empty_data' => '', 'label' => 'user.department.label', diff --git a/src/Form/UserSettingsType.php b/src/Form/UserSettingsType.php index c54e90e9..cf75b0f8 100644 --- a/src/Form/UserSettingsType.php +++ b/src/Form/UserSettingsType.php @@ -28,6 +28,7 @@ use App\Form\Type\RichTextEditorType; use App\Form\Type\ThemeChoiceType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Event\PreSetDataEvent; +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\EmailType; use Symfony\Component\Form\Extension\Core\Type\FileType; @@ -80,6 +81,11 @@ class UserSettingsType extends AbstractType 'label' => 'user.email.label', 'disabled' => !$this->security->isGranted('edit_infos', $options['data']) || $this->demo_mode, ]) + ->add('showEmailOnProfile', CheckboxType::class, [ + 'required' => false, + 'label' => 'user.show_email_on_profile.label', + 'disabled' => !$this->security->isGranted('edit_infos', $options['data']) || $this->demo_mode, + ]) ->add('avatar_file', FileType::class, [ 'label' => 'user_settings.change_avatar.label', 'mapped' => false, diff --git a/src/Security/Voter/UserVoter.php b/src/Security/Voter/UserVoter.php index dcd7cb20..a311e4db 100644 --- a/src/Security/Voter/UserVoter.php +++ b/src/Security/Voter/UserVoter.php @@ -38,10 +38,13 @@ class UserVoter extends ExtendedVoter protected function supports(string $attribute, $subject): bool { if (is_a($subject, User::class, true)) { - return in_array($attribute, array_merge( - $this->resolver->listOperationsForPermission('users'), - $this->resolver->listOperationsForPermission('self')), - false + return in_array($attribute, + array_merge( + $this->resolver->listOperationsForPermission('users'), + $this->resolver->listOperationsForPermission('self'), + ['info'] + ), + false ); } @@ -56,6 +59,16 @@ class UserVoter extends ExtendedVoter */ protected function voteOnUser(string $attribute, $subject, User $user): bool { + if ($attribute === 'info') { + //Every logged-in user (non-anonymous) can see the info pages of other users + if (!$user->isAnonymousUser()) { + return true; + } + + //For the anonymous user, use the user read permission + $attribute = 'read'; + } + //Check if the checked user is the user itself if (($subject instanceof User) && $subject->getID() === $user->getID() && $this->resolver->isValidOperation('self', $attribute)) { diff --git a/src/Services/LogSystem/EventCommentNeededHelper.php b/src/Services/LogSystem/EventCommentNeededHelper.php new file mode 100644 index 00000000..7305b304 --- /dev/null +++ b/src/Services/LogSystem/EventCommentNeededHelper.php @@ -0,0 +1,60 @@ +. + */ + +namespace App\Services\LogSystem; + +/** + * This service is used to check if a log change comment is needed for a given operation type. + * It is configured using the "enforce_change_comments_for" config parameter. + */ +class EventCommentNeededHelper +{ + protected array $enforce_change_comments_for; + + public const VALID_OPERATION_TYPES = [ + 'part_edit', + 'part_create', + 'part_delete', + 'part_stock_operation', + 'datastructure_edit', + 'datastructure_create', + 'datastructure_delete', + ]; + + public function __construct(array $enforce_change_comments_for) + { + $this->enforce_change_comments_for = $enforce_change_comments_for; + } + + /** + * Checks if a log change comment is needed for the given operation type + * @param string $comment_type + * @return bool + */ + public function isCommentNeeded(string $comment_type): bool + { + //Check if the comment type is valid + if (! in_array($comment_type, self::VALID_OPERATION_TYPES, true)) { + throw new \InvalidArgumentException('The comment type "'.$comment_type.'" is not valid!'); + } + + return in_array($comment_type, $this->enforce_change_comments_for, true); + } +} \ No newline at end of file diff --git a/src/Services/LogSystem/EventLogger.php b/src/Services/LogSystem/EventLogger.php index 80ee067e..8155819b 100644 --- a/src/Services/LogSystem/EventLogger.php +++ b/src/Services/LogSystem/EventLogger.php @@ -24,6 +24,7 @@ namespace App\Services\LogSystem; use App\Entity\LogSystem\AbstractLogEntry; use App\Entity\UserSystem\User; +use App\Services\Misc\ConsoleInfoHelper; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Security\Core\Security; @@ -34,14 +35,17 @@ class EventLogger protected array $whitelist; protected EntityManagerInterface $em; protected Security $security; + protected ConsoleInfoHelper $console_info_helper; - public function __construct(int $minimum_log_level, array $blacklist, array $whitelist, EntityManagerInterface $em, Security $security) + public function __construct(int $minimum_log_level, array $blacklist, array $whitelist, EntityManagerInterface $em, + Security $security, ConsoleInfoHelper $console_info_helper) { $this->minimum_log_level = $minimum_log_level; $this->blacklist = $blacklist; $this->whitelist = $whitelist; $this->em = $em; $this->security = $security; + $this->console_info_helper = $console_info_helper; } /** @@ -67,6 +71,11 @@ class EventLogger $logEntry->setUser($user); } + //Set the console user info, if the log entry was created in a console command + if ($this->console_info_helper->isCLI()) { + $logEntry->setCLIUsername($this->console_info_helper->getCLIUser() ?? 'Unknown'); + } + if ($this->shouldBeAdded($logEntry)) { $this->em->persist($logEntry); diff --git a/src/Services/Misc/ConsoleInfoHelper.php b/src/Services/Misc/ConsoleInfoHelper.php new file mode 100644 index 00000000..8aea004e --- /dev/null +++ b/src/Services/Misc/ConsoleInfoHelper.php @@ -0,0 +1,63 @@ +. + */ + +namespace App\Services\Misc; + +class ConsoleInfoHelper +{ + /** + * Returns true if the current script is executed in a CLI environment. + * @return bool true if the current script is executed in a CLI environment, false otherwise + */ + public function isCLI(): bool + { + return \in_array(\PHP_SAPI, ['cli', 'phpdbg'], true); + } + + /** + * Returns the username of the user who started the current script if possible. + * @return string|null the username of the user who started the current script if possible, null otherwise + */ + public function getCLIUser(): ?string + { + if (!$this->isCLI()) { + return null; + } + + //Try to use the posix extension if available (Linux) + if (function_exists('posix_getpwuid') && function_exists('posix_geteuid')) { + $user = posix_getpwuid(posix_geteuid()); + return $user['name']; + } + + //Try to retrieve the name via the environment variable Username (Windows) + if (isset($_SERVER['USERNAME'])) { + return $_SERVER['USERNAME']; + } + + //Try to retrieve the name via the environment variable USER (Linux) + if (isset($_SERVER['USER'])) { + return $_SERVER['USER']; + } + + //Otherwise we can't determine the username + return null; + } +} \ No newline at end of file diff --git a/src/Twig/MiscExtension.php b/src/Twig/MiscExtension.php new file mode 100644 index 00000000..e154ccf8 --- /dev/null +++ b/src/Twig/MiscExtension.php @@ -0,0 +1,43 @@ +. + */ + +namespace App\Twig; + +use App\Services\LogSystem\EventCommentNeededHelper; +use Twig\Extension\AbstractExtension; + +final class MiscExtension extends AbstractExtension +{ + private EventCommentNeededHelper $eventCommentNeededHelper; + + public function __construct(EventCommentNeededHelper $eventCommentNeededHelper) + { + $this->eventCommentNeededHelper = $eventCommentNeededHelper; + } + + public function getFunctions() + { + return [ + new \Twig\TwigFunction('event_comment_needed', + fn(string $operation_type) => $this->eventCommentNeededHelper->isCommentNeeded($operation_type) + ), + ]; + } +} \ No newline at end of file diff --git a/templates/admin/_delete_form.html.twig b/templates/admin/_delete_form.html.twig index 0423bdca..762b91b6 100644 --- a/templates/admin/_delete_form.html.twig +++ b/templates/admin/_delete_form.html.twig @@ -14,7 +14,8 @@
diff --git a/templates/admin/user_admin.html.twig b/templates/admin/user_admin.html.twig index e24ce36d..ccbd7b0a 100644 --- a/templates/admin/user_admin.html.twig +++ b/templates/admin/user_admin.html.twig @@ -21,6 +21,7 @@ {{ form_row(form.first_name) }} {{ form_row(form.last_name) }} {{ form_row(form.email) }} + {{ form_row(form.showEmailOnProfile) }} {{ form_row(form.department) }} {{ form_row(form.aboutMe) }} {% endblock %} diff --git a/templates/parts/edit/edit_part_info.html.twig b/templates/parts/edit/edit_part_info.html.twig index 5e5dc243..51b5d865 100644 --- a/templates/parts/edit/edit_part_info.html.twig +++ b/templates/parts/edit/edit_part_info.html.twig @@ -106,9 +106,9 @@ {{ form_widget(form.save_and_clone, {'attr': {'class': 'dropdown-item'}}) }} {{ form_widget(form.save_and_new, {'attr': {'class': 'dropdown-item'}}) }} -{{ user.email }}
#} - {{ user.email }} + {% if user.showEmailOnProfile %} + {{ user.email }} + {% else %} + - + {% endif %}