From cba5271b275429fa7e54bd8a0a9d238f25c46eec Mon Sep 17 00:00:00 2001 From: roadiz-ci Date: Mon, 16 Sep 2024 13:42:57 +0000 Subject: [PATCH] chore: bumped --- .github/workflows/run-test.yml | 2 +- .gitignore | 1 + .travis.yml | 75 + .travis/backoffice_assets.sh | 5 + .travis/composer_install.sh | 4 + .travis/php_lint.sh | 5 + LICENSE.md | 2 +- composer.json | 61 +- phpstan.neon | 9 +- src/.babelrc | 2 +- src/.editorconfig | 1 + src/.eslintrc.js | 2 +- .../AbstractAjaxController.php | 2 +- .../AjaxAbstractFieldsController.php | 13 +- .../AjaxAttributeValuesController.php | 58 +- .../AjaxCustomFormFieldsController.php | 7 +- .../AjaxCustomFormsExplorerController.php | 20 +- .../AjaxDocumentsExplorerController.php | 31 +- .../AjaxEntitiesExplorerController.php | 54 +- .../AjaxExplorerProviderController.php | 74 +- .../AjaxFolderTreeController.php | 8 +- src/AjaxControllers/AjaxFoldersController.php | 25 +- .../AjaxFoldersExplorerController.php | 10 +- .../AjaxNodeTreeController.php | 11 +- .../AjaxNodeTypeFieldsController.php | 3 + .../AjaxNodeTypesController.php | 11 +- src/AjaxControllers/AjaxNodesController.php | 161 +- .../AjaxNodesExplorerController.php | 66 +- .../AjaxSearchNodesSourcesController.php | 26 +- src/AjaxControllers/AjaxTagTreeController.php | 5 +- src/AjaxControllers/AjaxTagsController.php | 14 +- src/Controllers/AbstractAdminController.php | 120 +- .../AbstractAdminWithBulkController.php | 7 +- .../Attributes/AttributeController.php | 41 +- .../Attributes/AttributeGroupController.php | 3 + src/Controllers/CacheController.php | 12 +- .../CustomFormAnswersController.php | 5 +- .../CustomFormFieldAttributesController.php | 7 +- .../CustomFormFieldsController.php | 16 +- .../CustomForms/CustomFormsController.php | 47 +- .../CustomFormsUtilsController.php | 87 +- src/Controllers/DashboardController.php | 5 +- .../DocumentTranslationsController.php | 128 +- .../Documents/DocumentsController.php | 129 +- src/Controllers/FoldersController.php | 21 +- src/Controllers/GroupsController.php | 11 +- src/Controllers/GroupsUtilsController.php | 18 +- src/Controllers/HistoryController.php | 28 +- src/Controllers/LoginController.php | 14 +- src/Controllers/NodeTypeFieldsController.php | 131 +- .../NodeTypes/NodeTypesController.php | 78 +- .../NodeTypes/NodeTypesUtilsController.php | 41 +- src/Controllers/Nodes/ExportController.php | 22 +- src/Controllers/Nodes/HistoryController.php | 31 +- .../Nodes/NodesAttributesController.php | 90 +- src/Controllers/Nodes/NodesController.php | 154 +- .../Nodes/NodesSourcesController.php | 37 +- src/Controllers/Nodes/NodesTagsController.php | 104 ++ .../Nodes/NodesTreesController.php | 238 +-- .../Nodes/NodesUtilsController.php | 29 +- src/Controllers/Nodes/TranstypeController.php | 30 +- .../Nodes/UrlAliasesController.php | 380 +++++ src/Controllers/PingController.php | 26 + src/Controllers/RedirectionsController.php | 35 +- src/Controllers/RolesUtilsController.php | 18 +- src/Controllers/SearchController.php | 88 +- src/Controllers/SettingsController.php | 32 +- src/Controllers/SettingsUtilsController.php | 21 +- .../Tags/TagMultiCreationController.php | 102 +- src/Controllers/Tags/TagsController.php | 106 +- src/Controllers/Tags/TagsUtilsController.php | 26 +- src/Controllers/TranslationsController.php | 17 +- src/Controllers/Users/UsersController.php | 384 ++--- .../Users/UsersGroupsController.php | 140 +- .../Users/UsersRolesController.php | 4 +- .../Users/UsersSecurityController.php | 2 +- src/Controllers/WebhookController.php | 17 +- src/Event/UserActionsMenuEvent.php | 29 - src/Explorer/ConfigurableExplorerItem.php | 56 +- src/Explorer/FolderExplorerItem.php | 19 +- src/Explorer/SettingExplorerItem.php | 11 +- src/Explorer/UserExplorerItem.php | 13 +- src/Forms/AddUserType.php | 3 + src/Forms/CustomFormFieldType.php | 67 +- src/Forms/CustomFormType.php | 121 -- src/Forms/DataTransformer/TagTransformer.php | 5 + src/Forms/DocumentEditType.php | 10 +- src/Forms/DocumentTranslationType.php | 8 +- src/Forms/DynamicType.php | 5 + src/Forms/FolderCollectionType.php | 8 +- src/Forms/FolderTranslationType.php | 3 + src/Forms/FolderType.php | 3 + src/Forms/ImageCropAlignmentType.php | 44 - src/Forms/LoginType.php | 2 +- src/Forms/Node/AddNodeType.php | 9 +- .../NodeSource/NodeSourceCustomFormType.php | 15 +- .../NodeSource/NodeSourceDocumentType.php | 17 +- src/Forms/NodeSource/NodeSourceJoinType.php | 15 +- src/Forms/NodeSource/NodeSourceNodeType.php | 9 +- .../NodeSource/NodeSourceProviderType.php | 13 +- src/Forms/NodeSource/NodeSourceType.php | 13 +- src/Forms/NodeTagsType.php | 63 + src/Forms/NodeType.php | 16 +- src/Forms/NodeTypeFieldType.php | 5 +- src/Forms/NodeTypeType.php | 10 - src/Forms/RedirectionType.php | 3 + src/Forms/TranstypeType.php | 3 + src/Forms/UserType.php | 3 + src/Models/CustomFormModel.php | 24 +- src/Models/DocumentModel.php | 46 +- src/Models/ModelInterface.php | 3 + src/Models/NodeModel.php | 53 +- src/Models/NodeSourceModel.php | 30 +- src/Models/NodeTypeModel.php | 11 +- src/Models/TagModel.php | 14 +- src/Resources/app/Lazyload.js | 121 +- src/Resources/app/Rozier.js | 500 +++--- src/Resources/app/api/DocumentApi.js | 2 + .../assets/img/apple-touch-icon-114x114.png | Bin 0 -> 2125 bytes .../assets/img/apple-touch-icon-120x120.png | Bin 0 -> 2189 bytes .../assets/img/apple-touch-icon-144x144.png | Bin 0 -> 2607 bytes .../assets/img/apple-touch-icon-152x152.png | Bin 0 -> 2736 bytes .../assets/img/apple-touch-icon-180x180.png | Bin 0 -> 3208 bytes .../app/assets/img/apple-touch-icon-57x57.png | Bin 0 -> 1220 bytes .../app/assets/img/apple-touch-icon-60x60.png | Bin 0 -> 1264 bytes .../app/assets/img/apple-touch-icon-72x72.png | Bin 0 -> 1490 bytes .../app/assets/img/apple-touch-icon-76x76.png | Bin 0 -> 1543 bytes .../img/apple-touch-icon-precomposed.png | Bin 0 -> 3153 bytes .../app/assets/img/browserconfig.xml | 12 + .../app/assets/img/favicon-160x160.png | Bin 0 -> 2847 bytes .../app/assets/img/favicon-192x192.png | Bin 0 -> 3415 bytes .../app/assets/img/favicon-96x96.png | Bin 0 -> 1812 bytes src/Resources/app/assets/img/matrix@2x.png | Bin 0 -> 1341 bytes .../app/assets/img/mstile-144x144.png | Bin 0 -> 2499 bytes .../app/assets/img/mstile-150x150.png | Bin 0 -> 2551 bytes .../app/assets/img/mstile-310x150.png | Bin 0 -> 2769 bytes .../app/assets/img/mstile-310x310.png | Bin 0 -> 5365 bytes src/Resources/app/assets/img/mstile-70x70.png | Bin 0 -> 1777 bytes src/Resources/app/components/DrawerItem.vue | 69 +- src/Resources/app/components/Dropzone.vue | 2 +- src/Resources/app/components/RzTextarea.vue | 2 +- src/Resources/app/components/WarningModal.vue | 2 +- .../AttributeValuePosition.js | 29 +- .../custom-form-fields/CustomFormFieldEdit.js | 159 ++ .../CustomFormFieldsPosition.js | 3 +- src/Resources/app/components/import/Import.js | 119 ++ .../node-type-fields/NodeTypeFieldEdit.js | 167 ++ .../NodeTypeFieldsPosition.js | 32 +- .../app/components/node/NodeEditSource.js | 111 +- .../app/components/panels/EntriesPanel.js | 26 +- .../app/components/tabs/MainTreeTabs.js | 12 +- src/Resources/app/components/tag/TagEdit.js | 73 +- .../trees/NodeTreeContextActions.js | 148 +- .../app/containers/TagsEditorContainer.vue | 11 + .../fonts/roadiz-sans/RoadizSans-Light.eot | Bin 0 -> 23702 bytes .../fonts/roadiz-sans/RoadizSans-Light.woff | Bin 0 -> 26568 bytes .../fonts/roadiz-sans/RoadizSans-Light.woff2 | Bin 0 -> 21212 bytes .../fonts/roadiz-sans/RoadizSans-Medium.eot | Bin 0 -> 23720 bytes .../fonts/roadiz-sans/RoadizSans-Medium.woff | Bin 0 -> 26472 bytes .../fonts/roadiz-sans/RoadizSans-Medium.woff2 | Bin 0 -> 21164 bytes .../app/fonts/roadiz-sans/RoadizSans-Mono.eot | Bin 0 -> 22520 bytes .../fonts/roadiz-sans/RoadizSans-Mono.woff | Bin 0 -> 25156 bytes .../fonts/roadiz-sans/RoadizSans-Mono.woff2 | Bin 0 -> 20188 bytes .../fonts/roadiz-sans/RoadizSans-Regular.eot | Bin 0 -> 24157 bytes .../fonts/roadiz-sans/RoadizSans-Regular.woff | Bin 0 -> 26908 bytes .../roadiz-sans/RoadizSans-Regular.woff2 | Bin 0 -> 21544 bytes .../fonts/roadiz-sans/roadiz-sans-LICENCE.md | 27 + .../app/less/actions_menu/actions_menu.less | 62 +- src/Resources/app/less/alerts/alerts.less | 37 +- .../app/less/attributes/attributes.less | 6 +- .../app/less/autocomplete/autocomplete.less | 6 +- src/Resources/app/less/badges/badges.less | 34 +- src/Resources/app/less/base/boilerplate.less | 4 +- src/Resources/app/less/base/elements.less | 27 +- src/Resources/app/less/base/normalize.less | 2 +- .../app/less/breadcrumb/breadcrumb.less | 14 +- src/Resources/app/less/bulk/bulk.less | 62 +- src/Resources/app/less/buttons/buttons.less | 181 +- src/Resources/app/less/common.less | 52 +- .../custom-forms-fields.less | 38 +- .../app/less/custom-forms-front.less | 4 +- .../app/less/dashboard/latestSources.less | 8 +- .../app/less/documents/documents.less | 29 +- src/Resources/app/less/dropdown/dropdown.less | 14 +- src/Resources/app/less/dropzone/dropzone.less | 19 +- .../app/less/forms/collection_form.less | 2 +- .../app/less/forms/crop-alignment.less | 38 - .../app/less/forms/custom_switch.less | 15 +- src/Resources/app/less/forms/forms.less | 54 +- .../app/less/forms/markdown_editor.less | 29 +- src/Resources/app/less/grid/grid_layout.less | 19 +- src/Resources/app/less/history/history.less | 10 +- src/Resources/app/less/login/login.less | 65 +- src/Resources/app/less/navbars/common.less | 6 +- src/Resources/app/less/navbars/filterbar.less | 10 +- .../app/less/navbars/navigation.less | 10 +- .../app/less/navbars/translationbar.less | 6 +- .../app/less/node-type-fields/fields.less | 10 +- .../app/less/node-types/node-types.less | 6 +- src/Resources/app/less/nodes/edit.less | 16 +- src/Resources/app/less/nodes/global.less | 19 +- .../panels/entries_panel/admin_entries.less | 126 +- .../main_content_panel/main_content.less | 24 +- .../less/panels/trees_panel/trees_panel.less | 97 +- .../less/panels/user_panel/user_panel.less | 159 +- .../app/less/responsive/less-1280.less | 2 +- .../app/less/responsive/less-768.less | 70 +- src/Resources/app/less/style.less | 1 - src/Resources/app/less/tables/tables.less | 46 +- src/Resources/app/less/themes/import.less | 2 +- src/Resources/app/less/users/users.less | 5 +- src/Resources/app/less/vars.less | 108 +- src/Resources/app/less/vendor/jquery-ui.less | 184 +- .../less/widgets/children_nodes_widget.less | 50 +- .../app/less/widgets/customform_widget.less | 14 +- .../app/less/widgets/debugpanel.less | 3 +- .../app/less/widgets/documents_widget.less | 30 +- .../less/widgets/drawer_document_item.less | 26 +- .../app/less/widgets/drawer_item.less | 31 +- .../app/less/widgets/drawer_widget.less | 41 +- src/Resources/app/less/widgets/explorer.less | 64 +- .../app/less/widgets/filter_explorer.less | 4 +- .../app/less/widgets/folder_tree.less | 33 +- .../app/less/widgets/geotag_widget.less | 64 +- src/Resources/app/less/widgets/nestable.less | 105 +- src/Resources/app/less/widgets/node_tree.less | 4 +- src/Resources/app/less/widgets/spinner.less | 2 +- src/Resources/app/less/widgets/tag_tree.less | 13 +- .../app/services/LoginCheckService.js | 48 +- .../app/widgets/ChildrenNodesField.js | 238 ++- src/Resources/app/widgets/CssEditor.js | 9 +- .../app/widgets/GenericBulkActions.js | 101 -- src/Resources/app/widgets/JsonEditor.js | 4 +- .../app/widgets/LeafletGeotagField.js | 201 +-- src/Resources/app/widgets/MarkdownEditor.js | 51 +- .../app/widgets/MultiLeafletGeotagField.js | 217 +-- src/Resources/app/widgets/NodeStatuses.js | 95 +- src/Resources/app/widgets/NodeTree.js | 2 + src/Resources/app/widgets/SaveButtons.js | 1 + .../app/widgets/SettingsSaveButtons.js | 67 +- src/Resources/app/widgets/StackNodeTree.js | 257 +-- src/Resources/app/widgets/YamlEditor.js | 9 +- src/Resources/routes.yml | 5 + src/Resources/translations/helps.ar.xlf | 1 + src/Resources/translations/helps.de.xlf | 1 + src/Resources/translations/helps.en.xlf | 4 + src/Resources/translations/helps.es.xlf | 1 + src/Resources/translations/helps.fr.xlf | 4 + src/Resources/translations/helps.id.xlf | 4 + src/Resources/translations/helps.it.xlf | 1 + src/Resources/translations/helps.ru.xlf | 1 + src/Resources/translations/helps.sr.xlf | 1 + src/Resources/translations/helps.tr.xlf | 1 + src/Resources/translations/helps.uk.xlf | 1 + src/Resources/translations/helps.xlf | 4 + src/Resources/translations/helps.zh.xlf | 4 + src/Resources/translations/messages.ar.xlf | 4 + src/Resources/translations/messages.de.xlf | 4 + src/Resources/translations/messages.en.xlf | 146 +- src/Resources/translations/messages.es.xlf | 9 + src/Resources/translations/messages.fr.xlf | 146 +- src/Resources/translations/messages.id.xlf | 4 + src/Resources/translations/messages.it.xlf | 4 + src/Resources/translations/messages.ru.xlf | 4 + src/Resources/translations/messages.sr.xlf | 4 + src/Resources/translations/messages.tr.xlf | 4 + src/Resources/translations/messages.uk.xlf | 4 + src/Resources/translations/messages.xlf | 41 +- src/Resources/translations/messages.zh.xlf | 17 +- src/Resources/views/admin/base.html.twig | 3 +- .../views/admin/blocks/adminImage.html.twig | 2 +- .../views/admin/blocks/loginImage.html.twig | 2 +- .../views/admin/bulk_actions.html.twig | 47 - .../views/admin/webhooks/add.html.twig | 2 +- .../views/admin/webhooks/bulk_base.html.twig | 39 - .../admin/webhooks/bulk_delete.html.twig | 20 - .../views/admin/webhooks/edit.html.twig | 2 +- .../views/admin/webhooks/list.html.twig | 104 +- .../admin/webhooks/webhook_row.html.twig | 35 - .../webhooks/webhook_row_header.html.twig | 28 - src/Resources/views/attributes/add.html.twig | 2 +- .../views/attributes/attribute_row.html.twig | 39 - .../attributes/attribute_row_header.html.twig | 29 - .../views/attributes/bulk_base.html.twig | 39 - .../views/attributes/bulk_delete.html.twig | 20 - src/Resources/views/attributes/edit.html.twig | 7 +- .../views/attributes/groups/add.html.twig | 2 +- .../views/attributes/groups/edit.html.twig | 2 +- .../views/attributes/groups/list.html.twig | 12 +- src/Resources/views/attributes/list.html.twig | 33 +- src/Resources/views/base.html.twig | 21 +- .../views/cache/deleteDoctrine.html.twig | 70 +- src/Resources/views/css/mainColor.css.twig | 68 +- .../views/custom-form-answers/list.html.twig | 2 +- .../views/custom-form-fields/add.html.twig | 2 +- .../views/custom-form-fields/edit.html.twig | 2 +- .../views/custom-form-fields/list.html.twig | 12 +- .../views/custom-forms/add.html.twig | 2 +- .../views/custom-forms/bulk_base.html.twig | 39 - .../views/custom-forms/bulk_delete.html.twig | 20 - .../custom-forms/customform_row.html.twig | 53 - .../customform_row_header.html.twig | 30 - .../views/custom-forms/edit.html.twig | 4 +- .../views/custom-forms/list.html.twig | 76 +- src/Resources/views/dashboard/index.html.twig | 40 +- .../document-translations/edit.html.twig | 2 +- .../views/documents/actionsMenu.html.twig | 4 +- src/Resources/views/documents/edit.html.twig | 14 +- src/Resources/views/documents/embed.html.twig | 2 +- .../views/documents/upload.html.twig | 5 +- src/Resources/views/documents/usage.html.twig | 12 +- src/Resources/views/folders/add.html.twig | 2 +- src/Resources/views/folders/edit.html.twig | 8 +- src/Resources/views/folders/list.html.twig | 2 +- src/Resources/views/forms.html.twig | 86 +- src/Resources/views/groups/add.html.twig | 2 +- src/Resources/views/groups/edit.html.twig | 2 +- src/Resources/views/groups/list.html.twig | 2 +- src/Resources/views/groups/roles.html.twig | 2 +- .../views/modules/history-item.html.twig | 70 +- .../views/node-type-fields/add.html.twig | 2 +- .../views/node-type-fields/edit.html.twig | 2 +- .../views/node-type-fields/list.html.twig | 2 +- src/Resources/views/node-types/add.html.twig | 2 +- src/Resources/views/node-types/edit.html.twig | 6 +- src/Resources/views/node-types/list.html.twig | 6 +- src/Resources/views/nodes/add.html.twig | 2 +- .../views/nodes/attributes/edit.html.twig | 67 +- .../views/nodes/breadcrumb.html.twig | 42 +- .../views/nodes/bulkDelete.html.twig | 2 +- .../views/nodes/bulkStatus.html.twig | 2 +- src/Resources/views/nodes/edit.html.twig | 8 +- .../views/nodes/editSource.html.twig | 2 +- src/Resources/views/nodes/editTags.html.twig | 2 +- .../views/nodes/widgets/node-row.html.twig | 4 +- .../views/panels/tree_panel.html.twig | 24 +- .../views/panels/user_panel.html.twig | 52 +- .../views/partials/css-inject.html.twig | 4 +- .../views/partials/js-inject.html.twig | 4 +- .../views/partials/simple-js-inject.html.twig | 2 +- .../views/redirections/add.html.twig | 2 +- .../views/redirections/bulk_base.html.twig | 39 - .../views/redirections/bulk_delete.html.twig | 20 - .../views/redirections/edit.html.twig | 2 +- .../views/redirections/list.html.twig | 53 +- .../redirections/redirection_row.html.twig | 36 - .../redirection_row_header.html.twig | 36 - src/Resources/views/roles/add.html.twig | 2 +- src/Resources/views/roles/edit.html.twig | 2 +- src/Resources/views/roles/list.html.twig | 2 +- .../views/settingGroups/add.html.twig | 2 +- .../views/settingGroups/edit.html.twig | 2 +- .../views/settingGroups/list.html.twig | 2 +- src/Resources/views/settings/add.html.twig | 2 +- src/Resources/views/settings/edit.html.twig | 2 +- src/Resources/views/settings/list.html.twig | 6 +- src/Resources/views/simple.html.twig | 50 + src/Resources/views/tags/add.html.twig | 2 +- src/Resources/views/tags/bulkDelete.html.twig | 2 +- src/Resources/views/tags/edit.html.twig | 2 +- src/Resources/views/tags/list.html.twig | 4 +- src/Resources/views/tags/settings.html.twig | 8 +- .../views/translations/add.html.twig | 2 +- .../views/translations/edit.html.twig | 2 +- .../views/translations/list.html.twig | 2 +- .../views/users/actionsMenu.html.twig | 2 +- src/Resources/views/users/add.html.twig | 7 +- src/Resources/views/users/bulk_base.html.twig | 39 - .../views/users/bulk_delete.html.twig | 20 - .../views/users/bulk_disable.html.twig | 20 - .../views/users/bulk_enable.html.twig | 20 - src/Resources/views/users/delete.html.twig | 4 +- src/Resources/views/users/edit.html.twig | 23 +- .../views/users/editDetails.html.twig | 10 +- src/Resources/views/users/groups.html.twig | 2 +- src/Resources/views/users/navBar.html.twig | 4 +- src/Resources/views/users/roles.html.twig | 2 +- src/Resources/views/users/security.html.twig | 2 +- src/Resources/views/users/user_row.html.twig | 83 - .../views/users/user_row_header.html.twig | 35 - .../customFormSmallThumbnail.html.twig | 4 +- src/Resources/views/widgets/drawer.html.twig | 37 +- .../widgets/folderTree/folderTree.html.twig | 14 +- .../widgets/folderTree/singleFolder.html.twig | 1 - .../widgets/nodeSmallThumbnail.html.twig | 2 +- .../widgets/nodeTree/nodeStackTree.html.twig | 2 +- .../views/widgets/nodeTree/nodeTree.html.twig | 14 +- .../widgets/nodeTree/singleNode.html.twig | 13 +- .../views/widgets/tagTree/singleTag.html.twig | 1 - .../views/widgets/tagTree/tagTree.html.twig | 14 +- src/Resources/webpack/build/base.js | 8 +- src/RozierApp.php | 102 +- src/RozierServiceRegistry.php | 48 +- .../DocumentThumbnailSerializeSubscriber.php | 10 +- src/Traits/NodesTrait.php | 47 +- src/Traits/VersionedControllerTrait.php | 16 +- src/Utils/SessionListFilters.php | 66 + src/Widgets/AbstractWidget.php | 16 +- src/Widgets/FolderTreeWidget.php | 11 +- src/Widgets/NodeTreeWidget.php | 49 +- src/Widgets/TagTreeWidget.php | 25 +- src/Widgets/TreeWidgetFactory.php | 19 +- src/package.json | 22 +- src/static/css/app.439d0fdd7e9d3f43dd39.css | 30 + .../css/app.439d0fdd7e9d3f43dd39.css.map | 1 + src/static/css/app.7bd05be056971b4aa556.css | 30 - .../css/app.7bd05be056971b4aa556.css.map | 1 - .../css/vendor.9b59501e29a05f532c3c.css.map | 1 - ...3c.css => vendor.aaeedaa02e00b12e41cf.css} | 4 +- .../css/vendor.aaeedaa02e00b12e41cf.css.map | 1 + ...ght.3cb2dde3ccc587cd4b9015acd888d682.woff2 | Bin 0 -> 21212 bytes ...ight.4b78aae656bca1227c451a681682a81d.woff | Bin 0 -> 26568 bytes ...ium.1f2274d646ff39afd4b05eb40736e6d2.woff2 | Bin 0 -> 21164 bytes ...dium.d099b2b90aec499cb3500c79ccd40962.woff | Bin 0 -> 26472 bytes ...ono.04f18efe0897251ef8b8fd05387da836.woff2 | Bin 0 -> 20188 bytes ...Mono.c4fe4f0d09dd98e8e016ebf66f6af85e.woff | Bin 0 -> 25156 bytes ...ular.4ad219c9931fb2a89e21a9a88f7c1ff7.woff | Bin 0 -> 26908 bytes ...lar.78075c1857b3a12055a7d6ef730e66d9.woff2 | Bin 0 -> 21544 bytes src/static/js/app.634b80d0ad5c98d43dbf.js | 22 + src/static/js/app.8b2aede757b0bad38dc9.js | 22 - ...8dc9.js => simple.634b80d0ad5c98d43dbf.js} | 0 ...8dc9.js => vendor.634b80d0ad5c98d43dbf.js} | 14 +- src/static/vendor/vue.min.js | 6 +- src/yarn.lock | 1494 ++++++++--------- 424 files changed, 7268 insertions(+), 7023 deletions(-) create mode 100644 .travis.yml create mode 100644 .travis/backoffice_assets.sh create mode 100644 .travis/composer_install.sh create mode 100644 .travis/php_lint.sh create mode 100644 src/Controllers/Nodes/NodesTagsController.php create mode 100644 src/Controllers/Nodes/UrlAliasesController.php create mode 100644 src/Controllers/PingController.php delete mode 100644 src/Event/UserActionsMenuEvent.php delete mode 100644 src/Forms/CustomFormType.php delete mode 100644 src/Forms/ImageCropAlignmentType.php create mode 100644 src/Forms/NodeTagsType.php create mode 100644 src/Resources/app/assets/img/apple-touch-icon-114x114.png create mode 100644 src/Resources/app/assets/img/apple-touch-icon-120x120.png create mode 100644 src/Resources/app/assets/img/apple-touch-icon-144x144.png create mode 100644 src/Resources/app/assets/img/apple-touch-icon-152x152.png create mode 100644 src/Resources/app/assets/img/apple-touch-icon-180x180.png create mode 100644 src/Resources/app/assets/img/apple-touch-icon-57x57.png create mode 100644 src/Resources/app/assets/img/apple-touch-icon-60x60.png create mode 100644 src/Resources/app/assets/img/apple-touch-icon-72x72.png create mode 100644 src/Resources/app/assets/img/apple-touch-icon-76x76.png create mode 100644 src/Resources/app/assets/img/apple-touch-icon-precomposed.png create mode 100644 src/Resources/app/assets/img/browserconfig.xml create mode 100644 src/Resources/app/assets/img/favicon-160x160.png create mode 100644 src/Resources/app/assets/img/favicon-192x192.png create mode 100644 src/Resources/app/assets/img/favicon-96x96.png create mode 100644 src/Resources/app/assets/img/matrix@2x.png create mode 100644 src/Resources/app/assets/img/mstile-144x144.png create mode 100644 src/Resources/app/assets/img/mstile-150x150.png create mode 100644 src/Resources/app/assets/img/mstile-310x150.png create mode 100644 src/Resources/app/assets/img/mstile-310x310.png create mode 100644 src/Resources/app/assets/img/mstile-70x70.png create mode 100644 src/Resources/app/components/custom-form-fields/CustomFormFieldEdit.js create mode 100644 src/Resources/app/components/import/Import.js create mode 100644 src/Resources/app/components/node-type-fields/NodeTypeFieldEdit.js create mode 100644 src/Resources/app/fonts/roadiz-sans/RoadizSans-Light.eot create mode 100644 src/Resources/app/fonts/roadiz-sans/RoadizSans-Light.woff create mode 100644 src/Resources/app/fonts/roadiz-sans/RoadizSans-Light.woff2 create mode 100644 src/Resources/app/fonts/roadiz-sans/RoadizSans-Medium.eot create mode 100644 src/Resources/app/fonts/roadiz-sans/RoadizSans-Medium.woff create mode 100644 src/Resources/app/fonts/roadiz-sans/RoadizSans-Medium.woff2 create mode 100644 src/Resources/app/fonts/roadiz-sans/RoadizSans-Mono.eot create mode 100644 src/Resources/app/fonts/roadiz-sans/RoadizSans-Mono.woff create mode 100644 src/Resources/app/fonts/roadiz-sans/RoadizSans-Mono.woff2 create mode 100644 src/Resources/app/fonts/roadiz-sans/RoadizSans-Regular.eot create mode 100644 src/Resources/app/fonts/roadiz-sans/RoadizSans-Regular.woff create mode 100644 src/Resources/app/fonts/roadiz-sans/RoadizSans-Regular.woff2 create mode 100644 src/Resources/app/fonts/roadiz-sans/roadiz-sans-LICENCE.md delete mode 100644 src/Resources/app/less/forms/crop-alignment.less delete mode 100644 src/Resources/app/widgets/GenericBulkActions.js delete mode 100644 src/Resources/views/admin/bulk_actions.html.twig delete mode 100644 src/Resources/views/admin/webhooks/bulk_base.html.twig delete mode 100644 src/Resources/views/admin/webhooks/bulk_delete.html.twig delete mode 100644 src/Resources/views/admin/webhooks/webhook_row.html.twig delete mode 100644 src/Resources/views/admin/webhooks/webhook_row_header.html.twig delete mode 100644 src/Resources/views/attributes/attribute_row.html.twig delete mode 100644 src/Resources/views/attributes/attribute_row_header.html.twig delete mode 100644 src/Resources/views/attributes/bulk_base.html.twig delete mode 100644 src/Resources/views/attributes/bulk_delete.html.twig delete mode 100644 src/Resources/views/custom-forms/bulk_base.html.twig delete mode 100644 src/Resources/views/custom-forms/bulk_delete.html.twig delete mode 100644 src/Resources/views/custom-forms/customform_row.html.twig delete mode 100644 src/Resources/views/custom-forms/customform_row_header.html.twig delete mode 100644 src/Resources/views/redirections/bulk_base.html.twig delete mode 100644 src/Resources/views/redirections/bulk_delete.html.twig delete mode 100644 src/Resources/views/redirections/redirection_row.html.twig delete mode 100644 src/Resources/views/redirections/redirection_row_header.html.twig create mode 100644 src/Resources/views/simple.html.twig delete mode 100644 src/Resources/views/users/bulk_base.html.twig delete mode 100644 src/Resources/views/users/bulk_delete.html.twig delete mode 100644 src/Resources/views/users/bulk_disable.html.twig delete mode 100644 src/Resources/views/users/bulk_enable.html.twig delete mode 100644 src/Resources/views/users/user_row.html.twig delete mode 100644 src/Resources/views/users/user_row_header.html.twig create mode 100644 src/Utils/SessionListFilters.php create mode 100644 src/static/css/app.439d0fdd7e9d3f43dd39.css create mode 100644 src/static/css/app.439d0fdd7e9d3f43dd39.css.map delete mode 100644 src/static/css/app.7bd05be056971b4aa556.css delete mode 100644 src/static/css/app.7bd05be056971b4aa556.css.map delete mode 100644 src/static/css/vendor.9b59501e29a05f532c3c.css.map rename src/static/css/{vendor.9b59501e29a05f532c3c.css => vendor.aaeedaa02e00b12e41cf.css} (88%) create mode 100644 src/static/css/vendor.aaeedaa02e00b12e41cf.css.map create mode 100644 src/static/fonts/RoadizSans-Light.3cb2dde3ccc587cd4b9015acd888d682.woff2 create mode 100644 src/static/fonts/RoadizSans-Light.4b78aae656bca1227c451a681682a81d.woff create mode 100644 src/static/fonts/RoadizSans-Medium.1f2274d646ff39afd4b05eb40736e6d2.woff2 create mode 100644 src/static/fonts/RoadizSans-Medium.d099b2b90aec499cb3500c79ccd40962.woff create mode 100644 src/static/fonts/RoadizSans-Mono.04f18efe0897251ef8b8fd05387da836.woff2 create mode 100644 src/static/fonts/RoadizSans-Mono.c4fe4f0d09dd98e8e016ebf66f6af85e.woff create mode 100644 src/static/fonts/RoadizSans-Regular.4ad219c9931fb2a89e21a9a88f7c1ff7.woff create mode 100644 src/static/fonts/RoadizSans-Regular.78075c1857b3a12055a7d6ef730e66d9.woff2 create mode 100644 src/static/js/app.634b80d0ad5c98d43dbf.js delete mode 100644 src/static/js/app.8b2aede757b0bad38dc9.js rename src/static/js/{simple.8b2aede757b0bad38dc9.js => simple.634b80d0ad5c98d43dbf.js} (100%) rename src/static/js/{vendor.8b2aede757b0bad38dc9.js => vendor.634b80d0ad5c98d43dbf.js} (92%) diff --git a/.github/workflows/run-test.yml b/.github/workflows/run-test.yml index 189e3a55..3b17d56e 100644 --- a/.github/workflows/run-test.yml +++ b/.github/workflows/run-test.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php-version: ['8.1', '8.2', '8.3'] + php-version: ['8.0', '8.1'] steps: - uses: shivammathur/setup-php@v2 with: diff --git a/.gitignore b/.gitignore index 43469c37..ea8d6aa9 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ /dev.php /install.php /clear_cache.php +/pimple.json /assets project_env.sh diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..5346b623 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,75 @@ +node_js: + - "14" +php: + - 8.0 + - 8.1 + - nightly +dist: bionic +stages: + - "PHP lint tests" + - "Backoffice assets tests" +branches: + except: + - l10n_develop + +jobs: + allow_failures: + - php: nightly + + include: + - stage: "Backoffice assets tests" + language: node_js + node_js: "14" + script: sh .travis/backoffice_assets.sh + + - stage: "PHP lint tests" + language: php + sudo: required + services: + - mysql + env: + - DB=mysql + - MYSQL_VERSION=5.7 + - MYSQL_PASSWORD= + php: 7.4 + install: sh .travis/composer_install.sh + script: sh .travis/php_lint.sh + - stage: "PHP lint tests" + language: php + sudo: required + services: + - mysql + env: + - DB=mysql + - MYSQL_VERSION=5.7 + - MYSQL_PASSWORD= + php: 8.0 + install: sh .travis/composer_install.sh + script: sh .travis/php_lint.sh + - stage: "PHP lint tests" + language: php + sudo: required + services: + - mysql + env: + - DB=mysql + - MYSQL_VERSION=5.7 + - MYSQL_PASSWORD= + php: 8.1 + install: sh .travis/composer_install.sh + script: sh .travis/php_lint.sh + - stage: "PHP lint tests" + language: php + sudo: required + services: + - mysql + env: + - DB=mysql + - MYSQL_VERSION=5.7 + - MYSQL_PASSWORD= + php: nightly + install: sh .travis/composer_install.sh + script: sh .travis/php_lint.sh + + + diff --git a/.travis/backoffice_assets.sh b/.travis/backoffice_assets.sh new file mode 100644 index 00000000..92ba88c8 --- /dev/null +++ b/.travis/backoffice_assets.sh @@ -0,0 +1,5 @@ +#!/bin/sh -x +cd src || exit 1; +yarn install --pure-lockfile +yarn run install +yarn run build diff --git a/.travis/composer_install.sh b/.travis/composer_install.sh new file mode 100644 index 00000000..628aaebb --- /dev/null +++ b/.travis/composer_install.sh @@ -0,0 +1,4 @@ +#!/bin/sh -x +phpenv config-rm xdebug.ini; +curl -s http://getcomposer.org/installer | php; +php composer.phar install --dev --no-interaction; diff --git a/.travis/php_lint.sh b/.travis/php_lint.sh new file mode 100644 index 00000000..624af292 --- /dev/null +++ b/.travis/php_lint.sh @@ -0,0 +1,5 @@ +#!/bin/sh -x +vendor/bin/phpcs --report=full --report-file=./report.txt -p ./ || exit 1; +vendor/bin/phpstan analyse -c phpstan.neon || exit 1; +#vendor/bin/console lint:twig || exit 1; +#vendor/bin/console lint:twig src/Resources/views || exit 1; diff --git a/LICENSE.md b/LICENSE.md index 01cd7778..747e48b2 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright © 2024 Ambroise Maupate, Julien Blanchet +Copyright © 2023 Ambroise Maupate, Julien Blanchet Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/composer.json b/composer.json index e5bbf990..917762b9 100644 --- a/composer.json +++ b/composer.json @@ -28,48 +28,47 @@ "role": "Frontend developer" } ], - "minimum-stability": "dev", - "prefer-stable": true, "require": { - "php": ">=8.1", + "php": ">=8.0", "ext-zip": "*", - "doctrine/orm": "~2.19.0", + "doctrine/orm": "<2.17", "guzzlehttp/guzzle": "^7.2.0", "jms/serializer": "^3.9.0", "league/flysystem": "^3.0", + "pimple/pimple": "^3.3.1", "ramsey/uuid": "^4.7", - "roadiz/compat-bundle": "2.3.*", - "roadiz/core-bundle": "2.3.*", - "roadiz/doc-generator": "2.3.*", - "roadiz/documents": "2.3.*", - "roadiz/dts-generator": "2.3.*", - "roadiz/markdown": "2.3.*", - "roadiz/models": "2.3.*", + "roadiz/compat-bundle": "2.1.*", + "roadiz/core-bundle": "2.1.*", + "roadiz/doc-generator": "2.1.*", + "roadiz/documents": "2.1.*", + "roadiz/dts-generator": "2.1.*", + "roadiz/markdown": "2.1.*", + "roadiz/models": "2.1.*", "roadiz/nodetype-contracts": "~1.1.2", - "roadiz/openid": "2.3.*", - "roadiz/rozier-bundle": "2.3.*", - "symfony/asset": "6.4.*", - "symfony/filesystem": "6.4.*", - "symfony/form": "6.4.*", - "symfony/http-foundation": "6.4.*", - "symfony/http-kernel": "6.4.*", - "symfony/routing": "6.4.*", - "symfony/security-core": "6.4.*", - "symfony/security-csrf": "6.4.*", - "symfony/security-http": "6.4.*", - "symfony/translation": "6.4.*", - "symfony/validator": "6.4.*", - "symfony/workflow": "6.4.*", - "symfony/yaml": "6.4.*", + "roadiz/openid": "2.1.*", + "roadiz/rozier-bundle": "2.1.*", + "symfony/asset": "5.4.*", + "symfony/filesystem": "5.4.*", + "symfony/form": "5.4.*", + "symfony/http-foundation": "5.4.*", + "symfony/http-kernel": "5.4.*", + "symfony/routing": "5.4.*", + "symfony/security-core": "5.4.*", + "symfony/security-csrf": "5.4.*", + "symfony/security-http": "5.4.*", + "symfony/translation": "5.4.*", + "symfony/validator": "5.4.*", + "symfony/workflow": "5.4.*", + "symfony/yaml": "5.4.*", "twig/twig": "^3.1" }, "require-dev": { "php-coveralls/php-coveralls": "^2.4", "phpstan/phpstan": "^1.5.3", "phpstan/phpstan-doctrine": "^1.3", - "roadiz/entity-generator": "2.3.*", - "roadiz/random": "2.3.*", - "roadiz/jwt": "2.3.*", + "roadiz/entity-generator": "2.1.*", + "roadiz/random": "2.1.*", + "roadiz/jwt": "2.1.*", "squizlabs/php_codesniffer": "^3.5" }, "autoload": { @@ -95,8 +94,8 @@ }, "extra": { "branch-alias": { - "dev-main": "2.3.x-dev", - "dev-develop": "2.4.x-dev" + "dev-main": "2.1.x-dev", + "dev-develop": "2.2.x-dev" } } } diff --git a/phpstan.neon b/phpstan.neon index 6693cc61..353be6b9 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,5 +1,5 @@ parameters: - level: 7 + level: 6 paths: - src excludePaths: @@ -9,8 +9,6 @@ parameters: doctrine: repositoryClass: RZ\Roadiz\Core\Repositories\EntityRepository ignoreErrors: - - identifier: missingType.iterableValue - - identifier: missingType.generics - '#Call to an undefined method RZ\\Roadiz\\CoreBundle\\Repository#' - '#Call to an undefined method RZ\\Roadiz\\UserBundle\\Repository#' - '#Call to an undefined method Doctrine\\Persistence\\ObjectRepository#' @@ -30,11 +28,10 @@ parameters: - '#Doctrine\\ORM\\Mapping\\GeneratedValue constructor expects#' - '#type mapping mismatch: property can contain Doctrine\\Common\\Collections\\Collection]+> but database expects Doctrine\\Common\\Collections\\Collection&iterable<[^\>]+>#' - '#should return Doctrine\\Common\\Collections\\Collection]+Interface> but returns Doctrine\\Common\\Collections\\Collection]+>#' - - '#but returns Doctrine\\Common\\Collections\\ReadableCollection]+>#' - - '#does not accept Doctrine\\Common\\Collections\\ReadableCollection]+>#' reportUnmatchedIgnoredErrors: false - treatPhpDocTypesAsCertain: false + checkGenericClassInNonGenericObjectType: false + checkMissingIterableValueType: false includes: - vendor/phpstan/phpstan-doctrine/extension.neon diff --git a/src/.babelrc b/src/.babelrc index b007a11d..2595c191 100644 --- a/src/.babelrc +++ b/src/.babelrc @@ -1,4 +1,4 @@ { - "presets": ["env", "stage-0"], + "presets": ["es2015", "stage-0"], "plugins": ["transform-runtime", "lodash"] } diff --git a/src/.editorconfig b/src/.editorconfig index cd986d42..c2778daa 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -1,6 +1,7 @@ # TheatreTheme editor config for contributors # Root is false as your theme is inside Roadiz filetree # http://editorconfig.org/ +root = false [*] indent_style = space diff --git a/src/.eslintrc.js b/src/.eslintrc.js index 020a951c..536f9132 100644 --- a/src/.eslintrc.js +++ b/src/.eslintrc.js @@ -11,7 +11,7 @@ module.exports = { }, // https://github.com/standard/standard/blob/master/docs/RULES-en.md //extends: 'standard', - extends: ['prettier', 'plugin:prettier/recommended', 'plugin:vue/base'], + extends: ['prettier', 'plugin:prettier/recommended', 'plugin:vue/essential'], // required to lint *.vue files plugins: ['html', 'prettier'], // add your custom rules here diff --git a/src/AjaxControllers/AbstractAjaxController.php b/src/AjaxControllers/AbstractAjaxController.php index c4633eca..2cd1c2e0 100644 --- a/src/AjaxControllers/AbstractAjaxController.php +++ b/src/AjaxControllers/AbstractAjaxController.php @@ -42,7 +42,7 @@ protected function getTranslation(Request $request): ?TranslationInterface * @param string $method * @param bool $requestCsrfToken * - * @return bool Return true if request is valid, else throw exception + * @return boolean Return true if request is valid, else throw exception */ protected function validateRequest(Request $request, string $method = 'POST', bool $requestCsrfToken = true): bool { diff --git a/src/AjaxControllers/AjaxAbstractFieldsController.php b/src/AjaxControllers/AjaxAbstractFieldsController.php index 64f3941e..00ff7757 100644 --- a/src/AjaxControllers/AjaxAbstractFieldsController.php +++ b/src/AjaxControllers/AjaxAbstractFieldsController.php @@ -11,10 +11,19 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +/** + * @package Themes\Rozier\AjaxControllers + */ abstract class AjaxAbstractFieldsController extends AbstractAjaxController { - public function __construct(protected readonly HandlerFactoryInterface $handlerFactory) + private HandlerFactoryInterface $handlerFactory; + + /** + * @param HandlerFactoryInterface $handlerFactory + */ + public function __construct(HandlerFactoryInterface $handlerFactory) { + $this->handlerFactory = $handlerFactory; } protected function findEntity(int|string $entityId): ?AbstractField @@ -30,7 +39,7 @@ protected function findEntity(int|string $entityId): ?AbstractField * * @return null|Response */ - protected function handleFieldActions(Request $request, AbstractField $field = null): ?Response + protected function handleFieldActions(Request $request, AbstractField $field = null) { /* * Validate diff --git a/src/AjaxControllers/AjaxAttributeValuesController.php b/src/AjaxControllers/AjaxAttributeValuesController.php index a4bc2108..0b93e7f6 100644 --- a/src/AjaxControllers/AjaxAttributeValuesController.php +++ b/src/AjaxControllers/AjaxAttributeValuesController.php @@ -5,7 +5,7 @@ namespace Themes\Rozier\AjaxControllers; use RZ\Roadiz\CoreBundle\Entity\AttributeValue; -use RZ\Roadiz\CoreBundle\Security\Authorization\Voter\NodeVoter; +use RZ\Roadiz\CoreBundle\Entity\Node; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -28,47 +28,49 @@ final class AjaxAttributeValuesController extends AbstractAjaxController */ public function editAction(Request $request, int $attributeValueId): Response { - /* - * Validate - */ $this->validateRequest($request, 'POST', false); + $this->denyAccessUnlessGranted('ROLE_ACCESS_NODE_ATTRIBUTES'); /** @var AttributeValue|null $attributeValue */ $attributeValue = $this->em()->find(AttributeValue::class, (int) $attributeValueId); - if ($attributeValue === null) { - throw $this->createNotFoundException($this->getTranslator()->trans( - 'attribute_value.%attributeValueId%.not_exists', - [ - '%attributeValueId%' => $attributeValueId - ] - )); - } - - $this->denyAccessUnlessGranted(NodeVoter::EDIT_ATTRIBUTE, $attributeValue->getAttributable()); + if ($attributeValue !== null) { + $responseArray = []; + /* + * Get the right update method against "_action" parameter + */ + switch ($request->get('_action')) { + case 'updatePosition': + $responseArray = $this->updatePosition($request->request->all(), $attributeValue); + break; + } - $responseArray = []; - /* - * Get the right update method against "_action" parameter - */ - switch ($request->get('_action')) { - case 'updatePosition': - $responseArray = $this->updatePosition($request->request->all(), $attributeValue); - break; + return new JsonResponse( + $responseArray, + Response::HTTP_PARTIAL_CONTENT + ); } - return new JsonResponse( - $responseArray, - Response::HTTP_PARTIAL_CONTENT - ); + throw $this->createNotFoundException($this->getTranslator()->trans( + 'attribute_value.%attributeValueId%.not_exists', + [ + '%attributeValueId%' => $attributeValueId + ] + )); } - protected function updatePosition(array $parameters, AttributeValue $attributeValue): array + /** + * @param array $parameters + * @param AttributeValue $attributeValue + * + * @return array + */ + protected function updatePosition($parameters, AttributeValue $attributeValue): array { $attributable = $attributeValue->getAttributable(); $details = [ '%name%' => $attributeValue->getAttribute()->getLabelOrCode(), - '%nodeName%' => $attributable->getNodeName(), + '%nodeName%' => $attributable instanceof Node ? $attributable->getNodeName() : '', ]; if (!empty($parameters['afterAttributeValueId']) && is_numeric($parameters['afterAttributeValueId'])) { diff --git a/src/AjaxControllers/AjaxCustomFormFieldsController.php b/src/AjaxControllers/AjaxCustomFormFieldsController.php index 86dc7ebc..4153b9f4 100644 --- a/src/AjaxControllers/AjaxCustomFormFieldsController.php +++ b/src/AjaxControllers/AjaxCustomFormFieldsController.php @@ -8,6 +8,9 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +/** + * @package Themes\Rozier\AjaxControllers + */ class AjaxCustomFormFieldsController extends AjaxAbstractFieldsController { /** @@ -15,11 +18,11 @@ class AjaxCustomFormFieldsController extends AjaxAbstractFieldsController * such as coming from widgets. * * @param Request $request - * @param int $customFormFieldId + * @param int $customFormFieldId * * @return Response JSON response */ - public function editAction(Request $request, int $customFormFieldId): Response + public function editAction(Request $request, int $customFormFieldId) { $this->validateRequest($request); $this->denyAccessUnlessGranted('ROLE_ACCESS_CUSTOMFORMS_DELETE'); diff --git a/src/AjaxControllers/AjaxCustomFormsExplorerController.php b/src/AjaxControllers/AjaxCustomFormsExplorerController.php index 8c8707f2..901e6004 100644 --- a/src/AjaxControllers/AjaxCustomFormsExplorerController.php +++ b/src/AjaxControllers/AjaxCustomFormsExplorerController.php @@ -13,10 +13,16 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Themes\Rozier\Models\CustomFormModel; +/** + * @package Themes\Rozier\AjaxControllers + */ class AjaxCustomFormsExplorerController extends AbstractAjaxController { - public function __construct(private readonly UrlGeneratorInterface $urlGenerator) + private UrlGeneratorInterface $urlGenerator; + + public function __construct(UrlGeneratorInterface $urlGenerator) { + $this->urlGenerator = $urlGenerator; } /** @@ -24,9 +30,9 @@ public function __construct(private readonly UrlGeneratorInterface $urlGenerator * * @return Response JSON response */ - public function indexAction(Request $request): Response + public function indexAction(Request $request) { - $this->denyAccessUnlessGranted('ROLE_ACCESS_CUSTOMFORMS'); + $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES'); $arrayFilter = []; /* @@ -62,15 +68,15 @@ public function indexAction(Request $request): Response * Get a CustomForm list from an array of id. * * @param Request $request - * @return Response + * @return JsonResponse */ - public function listAction(Request $request): Response + public function listAction(Request $request) { if (!$request->query->has('ids')) { throw new InvalidParameterException('Ids should be provided within an array'); } - $this->denyAccessUnlessGranted('ROLE_ACCESS_CUSTOMFORMS'); + $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES'); $cleanCustomFormsIds = array_filter($request->query->filter('ids', [], \FILTER_DEFAULT, [ 'flags' => \FILTER_FORCE_ARRAY @@ -105,7 +111,7 @@ public function listAction(Request $request): Response * @param array|\Traversable $customForms * @return array */ - private function normalizeCustomForms(iterable $customForms): array + private function normalizeCustomForms($customForms) { $customFormsArray = []; diff --git a/src/AjaxControllers/AjaxDocumentsExplorerController.php b/src/AjaxControllers/AjaxDocumentsExplorerController.php index 5936a864..c816d16a 100644 --- a/src/AjaxControllers/AjaxDocumentsExplorerController.php +++ b/src/AjaxControllers/AjaxDocumentsExplorerController.php @@ -11,18 +11,31 @@ use RZ\Roadiz\Documents\UrlGenerators\DocumentUrlGeneratorInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Exception\InvalidParameterException; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Themes\Rozier\Models\DocumentModel; +/** + * @package Themes\Rozier\AjaxControllers + */ class AjaxDocumentsExplorerController extends AbstractAjaxController { + private RendererInterface $renderer; + private DocumentUrlGeneratorInterface $documentUrlGenerator; + private UrlGeneratorInterface $urlGenerator; + private EmbedFinderFactory $embedFinderFactory; + public function __construct( - private readonly RendererInterface $renderer, - private readonly DocumentUrlGeneratorInterface $documentUrlGenerator, - private readonly UrlGeneratorInterface $urlGenerator, - private readonly EmbedFinderFactory $embedFinderFactory + RendererInterface $renderer, + DocumentUrlGeneratorInterface $documentUrlGenerator, + UrlGeneratorInterface $urlGenerator, + EmbedFinderFactory $embedFinderFactory ) { + $this->renderer = $renderer; + $this->documentUrlGenerator = $documentUrlGenerator; + $this->urlGenerator = $urlGenerator; + $this->embedFinderFactory = $embedFinderFactory; } public static array $thumbnailArray = [ @@ -30,8 +43,12 @@ public function __construct( "quality" => 50, "inline" => false, ]; - - public function indexAction(Request $request): JsonResponse + /** + * @param Request $request + * + * @return Response JSON response + */ + public function indexAction(Request $request) { $this->denyAccessUnlessGranted('ROLE_ACCESS_DOCUMENTS'); @@ -94,7 +111,7 @@ public function indexAction(Request $request): JsonResponse * @param Request $request * @return JsonResponse */ - public function listAction(Request $request): JsonResponse + public function listAction(Request $request) { $this->denyAccessUnlessGranted('ROLE_ACCESS_DOCUMENTS'); diff --git a/src/AjaxControllers/AjaxEntitiesExplorerController.php b/src/AjaxControllers/AjaxEntitiesExplorerController.php index 39b2a9a5..4557121d 100644 --- a/src/AjaxControllers/AjaxEntitiesExplorerController.php +++ b/src/AjaxControllers/AjaxEntitiesExplorerController.php @@ -18,7 +18,7 @@ use Symfony\Component\Config\Definition\Processor; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\Routing\Exception\InvalidParameterException; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Yaml\Yaml; use Themes\Rozier\Explorer\ConfigurableExplorerItem; @@ -26,14 +26,26 @@ use Themes\Rozier\Explorer\SettingExplorerItem; use Themes\Rozier\Explorer\UserExplorerItem; +/** + * @package Themes\Rozier\AjaxControllers + */ class AjaxEntitiesExplorerController extends AbstractAjaxController { + private RendererInterface $renderer; + private DocumentUrlGeneratorInterface $documentUrlGenerator; + private UrlGeneratorInterface $urlGenerator; + private EmbedFinderFactory $embedFinderFactory; + public function __construct( - private readonly RendererInterface $renderer, - private readonly DocumentUrlGeneratorInterface $documentUrlGenerator, - private readonly UrlGeneratorInterface $urlGenerator, - private readonly EmbedFinderFactory $embedFinderFactory + RendererInterface $renderer, + DocumentUrlGeneratorInterface $documentUrlGenerator, + UrlGeneratorInterface $urlGenerator, + EmbedFinderFactory $embedFinderFactory ) { + $this->renderer = $renderer; + $this->documentUrlGenerator = $documentUrlGenerator; + $this->urlGenerator = $urlGenerator; + $this->embedFinderFactory = $embedFinderFactory; } /** @@ -46,7 +58,7 @@ protected function getFieldConfiguration(NodeTypeField $nodeTypeField): array $nodeTypeField->getType() !== AbstractField::MANY_TO_MANY_T && $nodeTypeField->getType() !== AbstractField::MANY_TO_ONE_T ) { - throw new BadRequestHttpException('nodeTypeField is not a valid entity join.'); + throw new InvalidParameterException('nodeTypeField is not a valid entity join.'); } $configs = [ @@ -63,16 +75,11 @@ public function indexAction(Request $request): JsonResponse $this->denyAccessUnlessGranted('ROLE_BACKEND_USER'); if (!$request->query->has('nodeTypeFieldId')) { - throw new BadRequestHttpException('nodeTypeFieldId parameter is missing.'); + throw new InvalidParameterException('nodeTypeFieldId parameter is missing.'); } - /** @var NodeTypeField|null $nodeTypeField */ + /** @var NodeTypeField $nodeTypeField */ $nodeTypeField = $this->em()->find(NodeTypeField::class, $request->query->get('nodeTypeFieldId')); - - if (null === $nodeTypeField) { - throw new BadRequestHttpException('nodeTypeField does not exist.'); - } - $configuration = $this->getFieldConfiguration($nodeTypeField); /** @var class-string $className */ $className = $configuration['classname']; @@ -114,28 +121,29 @@ public function indexAction(Request $request): JsonResponse ); } + /** + * Get a Node list from an array of id. + * + * @param Request $request + * @return JsonResponse + */ public function listAction(Request $request): JsonResponse { if (!$request->query->has('nodeTypeFieldId')) { - throw new BadRequestHttpException('nodeTypeFieldId parameter is missing.'); + throw new InvalidParameterException('nodeTypeFieldId parameter is missing.'); } if (!$request->query->has('ids')) { - throw new BadRequestHttpException('Ids should be provided within an array'); + throw new InvalidParameterException('Ids should be provided within an array'); } - $this->denyAccessUnlessGranted('ROLE_BACKEND_USER'); + $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES'); /** @var EntityManager $em */ $em = $this->em(); - /** @var NodeTypeField|null $nodeTypeField */ + /** @var NodeTypeField $nodeTypeField */ $nodeTypeField = $this->em()->find(NodeTypeField::class, $request->query->get('nodeTypeFieldId')); - - if (null === $nodeTypeField) { - throw new BadRequestHttpException('nodeTypeField does not exist.'); - } - $configuration = $this->getFieldConfiguration($nodeTypeField); /** @var class-string $className */ $className = $configuration['classname']; @@ -173,7 +181,7 @@ public function listAction(Request $request): JsonResponse * @param array $configuration * @return array */ - private function normalizeEntities(iterable $entities, array $configuration): array + private function normalizeEntities(iterable $entities, array &$configuration): array { $entitiesArray = []; diff --git a/src/AjaxControllers/AjaxExplorerProviderController.php b/src/AjaxControllers/AjaxExplorerProviderController.php index 3ea4fe98..b9ee2e04 100644 --- a/src/AjaxControllers/AjaxExplorerProviderController.php +++ b/src/AjaxControllers/AjaxExplorerProviderController.php @@ -4,9 +4,7 @@ namespace Themes\Rozier\AjaxControllers; -use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; -use Psr\Container\NotFoundExceptionInterface; use RZ\Roadiz\CoreBundle\Explorer\AbstractExplorerProvider; use RZ\Roadiz\CoreBundle\Explorer\ExplorerItemInterface; use RZ\Roadiz\CoreBundle\Explorer\ExplorerProviderInterface; @@ -15,17 +13,23 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Exception\InvalidParameterException; +/** + * @package Themes\Rozier\AjaxControllers + */ class AjaxExplorerProviderController extends AbstractAjaxController { - public function __construct(private readonly ContainerInterface $psrContainer) + private ContainerInterface $psrContainer; + + public function __construct(ContainerInterface $psrContainer) { + $this->psrContainer = $psrContainer; } /** - * @param class-string $providerClass + * @param class-string $providerClass * @return ExplorerProviderInterface - * @throws ContainerExceptionInterface - * @throws NotFoundExceptionInterface + * @throws \Psr\Container\ContainerExceptionInterface + * @throws \Psr\Container\NotFoundExceptionInterface */ protected function getProvider(string $providerClass): ExplorerProviderInterface { @@ -34,47 +38,27 @@ protected function getProvider(string $providerClass): ExplorerProviderInterface } return new $providerClass(); } - /** - * @throws ContainerExceptionInterface - * @throws NotFoundExceptionInterface + * @param Request $request + * @return Response JSON response */ - protected function getProviderFromRequest(Request $request): ExplorerProviderInterface + public function indexAction(Request $request) { - /** @var class-string|null $providerClass */ - $providerClass = $request->query->get('providerClass'); + $this->denyAccessUnlessGranted('ROLE_BACKEND_USER'); - if (!\is_string($providerClass)) { + if (!$request->query->has('providerClass')) { throw new InvalidParameterException('providerClass parameter is missing.'); } - if (!\class_exists($providerClass)) { - throw new InvalidParameterException('providerClass is not a valid class.'); - } - $reflection = new \ReflectionClass($providerClass); - if (!$reflection->implementsInterface(ExplorerProviderInterface::class)) { - throw new InvalidParameterException('providerClass is not a valid ExplorerProviderInterface class.'); + $providerClass = $request->query->get('providerClass'); + if (!class_exists($providerClass)) { + throw new InvalidParameterException('providerClass is not a valid class.'); } $provider = $this->getProvider($providerClass); if ($provider instanceof AbstractExplorerProvider) { $provider->setContainer($this->psrContainer); } - - return $provider; - } - - /** - * @param Request $request - * @return JsonResponse - * @throws ContainerExceptionInterface - * @throws NotFoundExceptionInterface - */ - public function indexAction(Request $request): JsonResponse - { - $this->denyAccessUnlessGranted('ROLE_BACKEND_USER'); - - $provider = $this->getProviderFromRequest($request); $options = [ 'page' => $request->query->get('page') ?: 1, 'itemPerPage' => $request->query->get('itemPerPage') ?: 30, @@ -115,14 +99,28 @@ public function indexAction(Request $request): JsonResponse * * @param Request $request * @return JsonResponse - * @throws ContainerExceptionInterface - * @throws NotFoundExceptionInterface */ - public function listAction(Request $request): JsonResponse + public function listAction(Request $request) { + if (!$request->query->has('providerClass')) { + throw new InvalidParameterException('providerClass parameter is missing.'); + } + + $providerClass = $request->query->get('providerClass'); + if (!class_exists($providerClass)) { + throw new InvalidParameterException('providerClass is not a valid class.'); + } + + if (!$request->query->has('ids')) { + throw new InvalidParameterException('Ids should be provided within an array'); + } + $this->denyAccessUnlessGranted('ROLE_BACKEND_USER'); - $provider = $this->getProviderFromRequest($request); + $provider = $this->getProvider($providerClass); + if ($provider instanceof AbstractExplorerProvider) { + $provider->setContainer($this->psrContainer); + } $entitiesArray = []; $cleanNodeIds = array_filter($request->query->filter('ids', [], \FILTER_DEFAULT, [ 'flags' => \FILTER_FORCE_ARRAY diff --git a/src/AjaxControllers/AjaxFolderTreeController.php b/src/AjaxControllers/AjaxFolderTreeController.php index 1d934a54..c0c2dbe0 100644 --- a/src/AjaxControllers/AjaxFolderTreeController.php +++ b/src/AjaxControllers/AjaxFolderTreeController.php @@ -10,10 +10,16 @@ use Themes\Rozier\Widgets\FolderTreeWidget; use Themes\Rozier\Widgets\TreeWidgetFactory; +/** + * @package Themes\Rozier\AjaxControllers + */ class AjaxFolderTreeController extends AbstractAjaxController { - public function __construct(private readonly TreeWidgetFactory $treeWidgetFactory) + private TreeWidgetFactory $treeWidgetFactory; + + public function __construct(TreeWidgetFactory $treeWidgetFactory) { + $this->treeWidgetFactory = $treeWidgetFactory; } public function getTreeAction(Request $request): JsonResponse diff --git a/src/AjaxControllers/AjaxFoldersController.php b/src/AjaxControllers/AjaxFoldersController.php index ffa230ad..9c44aa1d 100644 --- a/src/AjaxControllers/AjaxFoldersController.php +++ b/src/AjaxControllers/AjaxFoldersController.php @@ -11,17 +11,28 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +/** + * @package Themes\Rozier\AjaxControllers + */ class AjaxFoldersController extends AbstractAjaxController { - public function __construct(private readonly HandlerFactoryInterface $handlerFactory) + private HandlerFactoryInterface $handlerFactory; + + public function __construct(HandlerFactoryInterface $handlerFactory) { + $this->handlerFactory = $handlerFactory; } - /* + /** * Handle AJAX edition requests for Folder * such as coming from tag-tree widgets. + * + * @param Request $request + * @param int $folderId + * + * @return Response JSON response */ - public function editAction(Request $request, int $folderId): JsonResponse + public function editAction(Request $request, int $folderId) { $this->validateRequest($request); $this->denyAccessUnlessGranted('ROLE_ACCESS_DOCUMENTS'); @@ -73,7 +84,7 @@ public function editAction(Request $request, int $folderId): JsonResponse * @param Request $request * @return JsonResponse */ - public function searchAction(Request $request): JsonResponse + public function searchAction(Request $request) { $this->denyAccessUnlessGranted('ROLE_ACCESS_DOCUMENTS'); @@ -103,7 +114,11 @@ public function searchAction(Request $request): JsonResponse throw $this->createNotFoundException($this->getTranslator()->trans('no.folder.found')); } - protected function updatePosition(array $parameters, Folder $folder): void + /** + * @param array $parameters + * @param Folder $folder + */ + protected function updatePosition($parameters, Folder $folder): void { /* * First, we set the new parent diff --git a/src/AjaxControllers/AjaxFoldersExplorerController.php b/src/AjaxControllers/AjaxFoldersExplorerController.php index 8133a0a0..3617a29a 100644 --- a/src/AjaxControllers/AjaxFoldersExplorerController.php +++ b/src/AjaxControllers/AjaxFoldersExplorerController.php @@ -9,9 +9,17 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +/** + * @package Themes\Rozier\AjaxControllers + */ class AjaxFoldersExplorerController extends AbstractAjaxController { - public function indexAction(Request $request): JsonResponse + /** + * @param Request $request + * + * @return Response JSON response + */ + public function indexAction(Request $request) { $this->denyAccessUnlessGranted('ROLE_ACCESS_DOCUMENTS'); diff --git a/src/AjaxControllers/AjaxNodeTreeController.php b/src/AjaxControllers/AjaxNodeTreeController.php index 313eff55..88e75a43 100644 --- a/src/AjaxControllers/AjaxNodeTreeController.php +++ b/src/AjaxControllers/AjaxNodeTreeController.php @@ -14,12 +14,15 @@ use Themes\Rozier\Widgets\NodeTreeWidget; use Themes\Rozier\Widgets\TreeWidgetFactory; +/** + * @package Themes\Rozier\AjaxControllers + */ class AjaxNodeTreeController extends AbstractAjaxController { public function __construct( - private readonly NodeChrootResolver $nodeChrootResolver, - private readonly TreeWidgetFactory $treeWidgetFactory, - private readonly NodeTypes $nodeTypesBag + private NodeChrootResolver $nodeChrootResolver, + private TreeWidgetFactory $treeWidgetFactory, + private NodeTypes $nodeTypesBag ) { } @@ -93,7 +96,7 @@ public function getTreeAction(Request $request): JsonResponse $parent = $this->nodeChrootResolver->getChroot($this->getUser()); } - $nodeTree = $this->treeWidgetFactory->createRootNodeTree($parent, $translation); + $nodeTree = $this->treeWidgetFactory->createNodeTree($parent, $translation); $this->assignation['mainNodeTree'] = true; break; } diff --git a/src/AjaxControllers/AjaxNodeTypeFieldsController.php b/src/AjaxControllers/AjaxNodeTypeFieldsController.php index d825f480..e2b34626 100644 --- a/src/AjaxControllers/AjaxNodeTypeFieldsController.php +++ b/src/AjaxControllers/AjaxNodeTypeFieldsController.php @@ -8,6 +8,9 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +/** + * @package Themes\Rozier\AjaxControllers + */ class AjaxNodeTypeFieldsController extends AjaxAbstractFieldsController { /** diff --git a/src/AjaxControllers/AjaxNodeTypesController.php b/src/AjaxControllers/AjaxNodeTypesController.php index e2f2a736..183b482c 100644 --- a/src/AjaxControllers/AjaxNodeTypesController.php +++ b/src/AjaxControllers/AjaxNodeTypesController.php @@ -5,7 +5,6 @@ namespace Themes\Rozier\AjaxControllers; use Doctrine\ORM\EntityManager; -use Doctrine\ORM\Exception\NotSupported; use RZ\Roadiz\CoreBundle\Entity\NodeType; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -13,6 +12,9 @@ use Symfony\Component\Routing\Exception\InvalidParameterException; use Themes\Rozier\Models\NodeTypeModel; +/** + * @package Themes\Rozier\AjaxControllers + */ class AjaxNodeTypesController extends AbstractAjaxController { /** @@ -20,7 +22,7 @@ class AjaxNodeTypesController extends AbstractAjaxController * * @return Response JSON response */ - public function indexAction(Request $request): Response + public function indexAction(Request $request) { $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES'); $arrayFilter = []; @@ -57,9 +59,8 @@ public function indexAction(Request $request): Response * * @param Request $request * @return JsonResponse - * @throws NotSupported */ - public function listAction(Request $request): Response + public function listAction(Request $request) { $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES'); @@ -100,7 +101,7 @@ public function listAction(Request $request): Response * @param array|\Traversable $nodeTypes * @return array */ - private function normalizeNodeType(iterable $nodeTypes): array + private function normalizeNodeType($nodeTypes) { $nodeTypesArray = []; diff --git a/src/AjaxControllers/AjaxNodesController.php b/src/AjaxControllers/AjaxNodesController.php index fdf2e5e2..e8d7bb14 100644 --- a/src/AjaxControllers/AjaxNodesController.php +++ b/src/AjaxControllers/AjaxNodesController.php @@ -5,6 +5,7 @@ namespace Themes\Rozier\AjaxControllers; use Psr\Log\LoggerInterface; +use RZ\Roadiz\CoreBundle\Security\Authorization\Chroot\NodeChrootResolver; use RZ\Roadiz\CoreBundle\Entity\Node; use RZ\Roadiz\CoreBundle\Entity\Tag; use RZ\Roadiz\CoreBundle\Event\Node\NodeCreatedEvent; @@ -18,25 +19,38 @@ use RZ\Roadiz\CoreBundle\Node\NodeMover; use RZ\Roadiz\CoreBundle\Node\NodeNamePolicyInterface; use RZ\Roadiz\CoreBundle\Node\UniqueNodeGenerator; -use RZ\Roadiz\CoreBundle\Security\Authorization\Chroot\NodeChrootResolver; -use RZ\Roadiz\CoreBundle\Security\Authorization\Voter\NodeVoter; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Workflow\Registry; +/** + * @package Themes\Rozier\AjaxControllers + */ class AjaxNodesController extends AbstractAjaxController { + private NodeNamePolicyInterface $nodeNamePolicy; + private LoggerInterface $logger; + private NodeMover $nodeMover; + private NodeChrootResolver $nodeChrootResolver; + private Registry $workflowRegistry; + private UniqueNodeGenerator $uniqueNodeGenerator; + public function __construct( - private readonly NodeNamePolicyInterface $nodeNamePolicy, - private readonly LoggerInterface $logger, - private readonly NodeMover $nodeMover, - private readonly NodeChrootResolver $nodeChrootResolver, - private readonly Registry $workflowRegistry, - private readonly UniqueNodeGenerator $uniqueNodeGenerator + NodeNamePolicyInterface $nodeNamePolicy, + LoggerInterface $logger, + NodeMover $nodeMover, + NodeChrootResolver $nodeChrootResolver, + Registry $workflowRegistry, + UniqueNodeGenerator $uniqueNodeGenerator ) { + $this->nodeNamePolicy = $nodeNamePolicy; + $this->logger = $logger; + $this->nodeMover = $nodeMover; + $this->nodeChrootResolver = $nodeChrootResolver; + $this->workflowRegistry = $workflowRegistry; + $this->uniqueNodeGenerator = $uniqueNodeGenerator; } /** @@ -44,16 +58,12 @@ public function __construct( * @param int $nodeId * @return JsonResponse */ - public function getTagsAction(Request $request, int $nodeId): JsonResponse + public function getTagsAction(Request $request, int $nodeId) { + $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES'); $tags = []; - /** @var Node|null $node */ + /** @var Node $node */ $node = $this->em()->find(Node::class, (int) $nodeId); - if (null === $node) { - throw new NotFoundHttpException('Node not found'); - } - - $this->denyAccessUnlessGranted(NodeVoter::READ, $node); /** @var Tag $tag */ foreach ($node->getTags() as $tag) { @@ -70,77 +80,83 @@ public function getTagsAction(Request $request, int $nodeId): JsonResponse * such as coming from node-tree widgets. * * @param Request $request - * @param int|string $nodeId + * @param int $nodeId * * @return Response JSON response */ - public function editAction(Request $request, int|string $nodeId): Response + public function editAction(Request $request, $nodeId) { + /* + * Validate + */ $this->validateRequest($request); + $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES'); /** @var Node|null $node */ $node = $this->em()->find(Node::class, (int) $nodeId); - if (null === $node) { - throw $this->createNotFoundException($this->getTranslator()->trans('node.%nodeId%.not_exists', [ - '%nodeId%' => $nodeId, - ])); - } - /* - * Get the right update method against "_action" parameter - */ - switch ($request->get('_action')) { - case 'updatePosition': - $this->denyAccessUnlessGranted(NodeVoter::EDIT_SETTING, $node); - $this->updatePosition($request->request->all(), $node); + if ($node !== null) { + $responseArray = null; + + /* + * Get the right update method against "_action" parameter + */ + switch ($request->get('_action')) { + case 'updatePosition': + $this->updatePosition($request->request->all(), $node); + break; + case 'duplicate': + $duplicator = new NodeDuplicator( + $node, + $this->em(), + $this->nodeNamePolicy + ); + $newNode = $duplicator->duplicate(); + /* + * Dispatch event + */ + $this->dispatchEvent(new NodeCreatedEvent($newNode)); + $this->dispatchEvent(new NodeDuplicatedEvent($newNode)); + + $msg = $this->getTranslator()->trans('duplicated.node.%name%', [ + '%name%' => $node->getNodeName(), + ]); + $this->logger->info($msg, ['source' => $newNode->getNodeSources()->first()]); + + $responseArray = [ + 'statusCode' => '200', + 'status' => 'success', + 'responseText' => $msg, + ]; + break; + } + + if ($responseArray === null) { $responseArray = [ 'statusCode' => '200', 'status' => 'success', - 'responseText' => $this->getTranslator()->trans('node.%name%.was_moved', [ + 'responseText' => $this->getTranslator()->trans('node.%name%.updated', [ '%name%' => $node->getNodeName(), ]), ]; - break; - case 'duplicate': - $this->denyAccessUnlessGranted(NodeVoter::DUPLICATE, $node); - $duplicator = new NodeDuplicator( - $node, - $this->em(), - $this->nodeNamePolicy - ); - $newNode = $duplicator->duplicate(); - /* - * Dispatch event - */ - $this->dispatchEvent(new NodeCreatedEvent($newNode)); - $this->dispatchEvent(new NodeDuplicatedEvent($newNode)); - - $msg = $this->getTranslator()->trans('duplicated.node.%name%', [ - '%name%' => $node->getNodeName(), - ]); - $this->logger->info($msg, ['entity' => $newNode->getNodeSources()->first()]); + } - $responseArray = [ - 'statusCode' => '200', - 'status' => 'success', - 'responseText' => $msg, - ]; - break; - default: - throw new BadRequestHttpException('Action is not defined.'); + return new JsonResponse( + $responseArray, + Response::HTTP_PARTIAL_CONTENT + ); } - return new JsonResponse( - $responseArray, - Response::HTTP_PARTIAL_CONTENT - ); + throw $this->createNotFoundException($this->getTranslator()->trans('node.%nodeId%.not_exists', [ + '%nodeId%' => $nodeId, + ])); } /** * @param array $parameters * @param Node $node */ - protected function updatePosition(array $parameters, Node $node): void + protected function updatePosition($parameters, Node $node): void { if ($node->isLocked()) { throw new BadRequestHttpException('Locked node cannot be moved.'); @@ -179,11 +195,6 @@ protected function updatePosition(array $parameters, Node $node): void $this->dispatchEvent(new NodesSourcesUpdatedEvent($nodeSource)); } - $msg = $this->getTranslator()->trans('node.%name%.was_moved', [ - '%name%' => $node->getNodeName(), - ]); - $this->logger->info($msg, ['entity' => $node->getNodeSources()->first() ?: $node]); - $this->em()->flush(); } @@ -239,11 +250,15 @@ protected function parsePosition(array $parameters, float $default = 0.0): float * Update node's status. * * @param Request $request + * * @return JsonResponse + * @throws \Doctrine\ORM\ORMException + * @throws \Doctrine\ORM\OptimisticLockException */ public function statusesAction(Request $request): JsonResponse { $this->validateRequest($request); + $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES'); if ($request->get('nodeId', 0) <= 0) { throw new BadRequestHttpException($this->getTranslator()->trans('node.id.not_specified')); @@ -257,8 +272,6 @@ public function statusesAction(Request $request): JsonResponse ])); } - $this->denyAccessUnlessGranted(NodeVoter::EDIT_STATUS, $node); - $availableStatuses = [ 'visible' => 'setVisible', 'locked' => 'setLocked', @@ -297,14 +310,14 @@ public function statusesAction(Request $request): JsonResponse '%name%' => $node->getNodeName(), '%visible%' => $node->isVisible() ? $this->getTranslator()->trans('visible') : $this->getTranslator()->trans('invisible'), ]); - $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first() ?: $node); + $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first()); $this->dispatchEvent(new NodeVisibilityChangedEvent($node)); } else { $msg = $this->getTranslator()->trans('node.%name%.%field%.updated', [ '%name%' => $node->getNodeName(), '%field%' => $request->get('statusName'), ]); - $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first() ?: $node); + $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first()); } $this->dispatchEvent(new NodeUpdatedEvent($node)); $this->em()->flush(); @@ -348,7 +361,7 @@ protected function changeNodeStatus(Node $node, string $transition): JsonRespons '%name%' => $node->getNodeName(), '%status%' => $this->getTranslator()->trans(Node::getStatusLabel($node->getStatus())), ]); - $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first() ?: $node); + $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first()); return new JsonResponse( [ @@ -372,9 +385,9 @@ public function quickAddAction(Request $request): JsonResponse * Validate */ $this->validateRequest($request); + $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES'); try { - // Access security is handled by UniqueNodeGenerator $source = $this->uniqueNodeGenerator->generateFromRequest($request); /* diff --git a/src/AjaxControllers/AjaxNodesExplorerController.php b/src/AjaxControllers/AjaxNodesExplorerController.php index 7efcfcd4..834fc240 100644 --- a/src/AjaxControllers/AjaxNodesExplorerController.php +++ b/src/AjaxControllers/AjaxNodesExplorerController.php @@ -14,27 +14,34 @@ use RZ\Roadiz\CoreBundle\EntityApi\NodeTypeApi; use RZ\Roadiz\CoreBundle\SearchEngine\ClientRegistry; use RZ\Roadiz\CoreBundle\SearchEngine\NodeSourceSearchHandlerInterface; -use RZ\Roadiz\CoreBundle\SearchEngine\SolrSearchResultItem; -use RZ\Roadiz\CoreBundle\Security\Authorization\Voter\NodeVoter; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Exception\InvalidParameterException; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; -use Symfony\Bundle\SecurityBundle\Security; use Themes\Rozier\Models\NodeModel; use Themes\Rozier\Models\NodeSourceModel; -final class AjaxNodesExplorerController extends AbstractAjaxController +class AjaxNodesExplorerController extends AbstractAjaxController { + private SerializerInterface $serializer; + private ClientRegistry $clientRegistry; + private NodeSourceSearchHandlerInterface $nodeSourceSearchHandler; + private NodeTypeApi $nodeTypeApi; + private UrlGeneratorInterface $urlGenerator; + public function __construct( - private readonly SerializerInterface $serializer, - private readonly ClientRegistry $clientRegistry, - private readonly NodeSourceSearchHandlerInterface $nodeSourceSearchHandler, - private readonly NodeTypeApi $nodeTypeApi, - private readonly UrlGeneratorInterface $urlGenerator, - private readonly Security $security, + SerializerInterface $serializer, + ClientRegistry $clientRegistry, + NodeSourceSearchHandlerInterface $nodeSourceSearchHandler, + NodeTypeApi $nodeTypeApi, + UrlGeneratorInterface $urlGenerator ) { + $this->nodeSourceSearchHandler = $nodeSourceSearchHandler; + $this->nodeTypeApi = $nodeTypeApi; + $this->serializer = $serializer; + $this->urlGenerator = $urlGenerator; + $this->clientRegistry = $clientRegistry; } protected function getItemPerPage(): int @@ -54,8 +61,7 @@ protected function isSearchEngineAvailable(Request $request): bool */ public function indexAction(Request $request): Response { - // Only requires Search permission for nodes - $this->denyAccessUnlessGranted(NodeVoter::SEARCH); + $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES'); $criteria = $this->parseFilterFromRequest($request); $sorting = $this->parseSortingFromRequest($request); @@ -185,6 +191,7 @@ protected function getSolrSearchResults( $arrayFilter, $this->getItemPerPage(), true, + 2, (int) $currentPage ); $pageCount = ceil($results->getResultCount() / $this->getItemPerPage()); @@ -214,8 +221,7 @@ protected function getSolrSearchResults( */ public function listAction(Request $request): JsonResponse { - // Only requires Search permission for nodes - $this->denyAccessUnlessGranted(NodeVoter::SEARCH); + $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES'); if (!$request->query->has('ids')) { throw new InvalidParameterException('Ids should be provided within an array'); @@ -250,38 +256,32 @@ public function listAction(Request $request): JsonResponse /** * Normalize response Node list result. * - * @param iterable $nodes + * @param array|\Traversable $nodes * @return array */ - private function normalizeNodes(iterable $nodes): array + private function normalizeNodes($nodes) { $nodesArray = []; foreach ($nodes as $node) { - if ($node instanceof SolrSearchResultItem) { - $item = $node->getItem(); - if ($item instanceof NodesSources || $item instanceof Node) { - $this->normalizeItem($item, $nodesArray); + if (null !== $node) { + if ($node instanceof NodesSources) { + if (!key_exists($node->getNode()->getId(), $nodesArray)) { + $nodeModel = new NodeSourceModel($node, $this->urlGenerator); + $nodesArray[$node->getNode()->getId()] = $nodeModel->toArray(); + } + } else { + if (!key_exists($node->getId(), $nodesArray)) { + $nodeModel = new NodeModel($node, $this->urlGenerator); + $nodesArray[$node->getId()] = $nodeModel->toArray(); + } } - } else { - $this->normalizeItem($node, $nodesArray); } } return array_values($nodesArray); } - private function normalizeItem(NodesSources|Node $item, array &$nodesArray): void - { - if ($item instanceof NodesSources && !key_exists($item->getNode()->getId(), $nodesArray)) { - $nodeSourceModel = new NodeSourceModel($item, $this->urlGenerator, $this->security); - $nodesArray[$item->getNode()->getId()] = $nodeSourceModel->toArray(); - } elseif ($item instanceof Node && !key_exists($item->getId(), $nodesArray)) { - $nodeModel = new NodeModel($item, $this->urlGenerator, $this->security); - $nodesArray[$item->getId()] = $nodeModel->toArray(); - } - } - /** * @param array $data * @return JsonResponse diff --git a/src/AjaxControllers/AjaxSearchNodesSourcesController.php b/src/AjaxControllers/AjaxSearchNodesSourcesController.php index b8c34594..894c7145 100644 --- a/src/AjaxControllers/AjaxSearchNodesSourcesController.php +++ b/src/AjaxControllers/AjaxSearchNodesSourcesController.php @@ -8,35 +8,36 @@ use RZ\Roadiz\CoreBundle\Entity\NodesSourcesDocuments; use RZ\Roadiz\CoreBundle\Entity\Translation; use RZ\Roadiz\CoreBundle\SearchEngine\GlobalNodeSourceSearchHandler; -use RZ\Roadiz\CoreBundle\Security\Authorization\Voter\NodeVoter; use RZ\Roadiz\Documents\UrlGenerators\DocumentUrlGeneratorInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; -use Symfony\Bundle\SecurityBundle\Security; +/** + * @package Themes\Rozier\AjaxControllers + */ class AjaxSearchNodesSourcesController extends AbstractAjaxController { - public const RESULT_COUNT = 10; + public const RESULT_COUNT = 8; + private DocumentUrlGeneratorInterface $documentUrlGenerator; - public function __construct( - private readonly DocumentUrlGeneratorInterface $documentUrlGenerator, - private readonly Security $security - ) { + public function __construct(DocumentUrlGeneratorInterface $documentUrlGenerator) + { + $this->documentUrlGenerator = $documentUrlGenerator; } /** * Handle AJAX edition requests for Node - * such as coming from node-tree widgets. + * such as coming from nodetree widgets. * * @param Request $request * * @return Response JSON response */ - public function searchAction(Request $request): Response + public function searchAction(Request $request) { - $this->denyAccessUnlessGranted(NodeVoter::SEARCH); + $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES'); if (!$request->query->has('searchTerms') || $request->query->get('searchTerms') == '') { throw new BadRequestHttpException('searchTerms parameter is missing.'); @@ -45,13 +46,13 @@ public function searchAction(Request $request): Response $searchHandler = new GlobalNodeSourceSearchHandler($this->em()); $searchHandler->setDisplayNonPublishedNodes(true); - /** @var array $nodesSources */ + /** @var array $nodesSources */ $nodesSources = $searchHandler->getNodeSourcesBySearchTerm( $request->get('searchTerms'), static::RESULT_COUNT ); - if (count($nodesSources) > 0) { + if (null !== $nodesSources && count($nodesSources) > 0) { $responseArray = [ 'statusCode' => Response::HTTP_OK, 'status' => 'success', @@ -62,7 +63,6 @@ public function searchAction(Request $request): Response foreach ($nodesSources as $source) { if ( $source instanceof NodesSources && - $this->security->isGranted(NodeVoter::READ, $source) && !key_exists($source->getNode()->getId(), $responseArray['data']) ) { $responseArray['data'][$source->getNode()->getId()] = $this->getNodeSourceData($source); diff --git a/src/AjaxControllers/AjaxTagTreeController.php b/src/AjaxControllers/AjaxTagTreeController.php index 4712b013..f56f1f47 100644 --- a/src/AjaxControllers/AjaxTagTreeController.php +++ b/src/AjaxControllers/AjaxTagTreeController.php @@ -10,9 +10,12 @@ use Themes\Rozier\Widgets\TagTreeWidget; use Themes\Rozier\Widgets\TreeWidgetFactory; +/** + * @package Themes\Rozier\AjaxControllers + */ class AjaxTagTreeController extends AbstractAjaxController { - public function __construct(private readonly TreeWidgetFactory $treeWidgetFactory) + public function __construct(private TreeWidgetFactory $treeWidgetFactory) { } diff --git a/src/AjaxControllers/AjaxTagsController.php b/src/AjaxControllers/AjaxTagsController.php index 08a4a2cb..493b55e0 100644 --- a/src/AjaxControllers/AjaxTagsController.php +++ b/src/AjaxControllers/AjaxTagsController.php @@ -19,12 +19,18 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Themes\Rozier\Models\TagModel; +/** + * @package Themes\Rozier\AjaxControllers + */ class AjaxTagsController extends AbstractAjaxController { - public function __construct( - private readonly HandlerFactoryInterface $handlerFactory, - private readonly UrlGeneratorInterface $urlGenerator - ) { + private HandlerFactoryInterface $handlerFactory; + private UrlGeneratorInterface $urlGenerator; + + public function __construct(HandlerFactoryInterface $handlerFactory, UrlGeneratorInterface $urlGenerator) + { + $this->handlerFactory = $handlerFactory; + $this->urlGenerator = $urlGenerator; } /** diff --git a/src/Controllers/AbstractAdminController.php b/src/Controllers/AbstractAdminController.php index 2d73a9ba..e6574ce3 100644 --- a/src/Controllers/AbstractAdminController.php +++ b/src/Controllers/AbstractAdminController.php @@ -8,25 +8,33 @@ use JMS\Serializer\SerializationContext; use JMS\Serializer\SerializerInterface; use RZ\Roadiz\Core\AbstractEntities\PersistableInterface; -use RZ\Roadiz\CoreBundle\ListManager\SessionListFilters; use Symfony\Component\Form\Exception\InvalidConfigurationException; use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\String\UnicodeString; use Symfony\Contracts\EventDispatcher\Event; use Themes\Rozier\RozierApp; +use Themes\Rozier\Utils\SessionListFilters; abstract class AbstractAdminController extends RozierApp { public const ITEM_PER_PAGE = 20; - public function __construct( - protected readonly SerializerInterface $serializer, - protected readonly UrlGeneratorInterface $urlGenerator - ) { + protected SerializerInterface $serializer; + protected UrlGeneratorInterface $urlGenerator; + + /** + * @param SerializerInterface $serializer + * @param UrlGeneratorInterface $urlGenerator + */ + public function __construct(SerializerInterface $serializer, UrlGeneratorInterface $urlGenerator) + { + $this->serializer = $serializer; + $this->urlGenerator = $urlGenerator; } /** @@ -60,42 +68,14 @@ protected function getRepository(): ObjectRepository return $this->em()->getRepository($this->getEntityClass()); } - /** - * @return string - */ - protected function getRequiredDeletionRole(): string - { - return $this->getRequiredRole(); - } - - protected function getRequiredListingRole(): string - { - return $this->getRequiredRole(); - } - - protected function getRequiredCreationRole(): string - { - return $this->getRequiredRole(); - } - - protected function getRequiredEditionRole(): string - { - return $this->getRequiredRole(); - } - - protected function getRequiredExportRole(): string - { - return $this->getRequiredRole(); - } - /** * @param Request $request * @return Response|null * @throws \Twig\Error\RuntimeError */ - public function defaultAction(Request $request): ?Response + public function defaultAction(Request $request) { - $this->denyAccessUnlessGranted($this->getRequiredListingRole()); + $this->denyAccessUnlessGranted($this->getRequiredRole()); $this->additionalAssignation($request); $elm = $this->createEntityListManager( @@ -124,12 +104,12 @@ public function defaultAction(Request $request): ?Response /** * @param Request $request - * @return Response|null + * @return RedirectResponse|Response|null * @throws \Twig\Error\RuntimeError */ - public function addAction(Request $request): ?Response + public function addAction(Request $request) { - $this->denyAccessUnlessGranted($this->getRequiredCreationRole()); + $this->denyAccessUnlessGranted($this->getRequiredRole()); $this->additionalAssignation($request); $item = $this->createEmptyItem($request); @@ -158,7 +138,7 @@ public function addAction(Request $request): ?Response '%namespace%' => $this->getTranslator()->trans($this->getNamespace()) ] ); - $this->publishConfirmMessage($request, $msg, $item); + $this->publishConfirmMessage($request, $msg); return $this->getPostSubmitResponse($item, true, $request); } @@ -180,9 +160,9 @@ public function addAction(Request $request): ?Response * @return Response|null * @throws \Twig\Error\RuntimeError */ - public function editAction(Request $request, $id): ?Response + public function editAction(Request $request, $id) { - $this->denyAccessUnlessGranted($this->getRequiredEditionRole()); + $this->denyAccessUnlessGranted($this->getRequiredRole()); $this->additionalAssignation($request); /** @var mixed|object|null $item */ @@ -219,7 +199,7 @@ public function editAction(Request $request, $id): ?Response '%namespace%' => $this->getTranslator()->trans($this->getNamespace()) ] ); - $this->publishConfirmMessage($request, $msg, $item); + $this->publishConfirmMessage($request, $msg); return $this->getPostSubmitResponse($item, false, $request); } @@ -237,7 +217,7 @@ public function editAction(Request $request, $id): ?Response public function exportAction(Request $request): JsonResponse { - $this->denyAccessUnlessGranted($this->getRequiredExportRole()); + $this->denyAccessUnlessGranted($this->getRequiredRole()); $this->additionalAssignation($request); $items = $this->getRepository()->findAll(); @@ -263,10 +243,10 @@ public function exportAction(Request $request): JsonResponse /** * @param Request $request * @param int|string $id Numeric ID or UUID - * @return Response|null + * @return RedirectResponse|Response|null * @throws \Twig\Error\RuntimeError */ - public function deleteAction(Request $request, $id): ?Response + public function deleteAction(Request $request, $id) { $this->denyAccessUnlessGranted($this->getRequiredDeletionRole()); $this->additionalAssignation($request); @@ -304,7 +284,7 @@ public function deleteAction(Request $request, $id): ?Response '%namespace%' => $this->getTranslator()->trans($this->getNamespace()) ] ); - $this->publishConfirmMessage($request, $msg, $item); + $this->publishConfirmMessage($request, $msg); return $this->getPostDeleteResponse($item); } @@ -348,7 +328,15 @@ abstract protected function getTemplateFolder(): string; abstract protected function getRequiredRole(): string; /** - * @return class-string + * @return string + */ + protected function getRequiredDeletionRole(): string + { + return $this->getRequiredRole(); + } + + /** + * @return class-string */ abstract protected function getEntityClass(): string; @@ -424,26 +412,14 @@ protected function getPostSubmitResponse( bool $forceDefaultEditRoute = false, ?Request $request = null ): Response { - if (null === $request) { - // Redirect to default route if no request provided - return $this->redirect($this->urlGenerator->generate( - $this->getEditRouteName(), - $this->getEditRouteParameters($item) - )); - } - - $route = $request->attributes->get('_route'); - $referrer = $request->query->get('referer'); - /* * Force redirect to avoid resending form when refreshing page */ if ( - \is_string($referrer) && - $referrer !== '' && - (new UnicodeString($referrer))->trim()->startsWith('/') + null !== $request && $request->query->has('referer') && + (new UnicodeString($request->query->get('referer')))->startsWith('/') ) { - return $this->redirect($referrer); + return $this->redirect($request->query->get('referer')); } /* @@ -451,8 +427,8 @@ protected function getPostSubmitResponse( */ if ( false === $forceDefaultEditRoute && - \is_string($route) && - $route !== '' + null !== $request && + null !== $route = $request->attributes->get('_route') ) { return $this->redirect($this->urlGenerator->generate( $route, @@ -490,27 +466,21 @@ protected function getPostDeleteResponse(PersistableInterface $item): Response } /** - * @template T of object|Event - * @param T|iterable|array|null $event - * @return T|iterable|array|null + * @param Event|Event[]|mixed|null $event + * @return object|object[]|null */ - protected function dispatchSingleOrMultipleEvent(mixed $event): object|array|null + protected function dispatchSingleOrMultipleEvent($event) { if (null === $event) { return null; } if ($event instanceof Event) { - // @phpstan-ignore-next-line return $this->dispatchEvent($event); } - if (\is_iterable($event)) { + if (is_iterable($event)) { $events = []; - /** @var T|null $singleEvent */ foreach ($event as $singleEvent) { - $returningEvent = $this->dispatchSingleOrMultipleEvent($singleEvent); - if ($returningEvent instanceof Event) { - $events[] = $returningEvent; - } + $events[] = $this->dispatchSingleOrMultipleEvent($singleEvent); } return $events; } diff --git a/src/Controllers/AbstractAdminWithBulkController.php b/src/Controllers/AbstractAdminWithBulkController.php index 1efa45a5..bbd1ed32 100644 --- a/src/Controllers/AbstractAdminWithBulkController.php +++ b/src/Controllers/AbstractAdminWithBulkController.php @@ -17,12 +17,15 @@ abstract class AbstractAdminWithBulkController extends AbstractAdminController { + protected FormFactoryInterface $formFactory; + public function __construct( - protected readonly FormFactoryInterface $formFactory, + FormFactoryInterface $formFactory, SerializerInterface $serializer, UrlGeneratorInterface $urlGenerator ) { parent::__construct($serializer, $urlGenerator); + $this->formFactory = $formFactory; } protected function additionalAssignation(Request $request): void @@ -167,7 +170,7 @@ protected function bulkAction( '%namespace%' => $this->getTranslator()->trans($this->getNamespace()) ] ); - $this->publishConfirmMessage($request, $msg, $item); + $this->publishConfirmMessage($request, $msg); } } $this->em()->flush(); diff --git a/src/Controllers/Attributes/AttributeController.php b/src/Controllers/Attributes/AttributeController.php index 49883ec6..dcfe7c0d 100644 --- a/src/Controllers/Attributes/AttributeController.php +++ b/src/Controllers/Attributes/AttributeController.php @@ -11,25 +11,26 @@ use RZ\Roadiz\CoreBundle\Form\AttributeType; use RZ\Roadiz\CoreBundle\Importer\AttributeImporter; use Symfony\Component\Form\FormError; -use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; -use Themes\Rozier\Controllers\AbstractAdminWithBulkController; -use Twig\Error\RuntimeError; +use Themes\Rozier\Controllers\AbstractAdminController; -class AttributeController extends AbstractAdminWithBulkController +class AttributeController extends AbstractAdminController { + private AttributeImporter $attributeImporter; + public function __construct( - private readonly AttributeImporter $attributeImporter, - FormFactoryInterface $formFactory, + AttributeImporter $attributeImporter, SerializerInterface $serializer, UrlGeneratorInterface $urlGenerator ) { - parent::__construct($formFactory, $serializer, $urlGenerator); + parent::__construct($serializer, $urlGenerator); + $this->attributeImporter = $attributeImporter; } + /** * @inheritDoc */ @@ -38,11 +39,6 @@ protected function supports(PersistableInterface $item): bool return $item instanceof Attribute; } - protected function getBulkDeleteRouteName(): ?string - { - return 'attributesBulkDeletePage'; - } - /** * @inheritDoc */ @@ -107,10 +103,7 @@ protected function getFormType(): string */ protected function getDefaultOrder(Request $request): array { - return [ - 'weight' => 'DESC', - 'code' => 'ASC', - ]; + return ['code' => 'ASC']; } /** @@ -143,9 +136,8 @@ protected function getEntityName(PersistableInterface $item): string /** * @param Request $request * @return Response - * @throws RuntimeError */ - public function importAction(Request $request): Response + public function importAction(Request $request) { $this->denyAccessUnlessGranted('ROLE_ACCESS_ATTRIBUTES'); @@ -157,21 +149,10 @@ public function importAction(Request $request): Response $file = $form->get('file')->getData(); if ($file->isValid()) { - $serializedData = \file_get_contents($file->getPathname()); - if (false === $serializedData) { - throw new \RuntimeException('Cannot read uploaded file.'); - } + $serializedData = file_get_contents($file->getPathname()); $this->attributeImporter->import($serializedData); $this->em()->flush(); - - $msg = $this->getTranslator()->trans( - '%namespace%.imported', - [ - '%namespace%' => $this->getTranslator()->trans($this->getNamespace()) - ] - ); - $this->publishConfirmMessage($request, $msg); return $this->redirectToRoute('attributesHomePage'); } $form->addError(new FormError($this->getTranslator()->trans('file.not_uploaded'))); diff --git a/src/Controllers/Attributes/AttributeGroupController.php b/src/Controllers/Attributes/AttributeGroupController.php index 5fc541c3..69eeb70f 100644 --- a/src/Controllers/Attributes/AttributeGroupController.php +++ b/src/Controllers/Attributes/AttributeGroupController.php @@ -10,6 +10,9 @@ use Symfony\Component\HttpFoundation\Request; use Themes\Rozier\Controllers\AbstractAdminController; +/** + * @package Themes\Rozier\Controllers\Attributes + */ class AttributeGroupController extends AbstractAdminController { /** diff --git a/src/Controllers/CacheController.php b/src/Controllers/CacheController.php index 0020827d..ead8047e 100644 --- a/src/Controllers/CacheController.php +++ b/src/Controllers/CacheController.php @@ -14,10 +14,15 @@ final class CacheController extends RozierApp { + private LoggerInterface $logger; + private CacheClearerInterface $cacheClearer; + public function __construct( - private readonly CacheClearerInterface $cacheClearer, - private readonly LoggerInterface $logger + CacheClearerInterface $cacheClearer, + LoggerInterface $logger ) { + $this->logger = $logger; + $this->cacheClearer = $cacheClearer; } public function deleteDoctrineCache(Request $request): Response @@ -47,7 +52,7 @@ public function deleteDoctrineCache(Request $request): Response */ return $this->redirectToRoute('adminHomePage'); } - $this->prepareBaseAssignation(); + $this->assignation['form'] = $form->createView(); return $this->render('@RoadizRozier/cache/deleteDoctrine.html.twig', $this->assignation); @@ -77,7 +82,6 @@ public function deleteAssetsCache(Request $request): Response return $this->redirectToRoute('adminHomePage'); } - $this->prepareBaseAssignation(); $this->assignation['form'] = $form->createView(); return $this->render('@RoadizRozier/cache/deleteAssets.html.twig', $this->assignation); diff --git a/src/Controllers/CustomForms/CustomFormAnswersController.php b/src/Controllers/CustomForms/CustomFormAnswersController.php index 7aea9cfe..6a015479 100644 --- a/src/Controllers/CustomForms/CustomFormAnswersController.php +++ b/src/Controllers/CustomForms/CustomFormAnswersController.php @@ -16,6 +16,9 @@ use Themes\Rozier\RozierApp; use Twig\Error\RuntimeError; +/** + * @package Themes\Rozier\Controllers + */ class CustomFormAnswersController extends RozierApp { /** @@ -84,7 +87,7 @@ public function deleteAction(Request $request, int $customFormAnswerId): Respons $this->em()->flush(); $msg = $this->getTranslator()->trans('customFormAnswer.%id%.deleted', ['%id%' => $customFormAnswer->getId()]); - $this->publishConfirmMessage($request, $msg, $customFormAnswer); + $this->publishConfirmMessage($request, $msg); /* * Redirect to update schema page */ diff --git a/src/Controllers/CustomForms/CustomFormFieldAttributesController.php b/src/Controllers/CustomForms/CustomFormFieldAttributesController.php index e5747a84..a6fac366 100644 --- a/src/Controllers/CustomForms/CustomFormFieldAttributesController.php +++ b/src/Controllers/CustomForms/CustomFormFieldAttributesController.php @@ -11,6 +11,9 @@ use Themes\Rozier\RozierApp; use Twig\Error\RuntimeError; +/** + * @package Themes\Rozier\Controllers + */ class CustomFormFieldAttributesController extends RozierApp { /** @@ -51,8 +54,8 @@ protected function getAnswersByGroups(iterable $answers): array /** @var CustomFormFieldAttribute $answer */ foreach ($answers as $answer) { $groupName = $answer->getCustomFormField()->getGroupName(); - if (\is_string($groupName) && $groupName !== '') { - if (!isset($fieldsArray[$groupName]) || !\is_array($fieldsArray[$groupName])) { + if ($groupName != '') { + if (!isset($fieldsArray[$groupName])) { $fieldsArray[$groupName] = []; } $fieldsArray[$groupName][] = $answer; diff --git a/src/Controllers/CustomForms/CustomFormFieldsController.php b/src/Controllers/CustomForms/CustomFormFieldsController.php index 66e63255..0897579d 100644 --- a/src/Controllers/CustomForms/CustomFormFieldsController.php +++ b/src/Controllers/CustomForms/CustomFormFieldsController.php @@ -18,6 +18,9 @@ use Themes\Rozier\RozierApp; use Twig\Error\RuntimeError; +/** + * @package Themes\Rozier\Controllers + */ class CustomFormFieldsController extends RozierApp { /** @@ -61,7 +64,6 @@ public function editAction(Request $request, int $customFormFieldId): Response /** @var CustomFormField|null $field */ $field = $this->em()->find(CustomFormField::class, $customFormFieldId); - if ($field === null) { throw new ResourceNotFoundException(); } @@ -75,7 +77,7 @@ public function editAction(Request $request, int $customFormFieldId): Response $this->em()->flush(); $msg = $this->getTranslator()->trans('customFormField.%name%.updated', ['%name%' => $field->getName()]); - $this->publishConfirmMessage($request, $msg, $field); + $this->publishConfirmMessage($request, $msg); /* * Redirect to update schema page @@ -106,14 +108,14 @@ public function addAction(Request $request, int $customFormId): Response { $this->denyAccessUnlessGranted('ROLE_ACCESS_CUSTOMFORMS'); + $field = new CustomFormField(); $customForm = $this->em()->find(CustomForm::class, $customFormId); + $field->setCustomForm($customForm); + if ($customForm === null) { throw new ResourceNotFoundException(); } - $field = new CustomFormField(); - $field->setCustomForm($customForm); - $this->assignation['customForm'] = $customForm; $this->assignation['field'] = $field; $form = $this->createForm(CustomFormFieldType::class, $field); @@ -128,7 +130,7 @@ public function addAction(Request $request, int $customFormId): Response 'customFormField.%name%.created', ['%name%' => $field->getName()] ); - $this->publishConfirmMessage($request, $msg, $field); + $this->publishConfirmMessage($request, $msg); /* * Redirect to update schema page @@ -141,7 +143,7 @@ public function addAction(Request $request, int $customFormId): Response ); } catch (Exception $e) { $msg = $e->getMessage(); - $this->publishErrorMessage($request, $msg, $field); + $this->publishErrorMessage($request, $msg); /* * Redirect to add page */ diff --git a/src/Controllers/CustomForms/CustomFormsController.php b/src/Controllers/CustomForms/CustomFormsController.php index 042983c8..7956bec1 100644 --- a/src/Controllers/CustomForms/CustomFormsController.php +++ b/src/Controllers/CustomForms/CustomFormsController.php @@ -6,62 +6,98 @@ use RZ\Roadiz\Core\AbstractEntities\PersistableInterface; use RZ\Roadiz\CoreBundle\Entity\CustomForm; +use RZ\Roadiz\RozierBundle\Form\CustomFormType; use Symfony\Component\HttpFoundation\Request; -use Themes\Rozier\Controllers\AbstractAdminWithBulkController; -use Themes\Rozier\Forms\CustomFormType; +use Themes\Rozier\Controllers\AbstractAdminController; -class CustomFormsController extends AbstractAdminWithBulkController +/** + * @package Themes\Rozier\Controllers + */ +class CustomFormsController extends AbstractAdminController { + /** + * @inheritDoc + */ protected function supports(PersistableInterface $item): bool { return $item instanceof CustomForm; } + /** + * @inheritDoc + */ protected function getNamespace(): string { return 'custom-form'; } + /** + * @inheritDoc + */ protected function createEmptyItem(Request $request): PersistableInterface { return new CustomForm(); } + /** + * @inheritDoc + */ protected function getTemplateFolder(): string { return '@RoadizRozier/custom-forms'; } + /** + * @inheritDoc + */ protected function getRequiredRole(): string { return 'ROLE_ACCESS_CUSTOMFORMS'; } + /** + * @inheritDoc + */ protected function getEntityClass(): string { return CustomForm::class; } + /** + * @inheritDoc + */ protected function getFormType(): string { return CustomFormType::class; } + /** + * @inheritDoc + */ protected function getDefaultOrder(Request $request): array { return ['createdAt' => 'DESC']; } + /** + * @inheritDoc + */ protected function getDefaultRouteName(): string { return 'customFormsHomePage'; } + /** + * @inheritDoc + */ protected function getEditRouteName(): string { return 'customFormsEditPage'; } + /** + * @inheritDoc + */ protected function getEntityName(PersistableInterface $item): string { if ($item instanceof CustomForm) { @@ -69,9 +105,4 @@ protected function getEntityName(PersistableInterface $item): string } throw new \InvalidArgumentException('Item should be instance of ' . $this->getEntityClass()); } - - protected function getBulkDeleteRouteName(): ?string - { - return 'customFormsBulkDeletePage'; - } } diff --git a/src/Controllers/CustomForms/CustomFormsUtilsController.php b/src/Controllers/CustomForms/CustomFormsUtilsController.php index 535da55d..ba24ba04 100644 --- a/src/Controllers/CustomForms/CustomFormsUtilsController.php +++ b/src/Controllers/CustomForms/CustomFormsUtilsController.php @@ -4,30 +4,33 @@ namespace Themes\Rozier\Controllers\CustomForms; -use Doctrine\Persistence\ManagerRegistry; use PhpOffice\PhpSpreadsheet\Exception; -use RZ\Roadiz\CoreBundle\CustomForm\CustomFormAnswerSerializer; use RZ\Roadiz\CoreBundle\Entity\CustomForm; +use RZ\Roadiz\CoreBundle\Entity\CustomFormAnswer; +use RZ\Roadiz\CoreBundle\CustomForm\CustomFormAnswerSerializer; +use RZ\Roadiz\CoreBundle\Xlsx\XlsxExporter; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\ResponseHeaderBag; -use Symfony\Component\HttpFoundation\StreamedResponse; -use Symfony\Component\Serializer\SerializerInterface; -use Symfony\Contracts\Translation\TranslatorInterface; use Themes\Rozier\RozierApp; +/** + * @package Themes\Rozier\Controllers + */ class CustomFormsUtilsController extends RozierApp { - public function __construct( - private readonly ManagerRegistry $managerRegistry, - private readonly TranslatorInterface $translator, - private readonly CustomFormAnswerSerializer $customFormAnswerSerializer, - private readonly SerializerInterface $serializer - ) { + private CustomFormAnswerSerializer $customFormAnswerSerializer; + + /** + * @param CustomFormAnswerSerializer $customFormAnswerSerializer + */ + public function __construct(CustomFormAnswerSerializer $customFormAnswerSerializer) + { + $this->customFormAnswerSerializer = $customFormAnswerSerializer; } /** - * Export all custom form's answers in a CSV file. + * Export all custom form's answer in a Xlsx file (.rzt). * * @param Request $request * @param int $id @@ -38,35 +41,45 @@ public function __construct( */ public function exportAction(Request $request, int $id): Response { - $customForm = $this->managerRegistry->getRepository(CustomForm::class)->find($id); + /** @var CustomForm|null $customForm */ + $customForm = $this->em()->find(CustomForm::class, $id); if (null === $customForm) { throw $this->createNotFoundException(); } $answers = $customForm->getCustomFormAnswers(); - $answersArray = []; + + /** + * @var int $key + * @var CustomFormAnswer $answer + */ foreach ($answers as $key => $answer) { - $answersArray[$key] = $this->customFormAnswerSerializer->toSimpleArray($answer); + $array = array_merge( + [$answer->getIp(), $answer->getSubmittedAt()], + $this->customFormAnswerSerializer->toSimpleArray($answer) + ); + $answers[$key] = $array; } + $keys = ["ip", "submitted.date"]; + $fields = $customForm->getFieldsLabels(); - $keys = [ - 'ip', - 'submitted.date', - ...$fields - ]; - - $response = new StreamedResponse(function () use ($answersArray, $keys) { - echo $this->serializer->serialize($answersArray, 'csv', [ - 'csv_headers' => $keys - ]); - }); - $response->headers->set('Content-Type', 'text/csv'); + $keys = array_merge($keys, $fields); + + $exporter = new XlsxExporter($this->getTranslator()); + $xlsx = $exporter->exportXlsx($answers, $keys); + + $response = new Response( + $xlsx, + Response::HTTP_OK, + [] + ); + $response->headers->set( 'Content-Disposition', $response->headers->makeDisposition( ResponseHeaderBag::DISPOSITION_ATTACHMENT, - $customForm->getName() . '.csv' + $customForm->getName() . '.xlsx' ) ); @@ -80,12 +93,15 @@ public function exportAction(Request $request, int $id): Response * * @param Request $request * @param int $id + * * @return Response */ public function duplicateAction(Request $request, int $id): Response { $this->denyAccessUnlessGranted('ROLE_ACCESS_CUSTOMFORMS'); - $existingCustomForm = $this->managerRegistry->getRepository(CustomForm::class)->find($id); + /** @var CustomForm|null $existingCustomForm */ + $existingCustomForm = $this->em()->find(CustomForm::class, $id); + if (null === $existingCustomForm) { throw $this->createNotFoundException(); } @@ -94,7 +110,7 @@ public function duplicateAction(Request $request, int $id): Response $newCustomForm = clone $existingCustomForm; $newCustomForm->setCreatedAt(new \DateTime()); $newCustomForm->setUpdatedAt(new \DateTime()); - $em = $this->managerRegistry->getManager(); + $em = $this->em(); foreach ($newCustomForm->getFields() as $field) { $em->persist($field); @@ -103,11 +119,11 @@ public function duplicateAction(Request $request, int $id): Response $em->persist($newCustomForm); $em->flush(); - $msg = $this->translator->trans("duplicated.custom.form.%name%", [ + $msg = $this->getTranslator()->trans("duplicated.custom.form.%name%", [ '%name%' => $existingCustomForm->getDisplayName(), ]); - $this->publishConfirmMessage($request, $msg, $newCustomForm); + $this->publishConfirmMessage($request, $msg); return $this->redirectToRoute( 'customFormsEditPage', @@ -116,12 +132,11 @@ public function duplicateAction(Request $request, int $id): Response } catch (\Exception $e) { $this->publishErrorMessage( $request, - $this->translator->trans("impossible.duplicate.custom.form.%name%", [ + $this->getTranslator()->trans("impossible.duplicate.custom.form.%name%", [ '%name%' => $existingCustomForm->getDisplayName(), - ]), - $newCustomForm + ]) ); - $this->publishErrorMessage($request, $e->getMessage(), $existingCustomForm); + $this->publishErrorMessage($request, $e->getMessage()); return $this->redirectToRoute( 'customFormsEditPage', diff --git a/src/Controllers/DashboardController.php b/src/Controllers/DashboardController.php index 5a28614c..81178887 100644 --- a/src/Controllers/DashboardController.php +++ b/src/Controllers/DashboardController.php @@ -4,12 +4,15 @@ namespace Themes\Rozier\Controllers; -use RZ\Roadiz\CoreBundle\Logger\Entity\Log; +use RZ\Roadiz\CoreBundle\Entity\Log; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Themes\Rozier\RozierApp; use Twig\Error\RuntimeError; +/** + * @package Themes\Rozier\Controllers + */ class DashboardController extends RozierApp { /** diff --git a/src/Controllers/Documents/DocumentTranslationsController.php b/src/Controllers/Documents/DocumentTranslationsController.php index e8c1138f..70e81f4d 100644 --- a/src/Controllers/Documents/DocumentTranslationsController.php +++ b/src/Controllers/Documents/DocumentTranslationsController.php @@ -12,7 +12,6 @@ use RZ\Roadiz\CoreBundle\Entity\Translation; use RZ\Roadiz\CoreBundle\Event\Document\DocumentTranslationUpdatedEvent; use Symfony\Component\Form\Extension\Core\Type\HiddenType; -use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Exception\ResourceNotFoundException; @@ -23,19 +22,22 @@ use Themes\Rozier\Traits\VersionedControllerTrait; use Twig\Error\RuntimeError; +/** + * @package Themes\Rozier\Controllers\Documents + */ class DocumentTranslationsController extends RozierApp { use VersionedControllerTrait; /** * @param Request $request - * @param int $documentId - * @param int|null $translationId + * @param int $documentId + * @param int|null $translationId * * @return Response * @throws RuntimeError */ - public function editAction(Request $request, int $documentId, ?int $translationId = null): Response + public function editAction(Request $request, int $documentId, ?int $translationId = null) { $this->denyAccessUnlessGranted('ROLE_ACCESS_DOCUMENTS'); @@ -63,65 +65,69 @@ public function editAction(Request $request, int $documentId, ?int $translationI $documentTr = $this->createDocumentTranslation($document, $translation); } - if ($documentTr === null || $document === null) { - throw new ResourceNotFoundException(); - } - - $this->assignation['document'] = $document; - $this->assignation['translation'] = $translation; - $this->assignation['documentTr'] = $documentTr; + if ($documentTr !== null && $document !== null) { + $this->assignation['document'] = $document; + $this->assignation['translation'] = $translation; + $this->assignation['documentTr'] = $documentTr; - /** - * Versioning - */ - if ($this->isGranted('ROLE_ACCESS_VERSIONS')) { - if (null !== $response = $this->handleVersions($request, $documentTr)) { - return $response; + /** + * Versioning + */ + if ($this->isGranted('ROLE_ACCESS_VERSIONS')) { + if (null !== $response = $this->handleVersions($request, $documentTr)) { + return $response; + } } - } - /* - * Handle main form - */ - $form = $this->createForm(DocumentTranslationType::class, $documentTr, [ - 'referer' => $this->getRequest()->get('referer'), - 'disabled' => $this->isReadOnly, - ]); - $form->handleRequest($request); + /* + * Handle main form + */ + $form = $this->createForm(DocumentTranslationType::class, $documentTr, [ + 'referer' => $this->getRequest()->get('referer'), + 'disabled' => $this->isReadOnly, + ]); + $form->handleRequest($request); - if ($form->isSubmitted() && $form->isValid()) { - $this->onPostUpdate($documentTr, $request); + if ($form->isSubmitted() && $form->isValid()) { + $this->onPostUpdate($documentTr, $request); - $routeParams = [ - 'documentId' => $document->getId(), - 'translationId' => $translationId, - ]; + $routeParams = [ + 'documentId' => $document->getId(), + 'translationId' => $translationId, + ]; - if ($form->get('referer')->getData()) { - $routeParams = array_merge($routeParams, [ - 'referer' => $form->get('referer')->getData() - ]); + if ($form->get('referer')->getData()) { + $routeParams = array_merge($routeParams, [ + 'referer' => $form->get('referer')->getData() + ]); + } + + /* + * Force redirect to avoid resending form when refreshing page + */ + return $this->redirectToRoute( + 'documentsMetaPage', + $routeParams + ); } - /* - * Force redirect to avoid resending form when refreshing page - */ - return $this->redirectToRoute( - 'documentsMetaPage', - $routeParams - ); - } + $this->assignation['form'] = $form->createView(); + $this->assignation['readOnly'] = $this->isReadOnly; - $this->assignation['form'] = $form->createView(); - $this->assignation['readOnly'] = $this->isReadOnly; + return $this->render('@RoadizRozier/document-translations/edit.html.twig', $this->assignation); + } - return $this->render('@RoadizRozier/document-translations/edit.html.twig', $this->assignation); + throw new ResourceNotFoundException(); } - protected function createDocumentTranslation( - Document $document, - TranslationInterface $translation - ): DocumentTranslation { + /** + * @param Document $document + * @param TranslationInterface $translation + * + * @return DocumentTranslation + */ + protected function createDocumentTranslation(Document $document, TranslationInterface $translation) + { $dt = new DocumentTranslation(); $dt->setDocument($document); $dt->setTranslation($translation); @@ -141,7 +147,7 @@ protected function createDocumentTranslation( * @return Response * @throws RuntimeError */ - public function deleteAction(Request $request, int $documentId, int $translationId): Response + public function deleteAction(Request $request, int $documentId, int $translationId) { $this->denyAccessUnlessGranted('ROLE_ACCESS_DOCUMENTS_DELETE'); @@ -173,13 +179,13 @@ public function deleteAction(Request $request, int $documentId, int $translation 'document.translation.%name%.deleted', ['%name%' => (string) $document] ); - $this->publishConfirmMessage($request, $msg, $document); + $this->publishConfirmMessage($request, $msg); } catch (Exception $e) { $msg = $this->getTranslator()->trans( 'document.translation.%name%.cannot_delete', ['%name%' => (string) $document] ); - $this->publishErrorMessage($request, $msg, $document); + $this->publishErrorMessage($request, $msg); } /* * Force redirect to avoid resending form when refreshing page @@ -198,7 +204,12 @@ public function deleteAction(Request $request, int $documentId, int $translation throw new ResourceNotFoundException(); } - private function buildDeleteForm(DocumentTranslation $doc): FormInterface + /** + * @param DocumentTranslation $doc + * + * @return \Symfony\Component\Form\FormInterface + */ + private function buildDeleteForm(DocumentTranslation $doc) { $defaults = [ 'documentTranslationId' => $doc->getId(), @@ -228,10 +239,15 @@ protected function onPostUpdate(PersistableInterface $entity, Request $request): $msg = $this->getTranslator()->trans('document.translation.%name%.updated', [ '%name%' => (string) $entity->getDocument(), ]); - $this->publishConfirmMessage($request, $msg, $entity); + $this->publishConfirmMessage($request, $msg); } } + /** + * @param PersistableInterface $entity + * + * @return Response + */ protected function getPostUpdateRedirection(PersistableInterface $entity): ?Response { if ( diff --git a/src/Controllers/Documents/DocumentsController.php b/src/Controllers/Documents/DocumentsController.php index df1f8403..1acffaed 100644 --- a/src/Controllers/Documents/DocumentsController.php +++ b/src/Controllers/Documents/DocumentsController.php @@ -7,6 +7,7 @@ use GuzzleHttp\Exception\RequestException; use League\Flysystem\FilesystemException; use League\Flysystem\FilesystemOperator; +use League\Flysystem\UnableToMoveFile; use Psr\Log\LoggerInterface; use RZ\Roadiz\Core\Handlers\HandlerFactoryInterface; use RZ\Roadiz\CoreBundle\Document\DocumentFactory; @@ -19,7 +20,6 @@ use RZ\Roadiz\CoreBundle\Entity\Translation; use RZ\Roadiz\CoreBundle\EntityHandler\DocumentHandler; use RZ\Roadiz\CoreBundle\Exception\EntityAlreadyExistsException; -use RZ\Roadiz\CoreBundle\ListManager\SessionListFilters; use RZ\Roadiz\Documents\Events\DocumentCreatedEvent; use RZ\Roadiz\Documents\Events\DocumentDeletedEvent; use RZ\Roadiz\Documents\Events\DocumentFileUpdatedEvent; @@ -48,7 +48,6 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; -use Symfony\Component\String\UnicodeString; use Symfony\Component\Validator\Constraints\File; use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\NotNull; @@ -56,10 +55,23 @@ use Themes\Rozier\Forms\DocumentEmbedType; use Themes\Rozier\Models\DocumentModel; use Themes\Rozier\RozierApp; +use Themes\Rozier\Utils\SessionListFilters; use Twig\Error\RuntimeError; class DocumentsController extends RozierApp { + private array $documentPlatforms; + private DocumentFactory $documentFactory; + private HandlerFactoryInterface $handlerFactory; + private LoggerInterface $logger; + private RandomImageFinder $randomImageFinder; + private RendererInterface $renderer; + private DocumentUrlGeneratorInterface $documentUrlGenerator; + private UrlGeneratorInterface $urlGenerator; + private FilesystemOperator $documentsStorage; + private ?string $googleServerId; + private ?string $soundcloudClientId; + protected array $thumbnailFormat = [ 'quality' => 50, 'fit' => '128x128', @@ -69,21 +81,34 @@ class DocumentsController extends RozierApp 'controls' => false, 'loading' => 'lazy', ]; + private EmbedFinderFactory $embedFinderFactory; public function __construct( - private readonly array $documentPlatforms, - private readonly FilesystemOperator $documentsStorage, - private readonly HandlerFactoryInterface $handlerFactory, - private readonly LoggerInterface $logger, - private readonly RandomImageFinder $randomImageFinder, - private readonly DocumentFactory $documentFactory, - private readonly RendererInterface $renderer, - private readonly DocumentUrlGeneratorInterface $documentUrlGenerator, - private readonly UrlGeneratorInterface $urlGenerator, - private readonly EmbedFinderFactory $embedFinderFactory, - private readonly ?string $googleServerId = null, - private readonly ?string $soundcloudClientId = null + array $documentPlatforms, + FilesystemOperator $documentsStorage, + HandlerFactoryInterface $handlerFactory, + LoggerInterface $logger, + RandomImageFinder $randomImageFinder, + DocumentFactory $documentFactory, + RendererInterface $renderer, + DocumentUrlGeneratorInterface $documentUrlGenerator, + UrlGeneratorInterface $urlGenerator, + EmbedFinderFactory $embedFinderFactory, + ?string $googleServerId = null, + ?string $soundcloudClientId = null ) { + $this->documentPlatforms = $documentPlatforms; + $this->handlerFactory = $handlerFactory; + $this->logger = $logger; + $this->randomImageFinder = $randomImageFinder; + $this->documentFactory = $documentFactory; + $this->renderer = $renderer; + $this->documentUrlGenerator = $documentUrlGenerator; + $this->urlGenerator = $urlGenerator; + $this->googleServerId = $googleServerId; + $this->soundcloudClientId = $soundcloudClientId; + $this->documentsStorage = $documentsStorage; + $this->embedFinderFactory = $embedFinderFactory; } /** @@ -116,16 +141,20 @@ public function indexAction(Request $request, ?int $folderId = null): Response $this->assignation['folder'] = $folder; } - $type = $request->query->get('type', null); - if (\is_string($type) && trim($type) !== '') { - $prefilters['mimeType'] = trim($type); - $this->assignation['mimeType'] = trim($type); + if ( + $request->query->has('type') && + $request->query->get('type', '') !== '' + ) { + $prefilters['mimeType'] = trim($request->query->get('type', '')); + $this->assignation['mimeType'] = trim($request->query->get('type', '')); } - $embedPlatform = $request->query->get('embedPlatform', null); - if (\is_string($embedPlatform) && trim($embedPlatform) !== '') { - $prefilters['embedPlatform'] = trim($embedPlatform); - $this->assignation['embedPlatform'] = trim($embedPlatform); + if ( + $request->query->has('embedPlatform') && + $request->query->get('embedPlatform', '') !== '' + ) { + $prefilters['embedPlatform'] = trim($request->query->get('embedPlatform', '')); + $this->assignation['embedPlatform'] = trim($request->query->get('embedPlatform', '')); } $this->assignation['availablePlatforms'] = $this->documentPlatforms; @@ -311,13 +340,13 @@ public function editAction(Request $request, int $documentId): Response $this->dispatchEvent( new DocumentFileUpdatedEvent($document) ); - $this->publishConfirmMessage($request, $msg, $document); + $this->publishConfirmMessage($request, $msg); } $msg = $this->getTranslator()->trans('document.%name%.updated', [ '%name%' => (string) $document, ]); - $this->publishConfirmMessage($request, $msg, $document); + $this->publishConfirmMessage($request, $msg); $this->em()->flush(); // Event must be dispatched AFTER flush for async concurrency matters $this->dispatchEvent( @@ -390,13 +419,13 @@ public function deleteAction(Request $request, int $documentId): Response $msg = $this->getTranslator()->trans('document.%name%.deleted', [ '%name%' => (string) $document ]); - $this->publishConfirmMessage($request, $msg, $document); + $this->publishConfirmMessage($request, $msg); } catch (\Exception $e) { $msg = $this->getTranslator()->trans('document.%name%.cannot_delete', [ '%name%' => (string) $document ]); $this->logger->error($e->getMessage()); - $this->publishErrorMessage($request, $msg, $document); + $this->publishErrorMessage($request, $msg); } /* * Force redirect to avoid resending form when refreshing page @@ -448,7 +477,7 @@ public function bulkDeleteAction(Request $request): Response 'document.%name%.deleted', ['%name%' => (string) $document] ); - $this->publishConfirmMessage($request, $msg, $document); + $this->publishConfirmMessage($request, $msg); } $this->em()->flush(); @@ -494,18 +523,18 @@ public function embedAction(Request $request, ?int $folderId = null): Response if (is_iterable($document)) { foreach ($document as $singleDocument) { $msg = $this->getTranslator()->trans('document.%name%.uploaded', [ - '%name%' => (new UnicodeString((string) $singleDocument))->truncate(50, '...')->toString(), + '%name%' => (string) $singleDocument, ]); - $this->publishConfirmMessage($request, $msg, $singleDocument); + $this->publishConfirmMessage($request, $msg); $this->dispatchEvent( new DocumentCreatedEvent($singleDocument) ); } } else { $msg = $this->getTranslator()->trans('document.%name%.uploaded', [ - '%name%' => (new UnicodeString((string) $document))->truncate(50, '...')->toString(), + '%name%' => (string) $document, ]); - $this->publishConfirmMessage($request, $msg, $document); + $this->publishConfirmMessage($request, $msg); $this->dispatchEvent( new DocumentCreatedEvent($document) ); @@ -553,9 +582,9 @@ public function randomAction(Request $request, ?int $folderId = null): Response $document = $this->randomDocument($folderId); $msg = $this->getTranslator()->trans('document.%name%.uploaded', [ - '%name%' => (new UnicodeString((string) $document))->truncate(50, '...')->toString(), + '%name%' => (string) $document, ]); - $this->publishConfirmMessage($request, $msg, $document); + $this->publishConfirmMessage($request, $msg); $this->dispatchEvent( new DocumentCreatedEvent($document) @@ -597,31 +626,6 @@ public function downloadAction(Request $request, int $documentId): Response throw new ResourceNotFoundException(); } - /** - * Download document file inline. - * - * @param Request $request - * @param int $documentId - * @return Response - * @throws FilesystemException - */ - public function downloadInlineAction(Request $request, int $documentId): Response - { - $this->denyAccessUnlessGranted('ROLE_ACCESS_DOCUMENTS'); - - /** @var Document|null $document */ - $document = $this->em()->find(Document::class, $documentId); - - if ($document !== null) { - /** @var DocumentHandler $handler */ - $handler = $this->handlerFactory->getHandler($document); - - return $handler->getDownloadResponse(false); - } - - throw new ResourceNotFoundException(); - } - /** * @param Request $request * @param int|null $folderId @@ -651,9 +655,9 @@ public function uploadAction(Request $request, ?int $folderId = null, string $_f if (null !== $document) { $msg = $this->getTranslator()->trans('document.%name%.uploaded', [ - '%name%' => (new UnicodeString((string) $document))->truncate(50, '...')->toString(), + '%name%' => (string) $document, ]); - $this->publishConfirmMessage($request, $msg, $document); + $this->publishConfirmMessage($request, $msg); // Event must be dispatched AFTER flush for async concurrency matters $this->dispatchEvent( @@ -671,13 +675,13 @@ public function uploadAction(Request $request, ?int $folderId = null, string $_f return new JsonResponse([ 'success' => true, 'document' => $documentModel->toArray(), - ], Response::HTTP_CREATED); + ], JsonResponse::HTTP_CREATED); } else { return $this->redirectToRoute('documentsHomePage', ['folderId' => $folderId]); } } else { $msg = $this->getTranslator()->trans('document.cannot_persist'); - $this->publishErrorMessage($request, $msg, $document); + $this->publishErrorMessage($request, $msg); if ($_format === 'json' || $request->isXmlHttpRequest()) { throw $this->createNotFoundException($msg); @@ -693,7 +697,6 @@ public function uploadAction(Request $request, ?int $folderId = null, string $_f /** @var Form $child */ foreach ($form as $child) { if ($child->isSubmitted() && !$child->isValid()) { - /** @var FormError $error */ foreach ($child->getErrors() as $error) { $errorPerForm[$child->getName()][] = $this->getTranslator()->trans($error->getMessage()); } diff --git a/src/Controllers/FoldersController.php b/src/Controllers/FoldersController.php index c3e866b5..44f671d1 100644 --- a/src/Controllers/FoldersController.php +++ b/src/Controllers/FoldersController.php @@ -25,8 +25,11 @@ class FoldersController extends RozierApp { - public function __construct(private readonly DocumentArchiver $documentArchiver) + private DocumentArchiver $documentArchiver; + + public function __construct(DocumentArchiver $documentArchiver) { + $this->documentArchiver = $documentArchiver; } public function indexAction(Request $request): Response @@ -83,7 +86,7 @@ public function addAction(Request $request, ?int $parentFolderId = null): Respon 'folder.%name%.created', ['%name%' => $folder->getFolderName()] ); - $this->publishConfirmMessage($request, $msg, $folder); + $this->publishConfirmMessage($request, $msg); /* * Dispatch event @@ -92,7 +95,7 @@ public function addAction(Request $request, ?int $parentFolderId = null): Respon new FolderCreatedEvent($folder) ); } catch (\RuntimeException $e) { - $this->publishErrorMessage($request, $e->getMessage(), $folder); + $this->publishErrorMessage($request, $e->getMessage()); } return $this->redirectToRoute('foldersHomePage'); @@ -134,7 +137,7 @@ public function deleteAction(Request $request, int $folderId): Response 'folder.%name%.deleted', ['%name%' => $folder->getFolderName()] ); - $this->publishConfirmMessage($request, $msg, $folder); + $this->publishConfirmMessage($request, $msg); /* * Dispatch event @@ -143,7 +146,7 @@ public function deleteAction(Request $request, int $folderId): Response new FolderDeletedEvent($folder) ); } catch (\RuntimeException $e) { - $this->publishErrorMessage($request, $e->getMessage(), $folder); + $this->publishErrorMessage($request, $e->getMessage()); } return $this->redirectToRoute('foldersHomePage'); @@ -190,7 +193,7 @@ public function editAction(Request $request, int $folderId): Response 'folder.%name%.updated', ['%name%' => $folder->getFolderName()] ); - $this->publishConfirmMessage($request, $msg, $folder); + $this->publishConfirmMessage($request, $msg); /* * Dispatch event */ @@ -198,7 +201,7 @@ public function editAction(Request $request, int $folderId): Response new FolderUpdatedEvent($folder) ); } catch (\RuntimeException $e) { - $this->publishErrorMessage($request, $e->getMessage(), $folder); + $this->publishErrorMessage($request, $e->getMessage()); } return $this->redirectToRoute('foldersEditPage', ['folderId' => $folderId]); @@ -274,7 +277,7 @@ public function editTranslationAction(Request $request, int $folderId, int $tran 'folder.%name%.updated', ['%name%' => $folder->getFolderName()] ); - $this->publishConfirmMessage($request, $msg, $folder); + $this->publishConfirmMessage($request, $msg); /* * Dispatch event */ @@ -282,7 +285,7 @@ public function editTranslationAction(Request $request, int $folderId, int $tran new FolderUpdatedEvent($folder) ); } catch (\RuntimeException $e) { - $this->publishErrorMessage($request, $e->getMessage(), $folder); + $this->publishErrorMessage($request, $e->getMessage()); } return $this->redirectToRoute('foldersEditTranslationPage', [ diff --git a/src/Controllers/GroupsController.php b/src/Controllers/GroupsController.php index c4b9e949..61cbc768 100644 --- a/src/Controllers/GroupsController.php +++ b/src/Controllers/GroupsController.php @@ -20,6 +20,9 @@ use Themes\Rozier\Forms\GroupType; use Twig\Error\RuntimeError; +/** + * @package Themes\Rozier\Controllers + */ class GroupsController extends AbstractAdminController { /** @@ -147,7 +150,7 @@ public function editRolesAction(Request $request, int $id): Response '%group%' => $item->getName(), '%role%' => $role->getRole(), ]); - $this->publishConfirmMessage($request, $msg, $role); + $this->publishConfirmMessage($request, $msg); return $this->redirectToRoute( 'groupsEditRolesPage', @@ -203,7 +206,7 @@ public function removeRolesAction(Request $request, int $id, int $roleId): Respo '%role%' => $role->getRole(), '%group%' => $item->getName(), ]); - $this->publishConfirmMessage($request, $msg, $role); + $this->publishConfirmMessage($request, $msg); return $this->redirectToRoute( 'groupsEditRolesPage', @@ -251,7 +254,7 @@ public function editUsersAction(Request $request, int $id): Response '%group%' => $item->getName(), '%user%' => $user->getUserName(), ]); - $this->publishConfirmMessage($request, $msg, $user); + $this->publishConfirmMessage($request, $msg); return $this->redirectToRoute( 'groupsEditUsersPage', @@ -306,7 +309,7 @@ public function removeUsersAction(Request $request, int $id, int $userId): Respo '%user%' => $user->getUserName(), '%group%' => $item->getName(), ]); - $this->publishConfirmMessage($request, $msg, $user); + $this->publishConfirmMessage($request, $msg); return $this->redirectToRoute( 'groupsEditUsersPage', diff --git a/src/Controllers/GroupsUtilsController.php b/src/Controllers/GroupsUtilsController.php index 25e6be3e..f15b6499 100644 --- a/src/Controllers/GroupsUtilsController.php +++ b/src/Controllers/GroupsUtilsController.php @@ -20,10 +20,17 @@ class GroupsUtilsController extends RozierApp { - public function __construct( - private readonly SerializerInterface $serializer, - private readonly GroupsImporter $groupsImporter - ) { + private SerializerInterface $serializer; + private GroupsImporter $groupsImporter; + + /** + * @param SerializerInterface $serializer + * @param GroupsImporter $groupsImporter + */ + public function __construct(SerializerInterface $serializer, GroupsImporter $groupsImporter) + { + $this->serializer = $serializer; + $this->groupsImporter = $groupsImporter; } /** @@ -113,9 +120,6 @@ public function importJsonFileAction(Request $request): Response if ($file->isValid()) { $serializedData = file_get_contents($file->getPathname()); - if (false === $serializedData) { - throw new RuntimeError('Cannot read uploaded file.'); - } if (null !== \json_decode($serializedData)) { $this->groupsImporter->import($serializedData); diff --git a/src/Controllers/HistoryController.php b/src/Controllers/HistoryController.php index acc480ca..56e7a727 100644 --- a/src/Controllers/HistoryController.php +++ b/src/Controllers/HistoryController.php @@ -4,12 +4,12 @@ namespace Themes\Rozier\Controllers; -use Monolog\Logger; +use RZ\Roadiz\CoreBundle\Entity\Log; use RZ\Roadiz\CoreBundle\Entity\User; -use RZ\Roadiz\CoreBundle\Logger\Entity\Log; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Exception\ResourceNotFoundException; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Themes\Rozier\RozierApp; use Twig\Error\RuntimeError; @@ -19,14 +19,14 @@ class HistoryController extends RozierApp { public static array $levelToHuman = [ - Logger::EMERGENCY => "emergency", - Logger::CRITICAL => "critical", - Logger::ALERT => "alert", - Logger::ERROR => "error", - Logger::WARNING => "warning", - Logger::NOTICE => "notice", - Logger::INFO => "info", - Logger::DEBUG => "debug", + Log::EMERGENCY => "emergency", + Log::CRITICAL => "critical", + Log::ALERT => "alert", + Log::ERROR => "error", + Log::WARNING => "warning", + Log::NOTICE => "notice", + Log::INFO => "info", + Log::DEBUG => "debug", ]; /** @@ -64,20 +64,20 @@ public function indexAction(Request $request): Response * List user logs action. * * @param Request $request - * @param int|string $userId + * @param int $userId * * @return Response * @throws \Doctrine\ORM\ORMException * @throws \Doctrine\ORM\OptimisticLockException * @throws \Doctrine\ORM\TransactionRequiredException */ - public function userAction(Request $request, int|string $userId): Response + public function userAction(Request $request, int $userId): Response { $this->denyAccessUnlessGranted(['ROLE_BACKEND_USER', 'ROLE_ACCESS_LOGS']); if ( !($this->isGranted(['ROLE_ACCESS_USERS', 'ROLE_ACCESS_LOGS']) - || ($this->getUser() instanceof User && $this->getUser()->getId() === $userId)) + || ($this->getUser() instanceof User && $this->getUser()->getId() == $userId)) ) { throw $this->createAccessDeniedException("You don't have access to this page: ROLE_ACCESS_USERS"); } @@ -94,7 +94,7 @@ public function userAction(Request $request, int|string $userId): Response */ $listManager = $this->createEntityListManager( Log::class, - ['userId' => $user->getId()], + ['user' => $user], ['datetime' => 'DESC'] ); $listManager->setDisplayingNotPublishedNodes(true); diff --git a/src/Controllers/LoginController.php b/src/Controllers/LoginController.php index 7dea01cb..fe26ca9c 100644 --- a/src/Controllers/LoginController.php +++ b/src/Controllers/LoginController.php @@ -16,9 +16,9 @@ class LoginController extends RozierApp { public function __construct( - private readonly DocumentUrlGeneratorInterface $documentUrlGenerator, - private readonly RandomImageFinder $randomImageFinder, - private readonly Settings $settingsBag + private DocumentUrlGeneratorInterface $documentUrlGenerator, + private RandomImageFinder $randomImageFinder, + private Settings $settingsBag ) { } @@ -41,9 +41,10 @@ public function imageAction(Request $request): Response 'quality' => 80, 'sharpen' => 5, ]); - return $response->setData([ + $response->setData([ 'url' => $this->documentUrlGenerator->getUrl() ]); + return $response; } } @@ -53,8 +54,9 @@ public function imageAction(Request $request): Response if (null !== $feed) { $url = $feed['url'] ?? $feed['urls']['regular'] ?? $feed['urls']['full'] ?? $feed['urls']['raw'] ?? null; } - return $response->setData([ - 'url' => $url ?? '/themes/Rozier/static/assets/img/default_login.jpg' + $response->setData([ + 'url' => '/themes/Rozier/static/assets/img/default_login.jpg' ]); + return $response; } } diff --git a/src/Controllers/NodeTypeFieldsController.php b/src/Controllers/NodeTypeFieldsController.php index 4bdcd44b..5596ab58 100644 --- a/src/Controllers/NodeTypeFieldsController.php +++ b/src/Controllers/NodeTypeFieldsController.php @@ -21,10 +21,11 @@ class NodeTypeFieldsController extends RozierApp { - public function __construct( - private readonly bool $allowNodeTypeEdition, - private readonly MessageBusInterface $messageBus - ) { + private MessageBusInterface $messageBus; + + public function __construct(MessageBusInterface $messageBus) + { + $this->messageBus = $messageBus; } /** @@ -78,25 +79,21 @@ public function editAction(Request $request, int $nodeTypeFieldId): Response $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - if (!$this->allowNodeTypeEdition) { - $form->addError(new FormError('You cannot edit node-type fields in production.')); - } else { - $this->em()->flush(); + $this->em()->flush(); - /** @var NodeType $nodeType */ - $nodeType = $field->getNodeType(); - $this->messageBus->dispatch(new Envelope(new UpdateNodeTypeSchemaMessage($nodeType->getId()))); + /** @var NodeType $nodeType */ + $nodeType = $field->getNodeType(); + $this->messageBus->dispatch(new Envelope(new UpdateNodeTypeSchemaMessage($nodeType->getId()))); - $msg = $this->getTranslator()->trans('nodeTypeField.%name%.updated', ['%name%' => $field->getName()]); - $this->publishConfirmMessage($request, $msg, $field); + $msg = $this->getTranslator()->trans('nodeTypeField.%name%.updated', ['%name%' => $field->getName()]); + $this->publishConfirmMessage($request, $msg); - return $this->redirectToRoute( - 'nodeTypeFieldsEditPage', - [ - 'nodeTypeFieldId' => $nodeTypeFieldId, - ] - ); - } + return $this->redirectToRoute( + 'nodeTypeFieldsEditPage', + [ + 'nodeTypeFieldId' => $nodeTypeFieldId, + ] + ); } $this->assignation['form'] = $form->createView(); @@ -133,37 +130,31 @@ public function addAction(Request $request, int $nodeTypeId): Response $this->assignation['nodeType'] = $nodeType; $this->assignation['field'] = $field; - $form = $this->createForm(NodeTypeFieldType::class, $field, [ - 'disabled' => !$this->allowNodeTypeEdition - ]); + $form = $this->createForm(NodeTypeFieldType::class, $field); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - if (!$this->allowNodeTypeEdition) { - $form->addError(new FormError('You cannot add node-type fields in production.')); - } else { - try { - $this->em()->persist($field); - $this->em()->flush(); - $this->em()->refresh($nodeType); - - $this->messageBus->dispatch(new Envelope(new UpdateNodeTypeSchemaMessage($nodeType->getId()))); - - $msg = $this->getTranslator()->trans( - 'nodeTypeField.%name%.created', - ['%name%' => $field->getName()] - ); - $this->publishConfirmMessage($request, $msg, $field); - - return $this->redirectToRoute( - 'nodeTypeFieldsListPage', - [ - 'nodeTypeId' => $nodeTypeId, - ] - ); - } catch (Exception $e) { - $form->addError(new FormError($e->getMessage())); - } + try { + $this->em()->persist($field); + $this->em()->flush(); + $this->em()->refresh($nodeType); + + $this->messageBus->dispatch(new Envelope(new UpdateNodeTypeSchemaMessage($nodeType->getId()))); + + $msg = $this->getTranslator()->trans( + 'nodeTypeField.%name%.created', + ['%name%' => $field->getName()] + ); + $this->publishConfirmMessage($request, $msg); + + return $this->redirectToRoute( + 'nodeTypeFieldsListPage', + [ + 'nodeTypeId' => $nodeTypeId, + ] + ); + } catch (Exception $e) { + $form->addError(new FormError($e->getMessage())); } } @@ -194,30 +185,26 @@ public function deleteAction(Request $request, int $nodeTypeFieldId): Response $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - if (!$this->allowNodeTypeEdition) { - $form->addError(new FormError('You cannot delete node-type fields in production.')); - } else { - /** @var NodeType $nodeType */ - $nodeType = $field->getNodeType(); - $nodeTypeId = $nodeType->getId(); - $this->em()->remove($field); - $this->em()->flush(); - - $this->messageBus->dispatch(new Envelope(new UpdateNodeTypeSchemaMessage($nodeTypeId))); - - $msg = $this->getTranslator()->trans( - 'nodeTypeField.%name%.deleted', - ['%name%' => $field->getName()] - ); - $this->publishConfirmMessage($request, $msg, $field); - - return $this->redirectToRoute( - 'nodeTypeFieldsListPage', - [ - 'nodeTypeId' => $nodeTypeId, - ] - ); - } + /** @var NodeType $nodeType */ + $nodeType = $field->getNodeType(); + $nodeTypeId = $nodeType->getId(); + $this->em()->remove($field); + $this->em()->flush(); + + $this->messageBus->dispatch(new Envelope(new UpdateNodeTypeSchemaMessage($nodeTypeId))); + + $msg = $this->getTranslator()->trans( + 'nodeTypeField.%name%.deleted', + ['%name%' => $field->getName()] + ); + $this->publishConfirmMessage($request, $msg); + + return $this->redirectToRoute( + 'nodeTypeFieldsListPage', + [ + 'nodeTypeId' => $nodeTypeId, + ] + ); } $this->assignation['field'] = $field; diff --git a/src/Controllers/NodeTypes/NodeTypesController.php b/src/Controllers/NodeTypes/NodeTypesController.php index 58720914..c4de8822 100644 --- a/src/Controllers/NodeTypes/NodeTypesController.php +++ b/src/Controllers/NodeTypes/NodeTypesController.php @@ -6,7 +6,6 @@ use RZ\Roadiz\CoreBundle\Entity\NodeType; use RZ\Roadiz\CoreBundle\Exception\EntityAlreadyExistsException; -use RZ\Roadiz\CoreBundle\ListManager\SessionListFilters; use RZ\Roadiz\CoreBundle\Message\DeleteNodeTypeMessage; use RZ\Roadiz\CoreBundle\Message\UpdateNodeTypeSchemaMessage; use Symfony\Component\Form\Extension\Core\Type\FormType; @@ -17,14 +16,18 @@ use Symfony\Component\Messenger\MessageBusInterface; use Themes\Rozier\Forms\NodeTypeType; use Themes\Rozier\RozierApp; -use Twig\Error\RuntimeError; +use Themes\Rozier\Utils\SessionListFilters; +/** + * @package Themes\Rozier\Controllers\NodeTypes + */ class NodeTypesController extends RozierApp { - public function __construct( - private readonly bool $allowNodeTypeEdition, - private readonly MessageBusInterface $messageBus - ) { + private MessageBusInterface $messageBus; + + public function __construct(MessageBusInterface $messageBus) + { + $this->messageBus = $messageBus; } public function indexAction(Request $request): Response @@ -55,10 +58,12 @@ public function indexAction(Request $request): Response } /** + * Return an edition form for requested node-type. + * * @param Request $request - * @param int $nodeTypeId + * @param int $nodeTypeId + * * @return Response - * @throws RuntimeError */ public function editAction(Request $request, int $nodeTypeId): Response { @@ -77,10 +82,11 @@ public function editAction(Request $request, int $nodeTypeId): Response if ($form->isSubmitted() && $form->isValid()) { try { $this->em()->flush(); + $this->messageBus->dispatch(new Envelope(new UpdateNodeTypeSchemaMessage($nodeType->getId()))); $msg = $this->getTranslator()->trans('nodeType.%name%.updated', ['%name%' => $nodeType->getName()]); - $this->publishConfirmMessage($request, $msg, $nodeType); + $this->publishConfirmMessage($request, $msg); return $this->redirectToRoute('nodeTypesEditPage', [ 'nodeTypeId' => $nodeTypeId @@ -97,40 +103,35 @@ public function editAction(Request $request, int $nodeTypeId): Response } /** + * Return an creation form for requested node-type. + * * @param Request $request * * @return Response - * @throws RuntimeError */ public function addAction(Request $request): Response { $this->denyAccessUnlessGranted('ROLE_ACCESS_NODETYPES'); $nodeType = new NodeType(); - $form = $this->createForm(NodeTypeType::class, $nodeType, [ - 'disabled' => !$this->allowNodeTypeEdition - ]); + $form = $this->createForm(NodeTypeType::class, $nodeType); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - if (!$this->allowNodeTypeEdition) { - $form->addError(new FormError('You cannot create a node-type in production mode.')); - } else { - try { - $this->em()->persist($nodeType); - $this->em()->flush(); - - $this->messageBus->dispatch(new Envelope(new UpdateNodeTypeSchemaMessage($nodeType->getId()))); - - $msg = $this->getTranslator()->trans('nodeType.%name%.created', ['%name%' => $nodeType->getName()]); - $this->publishConfirmMessage($request, $msg, $nodeType); - - return $this->redirectToRoute('nodeTypesEditPage', [ - 'nodeTypeId' => $nodeType->getId() - ]); - } catch (EntityAlreadyExistsException $e) { - $form->addError(new FormError($e->getMessage())); - } + try { + $this->em()->persist($nodeType); + $this->em()->flush(); + + $this->messageBus->dispatch(new Envelope(new UpdateNodeTypeSchemaMessage($nodeType->getId()))); + + $msg = $this->getTranslator()->trans('nodeType.%name%.created', ['%name%' => $nodeType->getName()]); + $this->publishConfirmMessage($request, $msg); + + return $this->redirectToRoute('nodeTypesEditPage', [ + 'nodeTypeId' => $nodeType->getId() + ]); + } catch (EntityAlreadyExistsException $e) { + $form->addError(new FormError($e->getMessage())); } } @@ -142,10 +143,9 @@ public function addAction(Request $request): Response /** * @param Request $request - * @param int $nodeTypeId + * @param int $nodeTypeId * * @return Response - * @throws RuntimeError */ public function deleteAction(Request $request, int $nodeTypeId): Response { @@ -162,16 +162,12 @@ public function deleteAction(Request $request, int $nodeTypeId): Response $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - if (!$this->allowNodeTypeEdition) { - $form->addError(new FormError('You cannot delete a node-type in production mode.')); - } else { - $this->messageBus->dispatch(new Envelope(new DeleteNodeTypeMessage($nodeType->getId()))); + $this->messageBus->dispatch(new Envelope(new DeleteNodeTypeMessage($nodeType->getId()))); - $msg = $this->getTranslator()->trans('nodeType.%name%.deleted', ['%name%' => $nodeType->getName()]); - $this->publishConfirmMessage($request, $msg, $nodeType); + $msg = $this->getTranslator()->trans('nodeType.%name%.deleted', ['%name%' => $nodeType->getName()]); + $this->publishConfirmMessage($request, $msg); - return $this->redirectToRoute('nodeTypesHomePage'); - } + return $this->redirectToRoute('nodeTypesHomePage'); } $this->assignation['form'] = $form->createView(); diff --git a/src/Controllers/NodeTypes/NodeTypesUtilsController.php b/src/Controllers/NodeTypes/NodeTypesUtilsController.php index 059a6b13..f0e656ca 100644 --- a/src/Controllers/NodeTypes/NodeTypesUtilsController.php +++ b/src/Controllers/NodeTypes/NodeTypesUtilsController.php @@ -29,12 +29,21 @@ class NodeTypesUtilsController extends RozierApp { + private SerializerInterface $serializer; + private NodeTypes $nodeTypesBag; + private NodeTypesImporter $nodeTypesImporter; + private MessageBusInterface $messageBus; + public function __construct( - private readonly SerializerInterface $serializer, - private readonly NodeTypes $nodeTypesBag, - private readonly NodeTypesImporter $nodeTypesImporter, - private readonly MessageBusInterface $messageBus + SerializerInterface $serializer, + NodeTypes $nodeTypesBag, + NodeTypesImporter $nodeTypesImporter, + MessageBusInterface $messageBus ) { + $this->serializer = $serializer; + $this->nodeTypesBag = $nodeTypesBag; + $this->nodeTypesImporter = $nodeTypesImporter; + $this->messageBus = $messageBus; } /** @@ -43,9 +52,9 @@ public function __construct( * @param Request $request * @param int $nodeTypeId * - * @return JsonResponse + * @return Response */ - public function exportJsonFileAction(Request $request, int $nodeTypeId): JsonResponse + public function exportJsonFileAction(Request $request, int $nodeTypeId): Response { $this->denyAccessUnlessGranted('ROLE_ACCESS_NODETYPES'); @@ -62,7 +71,7 @@ public function exportJsonFileAction(Request $request, int $nodeTypeId): JsonRes 'json', SerializationContext::create()->setGroups(['node_type', 'position']) ), - Response::HTTP_OK, + JsonResponse::HTTP_OK, [ 'Content-Disposition' => sprintf('attachment; filename="%s"', $nodeType->getName() . '.json'), ], @@ -72,8 +81,8 @@ public function exportJsonFileAction(Request $request, int $nodeTypeId): JsonRes /** * @param Request $request + * * @return BinaryFileResponse - * @throws RuntimeError */ public function exportDocumentationAction(Request $request): BinaryFileResponse { @@ -82,10 +91,6 @@ public function exportDocumentationAction(Request $request): BinaryFileResponse $documentationGenerator = new DocumentationGenerator($this->nodeTypesBag, $this->getTranslator()); $tmpfname = tempnam(sys_get_temp_dir(), date('Y-m-d-H-i-s') . '.zip'); - if (false === $tmpfname) { - throw new RuntimeError('Unable to create temporary file.'); - } - unlink($tmpfname); // Deprecated: ZipArchive::open(): Using empty file as ZipArchive is deprecated $zipArchive = new ZipArchive(); $zipArchive->open($tmpfname, ZipArchive::CREATE); @@ -142,6 +147,10 @@ public function exportTypeScriptDeclarationAction(Request $request): Response return $response; } + /** + * @param Request $request + * @return BinaryFileResponse + */ public function exportAllAction(Request $request): BinaryFileResponse { $this->denyAccessUnlessGranted('ROLE_ACCESS_NODETYPES'); @@ -152,9 +161,6 @@ public function exportAllAction(Request $request): BinaryFileResponse $zipArchive = new ZipArchive(); $tmpfname = tempnam(sys_get_temp_dir(), date('Y-m-d-H-i-s') . '.zip'); - if (false === $tmpfname) { - throw new RuntimeError('Unable to create temporary file.'); - } unlink($tmpfname); // Deprecated: ZipArchive::open(): Using empty file as ZipArchive is deprecated $zipArchive->open($tmpfname, ZipArchive::CREATE); @@ -206,9 +212,6 @@ public function importJsonFileAction(Request $request): Response if ($file->isValid()) { $serializedData = file_get_contents($file->getPathname()); - if (false === $serializedData) { - throw new RuntimeError('Unable to read uploaded file.'); - } if (null !== json_decode($serializedData)) { $this->nodeTypesImporter->import($serializedData); @@ -235,7 +238,7 @@ public function importJsonFileAction(Request $request): Response /** * @return FormInterface */ - private function buildImportJsonFileForm(): FormInterface + private function buildImportJsonFileForm() { $builder = $this->createFormBuilder() ->add('node_type_file', FileType::class, [ diff --git a/src/Controllers/Nodes/ExportController.php b/src/Controllers/Nodes/ExportController.php index abc4d1f1..61be7980 100644 --- a/src/Controllers/Nodes/ExportController.php +++ b/src/Controllers/Nodes/ExportController.php @@ -4,11 +4,9 @@ namespace Themes\Rozier\Controllers\Nodes; -use PhpOffice\PhpSpreadsheet\Writer\Exception; use RZ\Roadiz\CoreBundle\Entity\Node; use RZ\Roadiz\CoreBundle\Entity\NodesSources; use RZ\Roadiz\CoreBundle\Entity\Translation; -use RZ\Roadiz\CoreBundle\Security\Authorization\Voter\NodeVoter; use RZ\Roadiz\CoreBundle\Xlsx\NodeSourceXlsxSerializer; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -17,8 +15,14 @@ class ExportController extends RozierApp { - public function __construct(private readonly NodeSourceXlsxSerializer $xlsxSerializer) + private NodeSourceXlsxSerializer $xlsxSerializer; + + /** + * @param NodeSourceXlsxSerializer $xlsxSerializer + */ + public function __construct(NodeSourceXlsxSerializer $xlsxSerializer) { + $this->xlsxSerializer = $xlsxSerializer; } /** @@ -30,10 +34,15 @@ public function __construct(private readonly NodeSourceXlsxSerializer $xlsxSeria * * @return Response * @throws \PhpOffice\PhpSpreadsheet\Exception - * @throws Exception + * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception */ public function exportAllXlsxAction(Request $request, int $translationId, ?int $parentNodeId = null): Response { + $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES'); + + /* + * Get translation + */ $translation = $this->em() ->find(Translation::class, $translationId); @@ -52,11 +61,8 @@ public function exportAllXlsxAction(Request $request, int $translationId, ?int $ if (null === $parentNode) { throw $this->createNotFoundException(); } - $this->denyAccessUnlessGranted(NodeVoter::READ, $parentNode); $criteria['node.parent'] = $parentNode; $filename = $parentNode->getNodeName() . '-' . date("YmdHis") . '.' . $translation->getLocale() . '.xlsx'; - } else { - $this->denyAccessUnlessGranted(NodeVoter::READ_AT_ROOT); } $sources = $this->em() @@ -66,7 +72,7 @@ public function exportAllXlsxAction(Request $request, int $translationId, ?int $ ->findBy($criteria, $order); $this->xlsxSerializer->setOnlyTexts(true); - $this->xlsxSerializer->addUrls(); + $this->xlsxSerializer->addUrls($request, $this->getSettingsBag()->get('force_locale')); $xlsx = $this->xlsxSerializer->serialize($sources); $response = new Response( diff --git a/src/Controllers/Nodes/HistoryController.php b/src/Controllers/Nodes/HistoryController.php index 6915d67e..54ef618d 100644 --- a/src/Controllers/Nodes/HistoryController.php +++ b/src/Controllers/Nodes/HistoryController.php @@ -4,19 +4,19 @@ namespace Themes\Rozier\Controllers\Nodes; -use Doctrine\ORM\QueryBuilder; +use RZ\Roadiz\CoreBundle\Entity\Log; use RZ\Roadiz\CoreBundle\Entity\Node; use RZ\Roadiz\CoreBundle\Entity\Translation; -use RZ\Roadiz\CoreBundle\ListManager\QueryBuilderListManager; -use RZ\Roadiz\CoreBundle\ListManager\SessionListFilters; -use RZ\Roadiz\CoreBundle\Logger\Entity\Log; -use RZ\Roadiz\CoreBundle\Security\Authorization\Voter\NodeVoter; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Themes\Rozier\RozierApp; +use Themes\Rozier\Utils\SessionListFilters; use Twig\Error\RuntimeError; +/** + * @package Themes\Rozier\Controllers\Nodes + */ class HistoryController extends RozierApp { /** @@ -27,26 +27,21 @@ class HistoryController extends RozierApp */ public function historyAction(Request $request, int $nodeId): Response { + $this->denyAccessUnlessGranted(['ROLE_ACCESS_NODES', 'ROLE_ACCESS_LOGS']); /** @var Node|null $node */ $node = $this->em()->find(Node::class, $nodeId); if (null === $node) { throw new ResourceNotFoundException(); } - $this->denyAccessUnlessGranted(NodeVoter::READ_LOGS, $node); - $qb = $this->em() - ->getRepository(Log::class) - ->getAllRelatedToNodeQueryBuilder($node); - - $listManager = new QueryBuilderListManager($request, $qb, 'obj'); - $listManager->setSearchingCallable(function (QueryBuilder $queryBuilder, string $search) { - $queryBuilder->andWhere($queryBuilder->expr()->orX( - $queryBuilder->expr()->like('obj.message', ':search'), - $queryBuilder->expr()->like('obj.channel', ':search') - )); - $queryBuilder->setParameter('search', '%' . $search . '%'); - }); + $listManager = $this->createEntityListManager( + Log::class, + [ + 'nodeSource' => $node->getNodeSources()->toArray(), + ], + ['datetime' => 'DESC'] + ); $listManager->setDisplayingNotPublishedNodes(true); $listManager->setDisplayingAllNodesStatuses(true); /* diff --git a/src/Controllers/Nodes/NodesAttributesController.php b/src/Controllers/Nodes/NodesAttributesController.php index f1dfe96e..f11dad81 100644 --- a/src/Controllers/Nodes/NodesAttributesController.php +++ b/src/Controllers/Nodes/NodesAttributesController.php @@ -9,13 +9,10 @@ use RZ\Roadiz\CoreBundle\Entity\AttributeValueTranslation; use RZ\Roadiz\CoreBundle\Entity\Node; use RZ\Roadiz\CoreBundle\Entity\NodesSources; -use RZ\Roadiz\CoreBundle\Entity\NodeType; use RZ\Roadiz\CoreBundle\Entity\Translation; use RZ\Roadiz\CoreBundle\Event\NodesSources\NodesSourcesUpdatedEvent; use RZ\Roadiz\CoreBundle\Form\AttributeValueTranslationType; use RZ\Roadiz\CoreBundle\Form\AttributeValueType; -use RZ\Roadiz\CoreBundle\Form\Error\FormErrorSerializer; -use RZ\Roadiz\CoreBundle\Security\Authorization\Voter\NodeVoter; use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\HttpFoundation\JsonResponse; @@ -27,10 +24,14 @@ class NodesAttributesController extends RozierApp { - public function __construct( - private readonly FormFactoryInterface $formFactory, - private readonly FormErrorSerializer $formErrorSerializer - ) { + private FormFactoryInterface $formFactory; + + /** + * @param FormFactoryInterface $formFactory + */ + public function __construct(FormFactoryInterface $formFactory) + { + $this->formFactory = $formFactory; } /** @@ -43,6 +44,8 @@ public function __construct( */ public function editAction(Request $request, int $nodeId, int $translationId): Response { + $this->denyAccessUnlessGranted('ROLE_ACCESS_NODE_ATTRIBUTES'); + /** @var Translation|null $translation */ $translation = $this->em()->find(Translation::class, $translationId); /** @var Node|null $node */ @@ -52,8 +55,6 @@ public function editAction(Request $request, int $nodeId, int $translationId): R throw $this->createNotFoundException('Node-source does not exist'); } - $this->denyAccessUnlessGranted(NodeVoter::EDIT_ATTRIBUTE, $node); - /** @var NodesSources|null $nodeSource */ $nodeSource = $this->em() ->getRepository(NodesSources::class) @@ -65,32 +66,12 @@ public function editAction(Request $request, int $nodeId, int $translationId): R throw $this->createNotFoundException('Node-source does not exist'); } - if (!$this->isAttributable($node)) { - throw $this->createNotFoundException('Node type is not attributable'); - } - if (null !== $response = $this->handleAddAttributeForm($request, $node, $translation)) { return $response; } - $isJson = - $request->isXmlHttpRequest() || - $request->getRequestFormat('html') === 'json' || - \in_array( - 'application/json', - $request->getAcceptableContentTypes() - ); - $this->assignation['attribute_value_translation_forms'] = []; - $nodeType = $node->getNodeType(); - $orderByWeight = false; - if ($nodeType instanceof NodeType) { - $orderByWeight = $nodeType->isSortingAttributesByWeight(); - } - $attributeValues = $this->em()->getRepository(AttributeValue::class)->findByAttributable( - $node, - $orderByWeight - ); + $attributeValues = $node->getAttributeValues(); /** @var AttributeValue $attributeValue */ foreach ($attributeValues as $attributeValue) { $name = $node->getNodeName() . '_attribute_' . $attributeValue->getId(); @@ -126,27 +107,27 @@ public function editAction(Request $request, int $nodeId, int $translationId): R ); $this->publishConfirmMessage($request, $msg, $nodeSource); - if ($isJson) { + if ($request->isXmlHttpRequest() || $request->getRequestFormat('html') === 'json') { return new JsonResponse([ 'status' => 'success', 'message' => $msg, - ], Response::HTTP_ACCEPTED); + ], JsonResponse::HTTP_ACCEPTED); } return $this->redirectToRoute('nodesEditAttributesPage', [ 'nodeId' => $node->getId(), 'translationId' => $translation->getId(), ]); } else { - $errors = $this->formErrorSerializer->getErrorsAsArray($attributeValueTranslationForm); + $errors = $this->getErrorsAsArray($attributeValueTranslationForm); /* * Handle errors when Ajax POST requests */ - if ($isJson) { + if ($request->isXmlHttpRequest() || $request->getRequestFormat('html') === 'json') { return new JsonResponse([ 'status' => 'fail', 'errors' => $errors, 'message' => $this->getTranslator()->trans('form_has_errors.check_you_fields'), - ], Response::HTTP_BAD_REQUEST); + ], JsonResponse::HTTP_BAD_REQUEST); } foreach ($errors as $error) { $this->publishErrorMessage($request, $error); @@ -159,7 +140,6 @@ public function editAction(Request $request, int $nodeId, int $translationId): R $this->assignation['source'] = $nodeSource; $this->assignation['translation'] = $translation; - $this->assignation['order_by_weight'] = $orderByWeight; $availableTranslations = $this->em() ->getRepository(Translation::class) ->findAvailableTranslationsForNode($node); @@ -174,15 +154,6 @@ protected function hasAttributes(): bool return $this->em()->getRepository(Attribute::class)->countBy([]) > 0; } - protected function isAttributable(Node $node): bool - { - $nodeType = $node->getNodeType(); - if ($nodeType instanceof NodeType) { - return $nodeType->isAttributable(); - } - return false; - } - /** * @param Request $request * @param Node $node @@ -192,9 +163,6 @@ protected function isAttributable(Node $node): bool */ protected function handleAddAttributeForm(Request $request, Node $node, Translation $translation): ?RedirectResponse { - if (!$this->isAttributable($node)) { - return null; - } if (!$this->hasAttributes()) { return null; } @@ -233,15 +201,16 @@ protected function handleAddAttributeForm(Request $request, Node $node, Translat /** * @param Request $request - * @param int $nodeId - * @param int $translationId - * @param int $attributeValueId + * @param int $nodeId + * @param int $translationId + * @param int $attributeValueId * * @return Response - * @throws RuntimeError */ - public function deleteAction(Request $request, int $nodeId, int $translationId, int $attributeValueId): Response + public function deleteAction(Request $request, $nodeId, $translationId, $attributeValueId): Response { + $this->denyAccessUnlessGranted('ROLE_ACCESS_ATTRIBUTES_DELETE'); + /** @var AttributeValue|null $item */ $item = $this->em()->find(AttributeValue::class, $attributeValueId); if ($item === null) { @@ -256,8 +225,6 @@ public function deleteAction(Request $request, int $nodeId, int $translationId, throw $this->createNotFoundException('Node-source does not exist'); } - $this->denyAccessUnlessGranted(NodeVoter::EDIT_ATTRIBUTE, $node); - /** @var NodesSources|null $nodeSource */ $nodeSource = $this->em() ->getRepository(NodesSources::class) @@ -284,9 +251,9 @@ public function deleteAction(Request $request, int $nodeId, int $translationId, '%nodeName%' => $nodeSource->getTitle(), ] ); - $this->publishConfirmMessage($request, $msg, $item); + $this->publishConfirmMessage($request, $msg); } catch (\RuntimeException $e) { - $this->publishErrorMessage($request, $e->getMessage(), $item); + $this->publishErrorMessage($request, $e->getMessage()); } return $this->redirectToRoute('nodesEditAttributesPage', [ @@ -310,10 +277,11 @@ public function deleteAction(Request $request, int $nodeId, int $translationId, * @param int $translationId * @param int $attributeValueId * @return Response - * @throws RuntimeError */ public function resetAction(Request $request, int $nodeId, int $translationId, int $attributeValueId): Response { + $this->denyAccessUnlessGranted('ROLE_ACCESS_ATTRIBUTES_DELETE'); + /** @var AttributeValueTranslation|null $item */ $item = $this->em() ->getRepository(AttributeValueTranslation::class) @@ -333,8 +301,6 @@ public function resetAction(Request $request, int $nodeId, int $translationId, i throw $this->createNotFoundException('Node-source does not exist'); } - $this->denyAccessUnlessGranted(NodeVoter::EDIT_ATTRIBUTE, $node); - /** @var NodesSources|null $nodeSource */ $nodeSource = $this->em() ->getRepository(NodesSources::class) @@ -361,9 +327,9 @@ public function resetAction(Request $request, int $nodeId, int $translationId, i '%nodeName%' => $nodeSource->getTitle(), ] ); - $this->publishConfirmMessage($request, $msg, $item); + $this->publishConfirmMessage($request, $msg); } catch (\RuntimeException $e) { - $this->publishErrorMessage($request, $e->getMessage(), $item); + $this->publishErrorMessage($request, $e->getMessage()); } return $this->redirectToRoute('nodesEditAttributesPage', [ diff --git a/src/Controllers/Nodes/NodesController.php b/src/Controllers/Nodes/NodesController.php index 482e097f..e304e24f 100644 --- a/src/Controllers/Nodes/NodesController.php +++ b/src/Controllers/Nodes/NodesController.php @@ -4,8 +4,6 @@ namespace Themes\Rozier\Controllers\Nodes; -use Doctrine\ORM\OptimisticLockException; -use Doctrine\ORM\ORMException; use RZ\Roadiz\Core\Handlers\HandlerFactoryInterface; use RZ\Roadiz\CoreBundle\Entity\Node; use RZ\Roadiz\CoreBundle\Entity\NodeType; @@ -18,14 +16,11 @@ use RZ\Roadiz\CoreBundle\Event\Node\NodeUndeletedEvent; use RZ\Roadiz\CoreBundle\Event\Node\NodeUpdatedEvent; use RZ\Roadiz\CoreBundle\Exception\EntityAlreadyExistsException; -use RZ\Roadiz\CoreBundle\ListManager\SessionListFilters; use RZ\Roadiz\CoreBundle\Node\Exception\SameNodeUrlException; use RZ\Roadiz\CoreBundle\Node\NodeFactory; use RZ\Roadiz\CoreBundle\Node\NodeMover; -use RZ\Roadiz\CoreBundle\Node\NodeOffspringResolverInterface; use RZ\Roadiz\CoreBundle\Node\UniqueNodeGenerator; use RZ\Roadiz\CoreBundle\Security\Authorization\Chroot\NodeChrootResolver; -use RZ\Roadiz\CoreBundle\Security\Authorization\Voter\NodeVoter; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\FormError; @@ -37,12 +32,31 @@ use Symfony\Component\Workflow\Registry; use Themes\Rozier\RozierApp; use Themes\Rozier\Traits\NodesTrait; +use Themes\Rozier\Utils\SessionListFilters; use Twig\Error\RuntimeError; -final class NodesController extends RozierApp +/** + * @package Themes\Rozier\Controllers\Nodes + */ +class NodesController extends RozierApp { use NodesTrait; + private NodeChrootResolver $nodeChrootResolver; + private NodeMover $nodeMover; + private Registry $workflowRegistry; + private HandlerFactoryInterface $handlerFactory; + private UniqueNodeGenerator $uniqueNodeGenerator; + private NodeFactory $nodeFactory; + /** + * @var class-string + */ + private string $nodeFormTypeClass; + /** + * @var class-string + */ + private string $addNodeFormTypeClass; + /** * @param NodeChrootResolver $nodeChrootResolver * @param NodeMover $nodeMover @@ -50,21 +64,27 @@ final class NodesController extends RozierApp * @param HandlerFactoryInterface $handlerFactory * @param UniqueNodeGenerator $uniqueNodeGenerator * @param NodeFactory $nodeFactory - * @param NodeOffspringResolverInterface $nodeOffspringResolver * @param class-string $nodeFormTypeClass * @param class-string $addNodeFormTypeClass */ public function __construct( - private readonly NodeChrootResolver $nodeChrootResolver, - private readonly NodeMover $nodeMover, - private readonly Registry $workflowRegistry, - private readonly HandlerFactoryInterface $handlerFactory, - private readonly UniqueNodeGenerator $uniqueNodeGenerator, - private readonly NodeFactory $nodeFactory, - private readonly NodeOffspringResolverInterface $nodeOffspringResolver, - private readonly string $nodeFormTypeClass, - private readonly string $addNodeFormTypeClass + NodeChrootResolver $nodeChrootResolver, + NodeMover $nodeMover, + Registry $workflowRegistry, + HandlerFactoryInterface $handlerFactory, + UniqueNodeGenerator $uniqueNodeGenerator, + NodeFactory $nodeFactory, + string $nodeFormTypeClass, + string $addNodeFormTypeClass ) { + $this->nodeChrootResolver = $nodeChrootResolver; + $this->nodeMover = $nodeMover; + $this->workflowRegistry = $workflowRegistry; + $this->handlerFactory = $handlerFactory; + $this->nodeFormTypeClass = $nodeFormTypeClass; + $this->addNodeFormTypeClass = $addNodeFormTypeClass; + $this->uniqueNodeGenerator = $uniqueNodeGenerator; + $this->nodeFactory = $nodeFactory; } protected function getNodeFactory(): NodeFactory @@ -168,14 +188,14 @@ public function indexAction(Request $request, ?string $filter = null): Response */ public function editAction(Request $request, int $nodeId, ?int $translationId = null): Response { + $this->validateNodeAccessForRole('ROLE_ACCESS_NODES_SETTING', $nodeId); + /** @var Node|null $node */ $node = $this->em()->find(Node::class, $nodeId); if (null === $node) { throw new ResourceNotFoundException(sprintf('Node #%s does not exist.', $nodeId)); } - $this->denyAccessUnlessGranted(NodeVoter::EDIT_SETTING, $node); - $this->em()->refresh($node); /* * Handle StackTypes form @@ -193,7 +213,7 @@ public function editAction(Request $request, int $nodeId, ?int $translationId = '%type%' => $type->getDisplayName(), ] ); - $this->publishConfirmMessage($request, $msg, $node); + $this->publishConfirmMessage($request, $msg); return $this->redirectToRoute( 'nodesEditPage', ['nodeId' => $node->getId()] @@ -234,7 +254,7 @@ public function editAction(Request $request, int $nodeId, ?int $translationId = $msg = $this->getTranslator()->trans('node.%name%.updated', [ '%name%' => $node->getNodeName(), ]); - $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first() ?: $node); + $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first()); return $this->redirectToRoute( 'nodesEditPage', ['nodeId' => $node->getId()] @@ -269,13 +289,13 @@ public function editAction(Request $request, int $nodeId, ?int $translationId = */ public function removeStackTypeAction(Request $request, int $nodeId, int $typeId): Response { + $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES'); + /** @var Node|null $node */ $node = $this->em()->find(Node::class, $nodeId); if (null === $node) { throw new ResourceNotFoundException(sprintf('Node #%s does not exist.', $nodeId)); } - $this->denyAccessUnlessGranted(NodeVoter::EDIT_SETTING, $node); - /** @var NodeType|null $type */ $type = $this->em()->find(NodeType::class, $typeId); if (null === $type) { @@ -292,7 +312,7 @@ public function removeStackTypeAction(Request $request, int $nodeId, int $typeId '%type%' => $type->getDisplayName(), ] ); - $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first() ?: null); + $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first()); return $this->redirectToRoute('nodesEditPage', ['nodeId' => $node->getId()]); } @@ -301,13 +321,12 @@ public function removeStackTypeAction(Request $request, int $nodeId, int $typeId * Handle node creation pages. * * @param Request $request - * @param int $nodeTypeId + * @param int $nodeTypeId * @param int|null $translationId * * @return Response - * @throws RuntimeError - * @throws ORMException - * @throws OptimisticLockException + * @throws \Doctrine\ORM\ORMException + * @throws \Doctrine\ORM\OptimisticLockException */ public function addAction(Request $request, int $nodeTypeId, ?int $translationId = null): Response { @@ -329,9 +348,7 @@ public function addAction(Request $request, int $nodeTypeId, ?int $translationId throw new ResourceNotFoundException(sprintf('Translation #%s does not exist.', $translationId)); } - $node = new Node(); - $node->setNodeType($type); - $node->setTtl($type->getDefaultTtl()); + $node = new Node($type); $chroot = $this->nodeChrootResolver->getChroot($this->getUser()); if (null !== $chroot) { @@ -357,7 +374,7 @@ public function addAction(Request $request, int $nodeTypeId, ?int $translationId 'node.%name%.created', ['%name%' => $node->getNodeName()] ); - $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first() ?: null); + $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first()); return $this->redirectToRoute( 'nodesEditSourcePage', @@ -389,12 +406,15 @@ public function addAction(Request $request, int $nodeTypeId, ?int $translationId * @param int|null $translationId * * @return Response - * @throws ORMException - * @throws OptimisticLockException - * @throws RuntimeError + * @throws \Doctrine\ORM\ORMException + * @throws \Doctrine\ORM\OptimisticLockException + * @throws \Twig\Error\RuntimeError */ public function addChildAction(Request $request, ?int $nodeId = null, ?int $translationId = null): Response { + // include CHRoot to enable creating node in it + $this->validateNodeAccessForRole('ROLE_ACCESS_NODES', $nodeId, true); + /** @var Translation|null $translation */ $translation = $this->em()->getRepository(Translation::class)->findDefault(); @@ -408,19 +428,15 @@ public function addChildAction(Request $request, ?int $nodeId = null, ?int $tran } if (null === $translation) { - throw new ResourceNotFoundException('Translation does not exist'); + throw new ResourceNotFoundException(sprintf('Translation does not exist')); } if (null !== $nodeId && $nodeId > 0) { - /** @var Node|null $parentNode */ - $parentNode = $this->em()->find(Node::class, $nodeId); - if (null === $parentNode) { - throw new ResourceNotFoundException(sprintf('Node #%s does not exist.', $nodeId)); - } - $this->denyAccessUnlessGranted(NodeVoter::CREATE, $parentNode); + /** @var Node $parentNode */ + $parentNode = $this->em() + ->find(Node::class, $nodeId); } else { $parentNode = null; - $this->denyAccessUnlessGranted(NodeVoter::CREATE_AT_ROOT); } $node = new Node(); @@ -447,7 +463,7 @@ public function addChildAction(Request $request, ?int $nodeId = null, ?int $tran 'child_node.%name%.created', ['%name%' => $node->getNodeName()] ); - $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first() ?: null); + $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first()); return $this->redirectToRoute( 'nodesEditSourcePage', @@ -478,10 +494,12 @@ public function addChildAction(Request $request, ?int $nodeId = null, ?int $tran * @param int $nodeId * * @return Response - * @throws RuntimeError + * @throws \Twig\Error\RuntimeError */ public function deleteAction(Request $request, int $nodeId): Response { + $this->validateNodeAccessForRole('ROLE_ACCESS_NODES_DELETE', $nodeId); + /** @var Node|null $node */ $node = $this->em()->find(Node::class, $nodeId); @@ -489,11 +507,9 @@ public function deleteAction(Request $request, int $nodeId): Response throw new ResourceNotFoundException(sprintf('Node #%s does not exist.', $nodeId)); } - $this->denyAccessUnlessGranted(NodeVoter::DELETE, $node); - $workflow = $this->workflowRegistry->get($node); if (!$workflow->can($node, 'delete')) { - $this->publishErrorMessage($request, sprintf('Node #%s cannot be deleted.', $nodeId), $node); + $this->publishErrorMessage($request, sprintf('Node #%s cannot be deleted.', $nodeId)); return $this->redirectToRoute( 'nodesEditSourcePage', [ @@ -528,14 +544,13 @@ public function deleteAction(Request $request, int $nodeId): Response 'node.%name%.deleted', ['%name%' => $node->getNodeName()] ); - $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first() ?: $node); + $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first()); - $referrer = $request->query->get('referer'); if ( - \is_string($referrer) && - (new UnicodeString($referrer))->trim()->startsWith('/') + $request->query->has('referer') && + (new UnicodeString($request->query->get('referer')))->startsWith('/') ) { - return $this->redirect($referrer); + return $this->redirect($request->query->get('referer')); } if (null !== $parent) { return $this->redirectToRoute( @@ -558,11 +573,11 @@ public function deleteAction(Request $request, int $nodeId): Response * @param Request $request * * @return Response - * @throws RuntimeError + * @throws \Twig\Error\RuntimeError */ public function emptyTrashAction(Request $request): Response { - $this->denyAccessUnlessGranted(NodeVoter::EMPTY_TRASH); + $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES_DELETE'); $form = $this->buildEmptyTrashForm(); $form->handleRequest($request); @@ -572,7 +587,10 @@ public function emptyTrashAction(Request $request): Response /** @var Node|null $chroot */ $chroot = $this->nodeChrootResolver->getChroot($this->getUser()); if ($chroot !== null) { - $criteria["parent"] = $this->nodeOffspringResolver->getAllOffspringIds($chroot); + /** @var NodeHandler $nodeHandler */ + $nodeHandler = $this->handlerFactory->getHandler($chroot); + $ids = $nodeHandler->getAllOffspringId(); + $criteria["parent"] = $ids; } $nodes = $this->em() @@ -610,10 +628,12 @@ public function emptyTrashAction(Request $request): Response * @param int $nodeId * * @return Response - * @throws RuntimeError + * @throws \Twig\Error\RuntimeError */ public function undeleteAction(Request $request, int $nodeId): Response { + $this->validateNodeAccessForRole('ROLE_ACCESS_NODES_DELETE', $nodeId); + /** @var Node|null $node */ $node = $this->em()->find(Node::class, $nodeId); @@ -621,11 +641,9 @@ public function undeleteAction(Request $request, int $nodeId): Response throw new ResourceNotFoundException(sprintf('Node #%s does not exist.', $nodeId)); } - $this->denyAccessUnlessGranted(NodeVoter::DELETE, $node); - $workflow = $this->workflowRegistry->get($node); if (!$workflow->can($node, 'undelete')) { - $this->publishErrorMessage($request, sprintf('Node #%s cannot be undeleted.', $nodeId), $node); + $this->publishErrorMessage($request, sprintf('Node #%s cannot be undeleted.', $nodeId)); return $this->redirectToRoute( 'nodesEditSourcePage', [ @@ -651,7 +669,7 @@ public function undeleteAction(Request $request, int $nodeId): Response 'node.%name%.undeleted', ['%name%' => $node->getNodeName()] ); - $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first() ?: $node); + $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first()); /* * Force redirect to avoid resending form when refreshing page */ @@ -694,26 +712,24 @@ public function generateAndAddNodeAction(Request $request): RedirectResponse throw new ResourceNotFoundException($msg); } } - /** - * @param Request $request - * @param int $nodeId + * @param Request $request + * @param int $nodeId * @return Response - * @throws RuntimeError */ public function publishAllAction(Request $request, int $nodeId): Response { + $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES_STATUS'); /** @var Node|null $node */ $node = $this->em()->find(Node::class, $nodeId); if (null === $node) { throw new ResourceNotFoundException(sprintf('Node #%s does not exist.', $nodeId)); } - $this->denyAccessUnlessGranted(NodeVoter::EDIT_STATUS, $node); $workflow = $this->workflowRegistry->get($node); if (!$workflow->can($node, 'publish')) { - $this->publishErrorMessage($request, sprintf('Node #%s cannot be published.', $nodeId), $node); + $this->publishErrorMessage($request, sprintf('Node #%s cannot be published.', $nodeId)); return $this->redirectToRoute( 'nodesEditSourcePage', [ @@ -732,13 +748,11 @@ public function publishAllAction(Request $request, int $nodeId): Response $this->em()->flush(); $msg = $this->getTranslator()->trans('node.offspring.published'); - $this->publishConfirmMessage($request, $msg, $node); + $this->publishConfirmMessage($request, $msg); return $this->redirectToRoute('nodesEditSourcePage', [ 'nodeId' => $nodeId, - 'translationId' => $node->getNodeSources()->first() ? - $node->getNodeSources()->first()->getTranslation()->getId() : - null, + 'translationId' => $node->getNodeSources()->first()->getTranslation()->getId(), ]); } diff --git a/src/Controllers/Nodes/NodesSourcesController.php b/src/Controllers/Nodes/NodesSourcesController.php index 980ed5f3..84012609 100644 --- a/src/Controllers/Nodes/NodesSourcesController.php +++ b/src/Controllers/Nodes/NodesSourcesController.php @@ -13,7 +13,6 @@ use RZ\Roadiz\CoreBundle\Event\NodesSources\NodesSourcesUpdatedEvent; use RZ\Roadiz\CoreBundle\Form\Error\FormErrorSerializer; use RZ\Roadiz\CoreBundle\Routing\NodeRouter; -use RZ\Roadiz\CoreBundle\Security\Authorization\Voter\NodeVoter; use RZ\Roadiz\CoreBundle\TwigExtension\JwtExtension; use Symfony\Component\Form\Extension\Core\Type\HiddenType; use Symfony\Component\Form\FormError; @@ -34,10 +33,13 @@ class NodesSourcesController extends RozierApp { use VersionedControllerTrait; - public function __construct( - private readonly JwtExtension $jwtExtension, - private readonly FormErrorSerializer $formErrorSerializer - ) { + private JwtExtension $jwtExtension; + private FormErrorSerializer $formErrorSerializer; + + public function __construct(JwtExtension $jwtExtension, FormErrorSerializer $formErrorSerializer) + { + $this->jwtExtension = $jwtExtension; + $this->formErrorSerializer = $formErrorSerializer; } /** @@ -52,6 +54,8 @@ public function __construct( */ public function editSourceAction(Request $request, int $nodeId, int $translationId): Response { + $this->validateNodeAccessForRole('ROLE_ACCESS_NODES', $nodeId); + /** @var Translation|null $translation */ $translation = $this->em()->find(Translation::class, $translationId); @@ -69,8 +73,6 @@ public function editSourceAction(Request $request, int $nodeId, int $translation throw new ResourceNotFoundException('Node does not exist'); } - $this->denyAccessUnlessGranted(NodeVoter::EDIT_CONTENT, $gNode); - /** @var NodesSources|null $source */ $source = $this->em() ->getRepository(NodesSources::class) @@ -107,16 +109,12 @@ public function editSourceAction(Request $request, int $nodeId, int $translation ] ); $form->handleRequest($request); - $isJsonRequest = - $request->isXmlHttpRequest() || - \in_array('application/json', $request->getAcceptableContentTypes()) - ; if ($form->isSubmitted()) { if ($form->isValid() && !$this->isReadOnly) { $this->onPostUpdate($source, $request); - if (!$isJsonRequest) { + if (!$request->isXmlHttpRequest()) { return $this->getPostUpdateRedirection($source); } @@ -169,7 +167,7 @@ public function editSourceAction(Request $request, int $nodeId, int $translation /* * Handle errors when Ajax POST requests */ - if ($isJsonRequest) { + if ($request->isXmlHttpRequest()) { $errors = $this->formErrorSerializer->getErrorsAsArray($form); return new JsonResponse([ 'status' => 'fail', @@ -209,10 +207,12 @@ public function removeAction(Request $request, int $nodeSourceId): Response if (null === $ns) { throw new ResourceNotFoundException('Node source does not exist'); } - $this->denyAccessUnlessGranted(NodeVoter::DELETE, $ns); + /** @var Node $node */ $node = $ns->getNode(); $this->em()->refresh($ns->getNode()); + $this->validateNodeAccessForRole('ROLE_ACCESS_NODES_DELETE', $node->getId()); + /* * Prevent deleting last node-source available in node. */ @@ -238,6 +238,7 @@ public function removeAction(Request $request, int $nodeSourceId): Response $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { + /** @var Node $node */ $node = $ns->getNode(); /* * Dispatch event @@ -247,18 +248,14 @@ public function removeAction(Request $request, int $nodeSourceId): Response $this->em()->remove($ns); $this->em()->flush(); - $ns = $node->getNodeSources()->first() ?: null; - - if (null === $ns) { - throw new ResourceNotFoundException('No more node-source available for this node.'); - } + $ns = $node->getNodeSources()->first(); $msg = $this->getTranslator()->trans('node_source.%node_source%.deleted.%translation%', [ '%node_source%' => $node->getNodeName(), '%translation%' => $ns->getTranslation()->getName(), ]); - $this->publishConfirmMessage($request, $msg, $node); + $this->publishConfirmMessage($request, $msg); return $this->redirectToRoute( 'nodesEditSourcePage', diff --git a/src/Controllers/Nodes/NodesTagsController.php b/src/Controllers/Nodes/NodesTagsController.php new file mode 100644 index 00000000..87f69398 --- /dev/null +++ b/src/Controllers/Nodes/NodesTagsController.php @@ -0,0 +1,104 @@ +nodeFactory = $nodeFactory; + } + + protected function getNodeFactory(): NodeFactory + { + return $this->nodeFactory; + } + + /** + * Return tags form for requested node. + * + * @param Request $request + * @param int $nodeId + * + * @return Response + * @throws \Twig\Error\RuntimeError + */ + public function editTagsAction(Request $request, int $nodeId): Response + { + $this->validateNodeAccessForRole('ROLE_ACCESS_NODES', $nodeId); + + /** @var NodesSources|null $source */ + $source = $this->em() + ->getRepository(NodesSources::class) + ->setDisplayingAllNodesStatuses(true) + ->setDisplayingNotPublishedNodes(true) + ->findOneBy([ + 'node.id' => $nodeId, + 'translation' => $this->em()->getRepository(Translation::class)->findDefault() + ]); + if (null === $source) { + /** @var NodesSources|null $source */ + $source = $this->em() + ->getRepository(NodesSources::class) + ->setDisplayingAllNodesStatuses(true) + ->setDisplayingNotPublishedNodes(true) + ->findOneBy([ + 'node.id' => $nodeId, + ]); + } + + if (null !== $source) { + $node = $source->getNode(); + $form = $this->createForm(NodeTagsType::class, $node); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + /* + * Dispatch event + */ + $this->dispatchEvent(new NodeTaggedEvent($node)); + $this->em()->flush(); + + $msg = $this->getTranslator()->trans('node.%node%.linked.tags', [ + '%node%' => $node->getNodeName(), + ]); + $this->publishConfirmMessage($request, $msg, $source); + + return $this->redirectToRoute( + 'nodesEditTagsPage', + ['nodeId' => $node->getId()] + ); + } + + $this->assignation['translation'] = $source->getTranslation(); + $this->assignation['node'] = $node; + $this->assignation['source'] = $source; + $this->assignation['form'] = $form->createView(); + + return $this->render('@RoadizRozier/nodes/editTags.html.twig', $this->assignation); + } + + throw new ResourceNotFoundException(); + } +} diff --git a/src/Controllers/Nodes/NodesTreesController.php b/src/Controllers/Nodes/NodesTreesController.php index 7604d5b6..0e2f157f 100644 --- a/src/Controllers/Nodes/NodesTreesController.php +++ b/src/Controllers/Nodes/NodesTreesController.php @@ -10,7 +10,6 @@ use RZ\Roadiz\CoreBundle\Entity\Translation; use RZ\Roadiz\CoreBundle\EntityHandler\NodeHandler; use RZ\Roadiz\CoreBundle\Security\Authorization\Chroot\NodeChrootResolver; -use RZ\Roadiz\CoreBundle\Security\Authorization\Voter\NodeVoter; use Symfony\Component\Form\ClickableInterface; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\HiddenType; @@ -28,17 +27,37 @@ use Symfony\Component\Workflow\Registry; use Themes\Rozier\RozierApp; use Themes\Rozier\Widgets\TreeWidgetFactory; -use Twig\Error\RuntimeError; +/** + * @package Themes\Rozier\Controllers\Nodes + */ class NodesTreesController extends RozierApp { + private NodeChrootResolver $nodeChrootResolver; + private TreeWidgetFactory $treeWidgetFactory; + private FormFactoryInterface $formFactory; + private HandlerFactoryInterface $handlerFactory; + private Registry $workflowRegistry; + + /** + * @param NodeChrootResolver $nodeChrootResolver + * @param TreeWidgetFactory $treeWidgetFactory + * @param FormFactoryInterface $formFactory + * @param HandlerFactoryInterface $handlerFactory + * @param Registry $workflowRegistry + */ public function __construct( - private readonly NodeChrootResolver $nodeChrootResolver, - private readonly TreeWidgetFactory $treeWidgetFactory, - private readonly FormFactoryInterface $formFactory, - private readonly HandlerFactoryInterface $handlerFactory, - private readonly Registry $workflowRegistry + NodeChrootResolver $nodeChrootResolver, + TreeWidgetFactory $treeWidgetFactory, + FormFactoryInterface $formFactory, + HandlerFactoryInterface $handlerFactory, + Registry $workflowRegistry ) { + $this->nodeChrootResolver = $nodeChrootResolver; + $this->treeWidgetFactory = $treeWidgetFactory; + $this->formFactory = $formFactory; + $this->handlerFactory = $handlerFactory; + $this->workflowRegistry = $workflowRegistry; } /** @@ -47,29 +66,25 @@ public function __construct( * @param int|null $translationId * * @return Response - * @throws RuntimeError */ - public function treeAction(Request $request, ?int $nodeId = null, ?int $translationId = null): Response + public function treeAction(Request $request, ?int $nodeId = null, ?int $translationId = null) { if (null !== $nodeId) { + $this->validateNodeAccessForRole('ROLE_ACCESS_NODES', $nodeId, true); /** @var Node|null $node */ $node = $this->em()->find(Node::class, $nodeId); + if (null === $node) { throw new ResourceNotFoundException(); } + $this->em()->refresh($node); - } elseif (null !== $user = $this->getUser()) { - $node = $this->nodeChrootResolver->getChroot($user); + } elseif (null !== $this->getUser()) { + $node = $this->nodeChrootResolver->getChroot($this->getUser()); } else { $node = null; } - if (null !== $node) { - $this->denyAccessUnlessGranted(NodeVoter::READ, $node); - } else { - $this->denyAccessUnlessGranted(NodeVoter::READ_AT_ROOT); - } - if (null !== $translationId) { /** @var Translation $translation */ $translation = $this->em() @@ -161,131 +176,120 @@ public function treeAction(Request $request, ?int $nodeId = null, ?int $translat /** * @param Request $request * @return Response - * @throws RuntimeError */ - public function bulkDeleteAction(Request $request): Response + public function bulkDeleteAction(Request $request) { - if (empty($request->get('deleteForm')['nodesIds'])) { - throw new ResourceNotFoundException(); - } + $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES_DELETE'); - $nodesIds = trim($request->get('deleteForm')['nodesIds']); - $nodesIds = explode(',', $nodesIds); - array_filter($nodesIds); + if (!empty($request->get('deleteForm')['nodesIds'])) { + $nodesIds = trim($request->get('deleteForm')['nodesIds']); + $nodesIds = explode(',', $nodesIds); + array_filter($nodesIds); - /** @var Node[] $nodes */ - $nodes = $this->em() - ->getRepository(Node::class) - ->setDisplayingNotPublishedNodes(true) - ->findBy([ - 'id' => $nodesIds, - ]); + /** @var Node[] $nodes */ + $nodes = $this->em() + ->getRepository(Node::class) + ->setDisplayingNotPublishedNodes(true) + ->findBy([ + 'id' => $nodesIds, + ]); - if (count($nodes) === 0) { - throw new ResourceNotFoundException(); - } + if (count($nodes) > 0) { + $form = $this->buildBulkDeleteForm( + $request->get('deleteForm')['referer'], + $nodesIds + ); + $form->handleRequest($request); + if ($request->get('confirm') && $form->isSubmitted() && $form->isValid()) { + $msg = $this->bulkDeleteNodes($form->getData()); + $this->publishConfirmMessage($request, $msg); + + if (!empty($form->getData()['referer'])) { + return $this->redirect($form->getData()['referer']); + } else { + return $this->redirectToRoute('nodesHomePage'); + } + } - foreach ($nodes as $node) { - $this->denyAccessUnlessGranted(NodeVoter::DELETE, $node); - } + $this->assignation['nodes'] = $nodes; + $this->assignation['form'] = $form->createView(); - $form = $this->buildBulkDeleteForm( - $request->get('deleteForm')['referer'], - $nodesIds - ); - $form->handleRequest($request); - if ($request->get('confirm') && $form->isSubmitted() && $form->isValid()) { - $msg = $this->bulkDeleteNodes($form->getData()); - $this->publishConfirmMessage($request, $msg); + if (!empty($request->get('deleteForm')['referer'])) { + $this->assignation['referer'] = $request->get('deleteForm')['referer']; + } - if (!empty($form->getData()['referer'])) { - return $this->redirect($form->getData()['referer']); - } else { - return $this->redirectToRoute('nodesHomePage'); + return $this->render('@RoadizRozier/nodes/bulkDelete.html.twig', $this->assignation); } } - $this->assignation['nodes'] = $nodes; - $this->assignation['form'] = $form->createView(); - - if (!empty($request->get('deleteForm')['referer'])) { - $this->assignation['referer'] = $request->get('deleteForm')['referer']; - } - - return $this->render('@RoadizRozier/nodes/bulkDelete.html.twig', $this->assignation); + throw new ResourceNotFoundException(); } /** * @param Request $request * * @return Response - * @throws RuntimeError */ - public function bulkStatusAction(Request $request): Response + public function bulkStatusAction(Request $request) { - if (empty($request->get('statusForm')['nodesIds'])) { - throw new ResourceNotFoundException(); - } + $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES_STATUS'); - $nodesIds = trim($request->get('statusForm')['nodesIds']); - $nodesIds = explode(',', $nodesIds); - array_filter($nodesIds); + if (!empty($request->get('statusForm')['nodesIds'])) { + $nodesIds = trim($request->get('statusForm')['nodesIds']); + $nodesIds = explode(',', $nodesIds); + array_filter($nodesIds); - /** @var Node[] $nodes */ - $nodes = $this->em() - ->getRepository(Node::class) - ->setDisplayingNotPublishedNodes(true) - ->findBy([ - 'id' => $nodesIds, - ]); + /** @var Node[] $nodes */ + $nodes = $this->em() + ->getRepository(Node::class) + ->setDisplayingNotPublishedNodes(true) + ->findBy([ + 'id' => $nodesIds, + ]); + if (count($nodes) > 0) { + $form = $this->buildBulkStatusForm( + $request->get('statusForm')['referer'], + $nodesIds, + (string) $request->get('statusForm')['status'] + ); - if (count($nodes) === 0) { - throw new ResourceNotFoundException(); - } + $form->handleRequest($request); - foreach ($nodes as $node) { - $this->denyAccessUnlessGranted(NodeVoter::EDIT_STATUS, $node); - } + if ($form->isSubmitted() && $form->isValid()) { + $msg = $this->bulkStatusNodes($form->getData()); + $this->publishConfirmMessage($request, $msg); - $form = $this->buildBulkStatusForm( - $request->get('statusForm')['referer'], - $nodesIds, - (string) $request->get('statusForm')['status'] - ); + if (!empty($form->getData()['referer'])) { + return $this->redirect($form->getData()['referer']); + } else { + return $this->redirectToRoute('nodesHomePage'); + } + } - $form->handleRequest($request); + $this->assignation['nodes'] = $nodes; + $this->assignation['form'] = $form->createView(); - if ($form->isSubmitted() && $form->isValid()) { - $msg = $this->bulkStatusNodes($form->getData()); - $this->publishConfirmMessage($request, $msg); + if (!empty($request->get('statusForm')['referer'])) { + $this->assignation['referer'] = $request->get('statusForm')['referer']; + } - if (!empty($form->getData()['referer'])) { - return $this->redirect($form->getData()['referer']); - } else { - return $this->redirectToRoute('nodesHomePage'); + return $this->render('@RoadizRozier/nodes/bulkStatus.html.twig', $this->assignation); } } - $this->assignation['nodes'] = $nodes; - $this->assignation['form'] = $form->createView(); - - if (!empty($request->get('statusForm')['referer'])) { - $this->assignation['referer'] = $request->get('statusForm')['referer']; - } - - return $this->render('@RoadizRozier/nodes/bulkStatus.html.twig', $this->assignation); + throw new ResourceNotFoundException(); } /** - * @param null|string $referer + * @param false|string $referer * @param array $nodesIds * * @return FormInterface */ private function buildBulkDeleteForm( - ?string $referer = null, - array $nodesIds = [] - ): FormInterface { + $referer = false, + $nodesIds = [] + ) { /** @var FormBuilder $builder */ $builder = $this->formFactory ->createNamedBuilder('deleteForm') @@ -298,7 +302,7 @@ private function buildBulkDeleteForm( ], ]); - if (null !== $referer && (new UnicodeString($referer))->startsWith('/')) { + if (false !== $referer && (new UnicodeString($referer))->startsWith('/')) { $builder->add('referer', HiddenType::class, [ 'data' => $referer, ]); @@ -341,7 +345,12 @@ private function bulkDeleteNodes(array $data) return $this->getTranslator()->trans('wrong.request'); } - private function bulkStatusNodes(array $data): string + /** + * @param array $data + * + * @return string + */ + private function bulkStatusNodes(array $data) { if (!empty($data['nodesIds'])) { $nodesIds = trim($data['nodesIds']); @@ -370,7 +379,10 @@ private function bulkStatusNodes(array $data): string return $this->getTranslator()->trans('wrong.request'); } - private function buildBulkTagForm(): FormInterface + /** + * @return FormInterface + */ + private function buildBulkTagForm() { /** @var FormBuilder $builder */ $builder = $this->formFactory @@ -503,17 +515,17 @@ private function untagNodes(array $data) } /** - * @param null|string $referer + * @param false|string $referer * @param array $nodesIds * @param string $status * * @return FormInterface */ private function buildBulkStatusForm( - ?string $referer = null, - array $nodesIds = [], - string $status = 'reject' - ): FormInterface { + $referer = false, + $nodesIds = [], + $status = 'reject' + ) { /** @var FormBuilder $builder */ $builder = $this->formFactory ->createNamedBuilder('statusForm') @@ -541,7 +553,7 @@ private function buildBulkStatusForm( ]) ; - if (null !== $referer && (new UnicodeString($referer))->startsWith('/')) { + if (false !== $referer && (new UnicodeString($referer))->startsWith('/')) { $builder->add('referer', HiddenType::class, [ 'data' => $referer, ]); diff --git a/src/Controllers/Nodes/NodesUtilsController.php b/src/Controllers/Nodes/NodesUtilsController.php index cf73e9fc..a25fa9bd 100644 --- a/src/Controllers/Nodes/NodesUtilsController.php +++ b/src/Controllers/Nodes/NodesUtilsController.php @@ -9,15 +9,23 @@ use RZ\Roadiz\CoreBundle\Event\Node\NodeDuplicatedEvent; use RZ\Roadiz\CoreBundle\Node\NodeDuplicator; use RZ\Roadiz\CoreBundle\Node\NodeNamePolicyInterface; -use RZ\Roadiz\CoreBundle\Security\Authorization\Voter\NodeVoter; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Themes\Rozier\RozierApp; +/** + * @package Themes\Rozier\Controllers\Nodes + */ class NodesUtilsController extends RozierApp { - public function __construct(private readonly NodeNamePolicyInterface $nodeNamePolicy) + private NodeNamePolicyInterface $nodeNamePolicy; + + /** + * @param NodeNamePolicyInterface $nodeNamePolicy + */ + public function __construct(NodeNamePolicyInterface $nodeNamePolicy) { + $this->nodeNamePolicy = $nodeNamePolicy; } /** @@ -28,16 +36,12 @@ public function __construct(private readonly NodeNamePolicyInterface $nodeNamePo * * @return Response */ - public function duplicateAction(Request $request, int $nodeId): Response + public function duplicateAction(Request $request, int $nodeId) { - /** @var Node|null $existingNode */ - $existingNode = $this->em()->find(Node::class, $nodeId); + $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES'); - if (null === $existingNode) { - throw $this->createNotFoundException(); - } - - $this->denyAccessUnlessGranted(NodeVoter::DUPLICATE, $existingNode); + /** @var Node $existingNode */ + $existingNode = $this->em()->find(Node::class, $nodeId); try { $duplicator = new NodeDuplicator( @@ -57,7 +61,7 @@ public function duplicateAction(Request $request, int $nodeId): Response '%name%' => $existingNode->getNodeName(), ]); - $this->publishConfirmMessage($request, $msg, $newNode->getNodeSources()->first() ?: $newNode); + $this->publishConfirmMessage($request, $msg, $newNode->getNodeSources()->first()); return $this->redirectToRoute( 'nodesEditPage', @@ -68,8 +72,7 @@ public function duplicateAction(Request $request, int $nodeId): Response $request, $this->getTranslator()->trans("impossible.duplicate.node.%name%", [ '%name%' => $existingNode->getNodeName(), - ]), - $existingNode + ]) ); return $this->redirectToRoute( diff --git a/src/Controllers/Nodes/TranstypeController.php b/src/Controllers/Nodes/TranstypeController.php index 7e3326ae..de1a7f3b 100644 --- a/src/Controllers/Nodes/TranstypeController.php +++ b/src/Controllers/Nodes/TranstypeController.php @@ -9,7 +9,7 @@ use RZ\Roadiz\CoreBundle\Event\Node\NodeUpdatedEvent; use RZ\Roadiz\CoreBundle\Event\NodesSources\NodesSourcesUpdatedEvent; use RZ\Roadiz\CoreBundle\Node\NodeTranstyper; -use RZ\Roadiz\CoreBundle\Security\Authorization\Voter\NodeVoter; +use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Exception\ResourceNotFoundException; @@ -17,22 +17,33 @@ use Themes\Rozier\RozierApp; use Twig\Error\RuntimeError; +/** + * @package Themes\Rozier\Controllers\Nodes + */ class TranstypeController extends RozierApp { - public function __construct(private readonly NodeTranstyper $nodeTranstyper) + private NodeTranstyper $nodeTranstyper; + + /** + * @param NodeTranstyper $nodeTranstyper + */ + public function __construct(NodeTranstyper $nodeTranstyper) { + $this->nodeTranstyper = $nodeTranstyper; } /** * @param Request $request * @param int $nodeId * - * @return Response + * @return RedirectResponse|Response * @throws RuntimeError * @throws \Exception */ - public function transtypeAction(Request $request, int $nodeId): Response + public function transtypeAction(Request $request, int $nodeId) { + $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES'); + /** @var Node|null $node */ $node = $this->em()->find(Node::class, $nodeId); $this->em()->refresh($node); @@ -41,11 +52,6 @@ public function transtypeAction(Request $request, int $nodeId): Response throw new ResourceNotFoundException(); } - /* - * Transtype is only available for higher rank users - */ - $this->denyAccessUnlessGranted(NodeVoter::EDIT_SETTING, $node); - $form = $this->createForm(TranstypeType::class, null, [ 'currentType' => $node->getNodeType(), ]); @@ -85,15 +91,13 @@ public function transtypeAction(Request $request, int $nodeId): Response '%node%' => $node->getNodeName(), '%type%' => $newNodeType->getName(), ]); - $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first() ?: $node); + $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first()); return $this->redirectToRoute( 'nodesEditSourcePage', [ 'nodeId' => $node->getId(), - 'translationId' => $node->getNodeSources()->first() ? - $node->getNodeSources()->first()->getTranslation()->getId() : - null, + 'translationId' => $node->getNodeSources()->first()->getTranslation()->getId(), ] ); } diff --git a/src/Controllers/Nodes/UrlAliasesController.php b/src/Controllers/Nodes/UrlAliasesController.php new file mode 100644 index 00000000..a9966545 --- /dev/null +++ b/src/Controllers/Nodes/UrlAliasesController.php @@ -0,0 +1,380 @@ +formFactory = $formFactory; + } + + /** + * Return aliases form for requested node. + * + * @param Request $request + * @param int $nodeId + * @param int|null $translationId + * + * @return Response + * @throws RuntimeError + */ + public function editAliasesAction(Request $request, int $nodeId, ?int $translationId = null): Response + { + $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES'); + + if (null === $translationId || $translationId < 1) { + $translation = $this->em()->getRepository(Translation::class)->findDefault(); + } else { + $translation = $this->em()->find(Translation::class, $translationId); + } + /** @var NodesSources|null $source */ + $source = $this->em() + ->getRepository(NodesSources::class) + ->setDisplayingAllNodesStatuses(true) + ->setDisplayingNotPublishedNodes(true) + ->findOneBy(['translation' => $translation, 'node.id' => $nodeId]); + + if ($source !== null && null !== $node = $source->getNode()) { + $redirections = $this->em() + ->getRepository(Redirection::class) + ->findBy([ + 'redirectNodeSource' => $node->getNodeSources()->toArray() + ]); + $uas = $this->em() + ->getRepository(UrlAlias::class) + ->findAllFromNode($node->getId()); + $availableTranslations = $this->em() + ->getRepository(Translation::class) + ->findAvailableTranslationsForNode($node); + + $this->assignation['node'] = $node; + $this->assignation['source'] = $source; + $this->assignation['aliases'] = []; + $this->assignation['redirections'] = []; + $this->assignation['translation'] = $translation; + $this->assignation['available_translations'] = $availableTranslations; + + /* + * SEO Form + */ + $seoForm = $this->createForm(NodeSourceSeoType::class, $source); + $seoForm->handleRequest($request); + if ($seoForm->isSubmitted() && $seoForm->isValid()) { + $this->em()->flush(); + $msg = $this->getTranslator()->trans('node.seo.updated'); + $this->publishConfirmMessage($request, $msg, $source); + /* + * Dispatch event + */ + $this->dispatchEvent(new NodesSourcesUpdatedEvent($source)); + return $this->redirectToRoute( + 'nodesEditSEOPage', + ['nodeId' => $node->getId(), 'translationId' => $translationId] + ); + } + + if (null !== $response = $this->handleAddRedirection($source, $request)) { + return $response; + } + /* + * each url alias edit form + */ + /** @var UrlAlias $alias */ + foreach ($uas as $alias) { + if (null !== $response = $this->handleSingleUrlAlias($alias, $request)) { + return $response; + } + } + + /** @var Redirection $redirection */ + foreach ($redirections as $redirection) { + if (null !== $response = $this->handleSingleRedirection($redirection, $request)) { + return $response; + } + } + + /* + * Main ADD url alias form + */ + $alias = new UrlAlias(); + $addAliasForm = $this->formFactory->createNamed( + 'add_urlalias_' . $node->getId(), + UrlAliasType::class, + $alias, + [ + 'with_translation' => true + ] + ); + $addAliasForm->handleRequest($request); + if ($addAliasForm->isSubmitted() && $addAliasForm->isValid()) { + try { + $alias = $this->addNodeUrlAlias($alias, $node, $addAliasForm->get('translation')->getData()); + $msg = $this->getTranslator()->trans('url_alias.%alias%.created.%translation%', [ + '%alias%' => $alias->getAlias(), + '%translation%' => $alias->getNodeSource()->getTranslation()->getName(), + ]); + $this->publishConfirmMessage($request, $msg, $source); + /* + * Dispatch event + */ + $this->dispatchEvent(new UrlAliasCreatedEvent($alias)); + + return $this->redirect($this->generateUrl( + 'nodesEditSEOPage', + ['nodeId' => $node->getId(), 'translationId' => $translationId] + ) . '#manage-aliases'); + } catch (EntityAlreadyExistsException $e) { + $addAliasForm->addError(new FormError($e->getMessage())); + } catch (NoTranslationAvailableException $e) { + $addAliasForm->addError(new FormError($e->getMessage())); + } + } + + $this->assignation['form'] = $addAliasForm->createView(); + $this->assignation['seoForm'] = $seoForm->createView(); + + return $this->render('@RoadizRozier/nodes/editAliases.html.twig', $this->assignation); + } + + throw new ResourceNotFoundException(); + } + + /** + * @param UrlAlias $alias + * @param Node $node + * @param Translation $translation + * @return UrlAlias + */ + private function addNodeUrlAlias(UrlAlias $alias, Node $node, Translation $translation): UrlAlias + { + /** @var NodesSources|null $nodeSource */ + $nodeSource = $this->em() + ->getRepository(NodesSources::class) + ->setDisplayingAllNodesStatuses(true) + ->setDisplayingNotPublishedNodes(true) + ->findOneBy(['node' => $node, 'translation' => $translation]); + + if ($nodeSource !== null) { + $alias->setNodeSource($nodeSource); + $this->em()->persist($alias); + $this->em()->flush(); + + return $alias; + } else { + $msg = $this->getTranslator()->trans('url_alias.no_translation.%translation%', [ + '%translation%' => $translation->getName() + ]); + throw new NoTranslationAvailableException($msg); + } + } + + /** + * @param UrlAlias $alias + * @param Request $request + * + * @return RedirectResponse|null + */ + private function handleSingleUrlAlias(UrlAlias $alias, Request $request): ?RedirectResponse + { + $editForm = $this->formFactory->createNamed( + 'edit_urlalias_' . $alias->getId(), + UrlAliasType::class, + $alias + ); + $deleteForm = $this->formFactory->createNamed('delete_urlalias_' . $alias->getId()); + // Match edit + $editForm->handleRequest($request); + if ($editForm->isSubmitted() && $editForm->isValid()) { + try { + try { + $this->em()->flush(); + $msg = $this->getTranslator()->trans( + 'url_alias.%alias%.updated', + ['%alias%' => $alias->getAlias()] + ); + $this->publishConfirmMessage($request, $msg, $alias->getNodeSource()); + /* + * Dispatch event + */ + $this->dispatchEvent(new UrlAliasUpdatedEvent($alias)); + /** @var Translation $translation */ + $translation = $alias->getNodeSource()->getTranslation(); + + return $this->redirect($this->generateUrl( + 'nodesEditSEOPage', + [ + 'nodeId' => $alias->getNodeSource()->getNode()->getId(), + 'translationId' => $translation->getId() + ] + ) . '#manage-aliases'); + } catch (\RuntimeException $exception) { + $editForm->addError(new FormError($exception->getMessage())); + } + } catch (EntityAlreadyExistsException $e) { + $editForm->addError(new FormError($e->getMessage())); + } + } + + // Match delete + $deleteForm->handleRequest($request); + if ($deleteForm->isSubmitted() && $deleteForm->isValid()) { + $this->em()->remove($alias); + $this->em()->flush(); + $msg = $this->getTranslator()->trans('url_alias.%alias%.deleted', ['%alias%' => $alias->getAlias()]); + $this->publishConfirmMessage($request, $msg, $alias->getNodeSource()); + + /* + * Dispatch event + */ + $this->dispatchEvent(new UrlAliasDeletedEvent($alias)); + + /** @var Translation $translation */ + $translation = $alias->getNodeSource()->getTranslation(); + + return $this->redirect($this->generateUrl( + 'nodesEditSEOPage', + [ + 'nodeId' => $alias->getNodeSource()->getNode()->getId(), + 'translationId' => $translation->getId() + ] + ) . '#manage-aliases'); + } + + $this->assignation['aliases'][] = [ + 'alias' => $alias, + 'editForm' => $editForm->createView(), + 'deleteForm' => $deleteForm->createView(), + ]; + + return null; + } + + /** + * @param NodesSources $source + * @param Request $request + * @return RedirectResponse|null + */ + private function handleAddRedirection(NodesSources $source, Request $request): ?RedirectResponse + { + $redirection = new Redirection(); + $redirection->setRedirectNodeSource($source); + $redirection->setType(Response::HTTP_MOVED_PERMANENTLY); + + $addForm = $this->formFactory->createNamed( + 'add_redirection', + RedirectionType::class, + $redirection, + [ + 'placeholder' => $this->generateUrl($source), + 'only_query' => true + ] + ); + + $addForm->handleRequest($request); + if ($addForm->isSubmitted() && $addForm->isValid()) { + $this->em()->persist($redirection); + $this->em()->flush(); + + /** @var Translation $translation */ + $translation = $redirection->getRedirectNodeSource()->getTranslation(); + + return $this->redirect($this->generateUrl( + 'nodesEditSEOPage', + [ + 'nodeId' => $redirection->getRedirectNodeSource()->getNode()->getId(), + 'translationId' => $translation->getId() + ] + ) . '#manage-redirections'); + } + + $this->assignation['addRedirection'] = $addForm->createView(); + + return null; + } + + /** + * @param Redirection $redirection + * @param Request $request + * @return RedirectResponse|null + */ + private function handleSingleRedirection(Redirection $redirection, Request $request): ?RedirectResponse + { + $editForm = $this->formFactory->createNamed( + 'edit_redirection_' . $redirection->getId(), + RedirectionType::class, + $redirection, + [ + 'only_query' => true + ] + ); + + /** @var Translation $translation */ + $translation = $redirection->getRedirectNodeSource()->getTranslation(); + + $deleteForm = $this->formFactory->createNamed('delete_redirection_' . $redirection->getId()); + + $editForm->handleRequest($request); + if ($editForm->isSubmitted() && $editForm->isValid()) { + $this->em()->flush(); + return $this->redirect($this->generateUrl( + 'nodesEditSEOPage', + [ + 'nodeId' => $redirection->getRedirectNodeSource()->getNode()->getId(), + 'translationId' => $translation->getId() + ] + ) . '#manage-redirections'); + } + + // Match delete + $deleteForm->handleRequest($request); + if ($deleteForm->isSubmitted() && $deleteForm->isValid()) { + $this->em()->remove($redirection); + $this->em()->flush(); + return $this->redirect($this->generateUrl( + 'nodesEditSEOPage', + [ + 'nodeId' => $redirection->getRedirectNodeSource()->getNode()->getId(), + 'translationId' => $translation->getId() + ] + ) . '#manage-redirections'); + } + $this->assignation['redirections'][] = [ + 'redirection' => $redirection, + 'editForm' => $editForm->createView(), + 'deleteForm' => $deleteForm->createView(), + ]; + + return null; + } +} diff --git a/src/Controllers/PingController.php b/src/Controllers/PingController.php new file mode 100644 index 00000000..898e2a31 --- /dev/null +++ b/src/Controllers/PingController.php @@ -0,0 +1,26 @@ +denyAccessUnlessGranted('ROLE_BACKEND_USER'); + return $this->renderJson(['Pong']); + } +} diff --git a/src/Controllers/RedirectionsController.php b/src/Controllers/RedirectionsController.php index b2819ef5..cd787e20 100644 --- a/src/Controllers/RedirectionsController.php +++ b/src/Controllers/RedirectionsController.php @@ -6,14 +6,10 @@ use RZ\Roadiz\Core\AbstractEntities\PersistableInterface; use RZ\Roadiz\CoreBundle\Entity\Redirection; -use RZ\Roadiz\CoreBundle\Event\Redirection\PostCreatedRedirectionEvent; -use RZ\Roadiz\CoreBundle\Event\Redirection\PostDeletedRedirectionEvent; -use RZ\Roadiz\CoreBundle\Event\Redirection\PostUpdatedRedirectionEvent; -use RZ\Roadiz\CoreBundle\Event\Redirection\RedirectionEvent; use Symfony\Component\HttpFoundation\Request; use Themes\Rozier\Forms\RedirectionType; -class RedirectionsController extends AbstractAdminWithBulkController +class RedirectionsController extends AbstractAdminController { /** * @inheritDoc @@ -105,33 +101,4 @@ protected function getDefaultOrder(Request $request): array { return ['query' => 'ASC']; } - - protected function createPostCreateEvent(PersistableInterface $item): RedirectionEvent - { - if (!($item instanceof Redirection)) { - throw new \InvalidArgumentException('Item should be instance of ' . Redirection::class); - } - return new PostCreatedRedirectionEvent($item); - } - - protected function createPostUpdateEvent(PersistableInterface $item): RedirectionEvent - { - if (!($item instanceof Redirection)) { - throw new \InvalidArgumentException('Item should be instance of ' . Redirection::class); - } - return new PostUpdatedRedirectionEvent($item); - } - - protected function createDeleteEvent(PersistableInterface $item): RedirectionEvent - { - if (!($item instanceof Redirection)) { - throw new \InvalidArgumentException('Item should be instance of ' . Redirection::class); - } - return new PostDeletedRedirectionEvent($item); - } - - protected function getBulkDeleteRouteName(): ?string - { - return 'redirectionsBulkDeletePage'; - } } diff --git a/src/Controllers/RolesUtilsController.php b/src/Controllers/RolesUtilsController.php index 4cc08dc7..382baaf3 100644 --- a/src/Controllers/RolesUtilsController.php +++ b/src/Controllers/RolesUtilsController.php @@ -20,10 +20,17 @@ class RolesUtilsController extends RozierApp { - public function __construct( - private readonly SerializerInterface $serializer, - private readonly RolesImporter $rolesImporter - ) { + private SerializerInterface $serializer; + private RolesImporter $rolesImporter; + + /** + * @param SerializerInterface $serializer + * @param RolesImporter $rolesImporter + */ + public function __construct(SerializerInterface $serializer, RolesImporter $rolesImporter) + { + $this->serializer = $serializer; + $this->rolesImporter = $rolesImporter; } /** @@ -83,9 +90,6 @@ public function importJsonFileAction(Request $request): Response if ($file->isValid()) { $serializedData = file_get_contents($file->getPathname()); - if (false === $serializedData) { - throw new RuntimeError('Cannot read uploaded file.'); - } if (null !== \json_decode($serializedData)) { if ($this->rolesImporter->import($serializedData)) { diff --git a/src/Controllers/SearchController.php b/src/Controllers/SearchController.php index e615d884..d6d4aecf 100644 --- a/src/Controllers/SearchController.php +++ b/src/Controllers/SearchController.php @@ -5,6 +5,7 @@ namespace Themes\Rozier\Controllers; use DateTime; +use IteratorAggregate; use PhpOffice\PhpSpreadsheet\Exception; use RZ\Roadiz\Core\AbstractEntities\AbstractField; use RZ\Roadiz\CoreBundle\Entity\Node; @@ -17,7 +18,7 @@ use RZ\Roadiz\CoreBundle\Form\NodeStatesType; use RZ\Roadiz\CoreBundle\Form\NodeTypesType; use RZ\Roadiz\CoreBundle\Form\SeparatorType; -use RZ\Roadiz\CoreBundle\Xlsx\NodeSourceXlsxSerializer; +use RZ\Roadiz\CoreBundle\Xlsx\XlsxExporter; use Symfony\Component\Form\ClickableInterface; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; @@ -43,10 +44,6 @@ class SearchController extends RozierApp protected bool $pagination = true; protected ?int $itemPerPage = null; - public function __construct(protected readonly NodeSourceXlsxSerializer $xlsxSerializer) - { - } - /** * @param mixed $var * @return bool @@ -71,7 +68,7 @@ public function notBlank(mixed $var): bool * * @return array */ - protected function appendDateTimeCriteria(array &$data, string $fieldName): array + protected function appendDateTimeCriteria(array &$data, string $fieldName) { $date = $data[$fieldName]['compareDatetime']; if ($date instanceof DateTime) { @@ -89,10 +86,12 @@ protected function appendDateTimeCriteria(array &$data, string $fieldName): arra * @param string $prefix * @return mixed */ - protected function processCriteria($data, string $prefix = ""): mixed + protected function processCriteria($data, string $prefix = "") { if (!empty($data[$prefix . "nodeName"])) { - if (!isset($data[$prefix . "nodeName_exact"]) || $data[$prefix . "nodeName_exact"] !== true) { + if (isset($data[$prefix . "nodeName_exact"]) && $data[$prefix . "nodeName_exact"] === true) { + $data[$prefix . "nodeName"] = $data[$prefix . "nodeName"]; + } else { $data[$prefix . "nodeName"] = ["LIKE", "%" . $data[$prefix . "nodeName"] . "%"]; } } @@ -140,11 +139,11 @@ protected function processCriteria($data, string $prefix = ""): mixed } /** - * @param array $data + * @param array|\Traversable $data * @param NodeType $nodetype - * @return array + * @return mixed */ - protected function processCriteriaNodetype(array $data, NodeType $nodetype): array + protected function processCriteriaNodetype($data, NodeType $nodetype) { $fields = $nodetype->getFields(); foreach ($data as $key => $value) { @@ -198,7 +197,7 @@ protected function processCriteriaNodetype(array $data, NodeType $nodetype): arr * @return Response * @throws RuntimeError */ - public function searchNodeAction(Request $request): Response + public function searchNodeAction(Request $request) { $builder = $this->buildSimpleForm(''); $form = $this->addButtons($builder)->getForm(); @@ -254,12 +253,12 @@ public function searchNodeAction(Request $request): Response * @param Request $request * @param int $nodetypeId * - * @return Response + * @return null|RedirectResponse|Response * @throws Exception * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception * @throws RuntimeError */ - public function searchNodeSourceAction(Request $request, int $nodetypeId): Response + public function searchNodeSourceAction(Request $request, int $nodetypeId) { /** @var NodeType|null $nodetype */ $nodetype = $this->em()->find(NodeType::class, $nodetypeId); @@ -268,6 +267,7 @@ public function searchNodeSourceAction(Request $request, int $nodetypeId): Respo $this->extendForm($builder, $nodetype); $this->addButtons($builder, true); + /** @var Form $form */ $form = $builder->getForm(); $form->handleRequest($request); @@ -279,7 +279,7 @@ public function searchNodeSourceAction(Request $request, int $nodetypeId): Respo return $response; } - if (null !== $response = $this->handleNodeForm($request, $form, $nodetype)) { + if (null !== $response = $this->handleNodeForm($form, $nodetype)) { return $response; } @@ -316,11 +316,11 @@ protected function buildNodeTypeForm(?int $nodetypeId = null): FormBuilderInterf /** * @param FormBuilderInterface $builder - * @param bool $export + * @param bool $exportXlsx * * @return FormBuilderInterface */ - protected function addButtons(FormBuilderInterface $builder, bool $export = false): FormBuilderInterface + protected function addButtons(FormBuilderInterface $builder, bool $exportXlsx = false): FormBuilderInterface { $builder->add('search', SubmitType::class, [ 'label' => 'search.a.node', @@ -329,7 +329,7 @@ protected function addButtons(FormBuilderInterface $builder, bool $export = fals ], ]); - if ($export) { + if ($exportXlsx) { $builder->add('export', SubmitType::class, [ 'label' => 'export.all.nodesSource', 'attr' => [ @@ -346,7 +346,7 @@ protected function addButtons(FormBuilderInterface $builder, bool $export = fals * * @return null|RedirectResponse */ - protected function handleNodeTypeForm(FormInterface $nodeTypeForm): ?RedirectResponse + protected function handleNodeTypeForm(FormInterface $nodeTypeForm) { if ($nodeTypeForm->isSubmitted() && $nodeTypeForm->isValid()) { if (empty($nodeTypeForm->getData()['nodetype'])) { @@ -365,7 +365,6 @@ protected function handleNodeTypeForm(FormInterface $nodeTypeForm): ?RedirectRes } /** - * @param Request $request * @param FormInterface $form * @param NodeType $nodetype * @@ -373,7 +372,7 @@ protected function handleNodeTypeForm(FormInterface $nodeTypeForm): ?RedirectRes * @throws Exception * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception */ - protected function handleNodeForm(Request $request, FormInterface $form, NodeType $nodetype): ?Response + protected function handleNodeForm(FormInterface $form, NodeType $nodetype): ?Response { if ($form->isSubmitted() && $form->isValid()) { $data = []; @@ -419,13 +418,8 @@ protected function handleNodeForm(Request $request, FormInterface $form, NodeTyp */ $button = $form->get('export'); if ($button instanceof ClickableInterface && $button->isClicked()) { - $filename = 'search-' . $nodetype->getName() . '-' . date("YmdHis") . '.xlsx'; - $this->xlsxSerializer->setOnlyTexts(true); - $this->xlsxSerializer->addUrls(); - $xlsx = $this->xlsxSerializer->serialize($entities); - $response = new Response( - $xlsx, + $this->getXlsxResults($nodetype, $entities), Response::HTTP_OK, [] ); @@ -434,11 +428,10 @@ protected function handleNodeForm(Request $request, FormInterface $form, NodeTyp 'Content-Disposition', $response->headers->makeDisposition( ResponseHeaderBag::DISPOSITION_ATTACHMENT, - $filename + 'search.xlsx' ) ); - $response->prepare($request); return $response; } @@ -450,6 +443,43 @@ protected function handleNodeForm(Request $request, FormInterface $form, NodeTyp return null; } + /** + * @param NodeType $nodetype + * @param array|IteratorAggregate $entities + * + * @return string + * @throws Exception + * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception + */ + protected function getXlsxResults(NodeType $nodetype, $entities): string + { + $fields = $nodetype->getFields(); + $keys = []; + $answers = []; + $keys[] = "title"; + /** @var NodeTypeField $field */ + foreach ($fields as $field) { + if (!$field->isVirtual() && !$field->isCollection()) { + $keys[] = $field->getName(); + } + } + foreach ($entities as $idx => $nodesSource) { + $array = []; + foreach ($keys as $key) { + $getter = 'get' . str_replace('_', '', ucwords($key)); + $tmp = $nodesSource->$getter(); + if (is_array($tmp)) { + $tmp = implode(',', $tmp); + } + $array[] = $tmp; + } + $answers[$idx] = $array; + } + + $exporter = new XlsxExporter($this->getTranslator()); + return $exporter->exportXlsx($answers, $keys); + } + /** * @param string $prefix * @return FormBuilderInterface diff --git a/src/Controllers/SettingsController.php b/src/Controllers/SettingsController.php index 4a12ba8d..81284ef6 100644 --- a/src/Controllers/SettingsController.php +++ b/src/Controllers/SettingsController.php @@ -15,7 +15,6 @@ use RZ\Roadiz\CoreBundle\Exception\EntityAlreadyExistsException; use RZ\Roadiz\CoreBundle\Form\Error\FormErrorSerializer; use RZ\Roadiz\CoreBundle\Form\SettingType; -use RZ\Roadiz\CoreBundle\ListManager\SessionListFilters; use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\FormError; use Symfony\Component\Form\FormFactoryInterface; @@ -24,14 +23,18 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Themes\Rozier\RozierApp; +use Themes\Rozier\Utils\SessionListFilters; use Twig\Error\RuntimeError; class SettingsController extends RozierApp { - public function __construct( - private readonly FormFactoryInterface $formFactory, - private readonly FormErrorSerializer $formErrorSerializer - ) { + private FormFactoryInterface $formFactory; + private FormErrorSerializer $formErrorSerializer; + + public function __construct(FormFactoryInterface $formFactory, FormErrorSerializer $formErrorSerializer) + { + $this->formFactory = $formFactory; + $this->formErrorSerializer = $formErrorSerializer; } /** @@ -113,13 +116,6 @@ protected function commonSettingList(Request $request, SettingGroup $settingGrou $this->assignation['filters'] = $listManager->getAssignation(); $settings = $listManager->getEntities(); $this->assignation['settings'] = []; - $isJson = - $request->isXmlHttpRequest() || - $request->getRequestFormat('html') === 'json' || - \in_array( - 'application/json', - $request->getAcceptableContentTypes() - ); /** @var Setting $setting */ foreach ($settings as $setting) { @@ -137,9 +133,9 @@ protected function commonSettingList(Request $request, SettingGroup $settingGrou 'setting.%name%.updated', ['%name%' => $setting->getName()] ); - $this->publishConfirmMessage($request, $msg, $setting); + $this->publishConfirmMessage($request, $msg); - if ($isJson) { + if ($request->isXmlHttpRequest() || $request->getRequestFormat('html') === 'json') { return new JsonResponse([ 'status' => 'success', 'message' => $msg, @@ -166,7 +162,7 @@ protected function commonSettingList(Request $request, SettingGroup $settingGrou /* * Do not publish any message, it may lead to flushing invalid form */ - if ($isJson) { + if ($request->isXmlHttpRequest() || $request->getRequestFormat('html') === 'json') { return new JsonResponse([ 'status' => 'failed', 'errors' => $errors, @@ -222,7 +218,7 @@ public function editAction(Request $request, int $settingId): Response $this->dispatchEvent(new SettingUpdatedEvent($setting)); $this->em()->flush(); $msg = $this->getTranslator()->trans('setting.%name%.updated', ['%name%' => $setting->getName()]); - $this->publishConfirmMessage($request, $msg, $setting); + $this->publishConfirmMessage($request, $msg); /* * Force redirect to avoid resending form when refreshing page */ @@ -277,7 +273,7 @@ public function addAction(Request $request): Response $this->em()->persist($setting); $this->em()->flush(); $msg = $this->getTranslator()->trans('setting.%name%.created', ['%name%' => $setting->getName()]); - $this->publishConfirmMessage($request, $msg, $setting); + $this->publishConfirmMessage($request, $msg); return $this->redirectToRoute('settingsHomePage'); } catch (EntityAlreadyExistsException $e) { @@ -323,7 +319,7 @@ public function deleteAction(Request $request, int $settingId): Response $this->em()->flush(); $msg = $this->getTranslator()->trans('setting.%name%.deleted', ['%name%' => $setting->getName()]); - $this->publishConfirmMessage($request, $msg, $setting); + $this->publishConfirmMessage($request, $msg); /* * Force redirect to avoid resending form when refreshing page diff --git a/src/Controllers/SettingsUtilsController.php b/src/Controllers/SettingsUtilsController.php index 8d963eb4..7ddb148c 100644 --- a/src/Controllers/SettingsUtilsController.php +++ b/src/Controllers/SettingsUtilsController.php @@ -6,9 +6,9 @@ use JMS\Serializer\SerializationContext; use JMS\Serializer\SerializerInterface; +use RZ\Roadiz\CoreBundle\Importer\SettingsImporter; use RZ\Roadiz\CoreBundle\Entity\Setting; use RZ\Roadiz\CoreBundle\Entity\SettingGroup; -use RZ\Roadiz\CoreBundle\Importer\SettingsImporter; use RZ\Roadiz\Utils\StringHandler; use Symfony\Component\Form\Extension\Core\Type\FileType; use Symfony\Component\Form\FormError; @@ -21,10 +21,17 @@ class SettingsUtilsController extends RozierApp { - public function __construct( - private readonly SerializerInterface $serializer, - private readonly SettingsImporter $settingsImporter - ) { + private SerializerInterface $serializer; + private SettingsImporter $settingsImporter; + + /** + * @param SerializerInterface $serializer + * @param SettingsImporter $settingsImporter + */ + public function __construct(SerializerInterface $serializer, SettingsImporter $settingsImporter) + { + $this->serializer = $serializer; + $this->settingsImporter = $settingsImporter; } /** @@ -96,10 +103,6 @@ public function importJsonFileAction(Request $request): Response if ($file->isValid()) { $serializedData = file_get_contents($file->getPathname()); - if (!\is_string($serializedData)) { - throw new RuntimeError('Imported file is not a string.'); - } - if (null !== \json_decode($serializedData)) { if ($this->settingsImporter->import($serializedData)) { $msg = $this->getTranslator()->trans('setting.imported'); diff --git a/src/Controllers/Tags/TagMultiCreationController.php b/src/Controllers/Tags/TagMultiCreationController.php index fa8632b9..630fba1c 100644 --- a/src/Controllers/Tags/TagMultiCreationController.php +++ b/src/Controllers/Tags/TagMultiCreationController.php @@ -9,81 +9,91 @@ use RZ\Roadiz\CoreBundle\Event\Tag\TagCreatedEvent; use RZ\Roadiz\CoreBundle\Tag\TagFactory; use Symfony\Component\Form\FormError; +use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Themes\Rozier\Forms\MultiTagType; use Themes\Rozier\RozierApp; +/** + * @package Themes\Rozier\Controllers\Tags + */ class TagMultiCreationController extends RozierApp { - public function __construct(private readonly TagFactory $tagFactory) + private TagFactory $tagFactory; + + /** + * @param TagFactory $tagFactory + */ + public function __construct(TagFactory $tagFactory) { + $this->tagFactory = $tagFactory; } /** * @param Request $request * @param int $parentTagId - * @return Response + * @return RedirectResponse|Response|null * @throws \Twig\Error\RuntimeError */ - public function addChildAction(Request $request, int $parentTagId): Response + public function addChildAction(Request $request, int $parentTagId) { $this->denyAccessUnlessGranted('ROLE_ACCESS_TAGS'); $translation = $this->em()->getRepository(Translation::class)->findDefault(); $parentTag = $this->em()->find(Tag::class, $parentTagId); - if (null === $parentTag) { - throw new ResourceNotFoundException(); - } + if (null !== $parentTag) { + $form = $this->createForm(MultiTagType::class); + $form->handleRequest($request); - $form = $this->createForm(MultiTagType::class); - $form->handleRequest($request); - - if ($form->isSubmitted() && $form->isValid()) { - try { - $data = $form->getData(); - $names = explode(',', $data['names']); - $names = array_map('trim', $names); - $names = array_filter($names); - $names = array_unique($names); - - /* - * Get latest position to add tags after. - */ - $latestPosition = $this->em() - ->getRepository(Tag::class) - ->findLatestPositionInParent($parentTag); - - $tagsArray = []; - foreach ($names as $name) { - $tagsArray[] = $this->tagFactory->create($name, $translation, $parentTag, $latestPosition); - $this->em()->flush(); - } + if ($form->isSubmitted() && $form->isValid()) { + try { + $data = $form->getData(); + $names = explode(',', $data['names']); + $names = array_map('trim', $names); + $names = array_filter($names); + $names = array_unique($names); - /* - * Dispatch event and msg - */ - foreach ($tagsArray as $tag) { /* - * Dispatch event + * Get latest position to add tags after. */ - $this->dispatchEvent(new TagCreatedEvent($tag)); - $msg = $this->getTranslator()->trans('child.tag.%name%.created', ['%name%' => $tag->getTagName()]); - $this->publishConfirmMessage($request, $msg, $tag); - } + $latestPosition = $this->em() + ->getRepository(Tag::class) + ->findLatestPositionInParent($parentTag); + + $tagsArray = []; + foreach ($names as $name) { + $tagsArray[] = $this->tagFactory->create($name, $translation, $parentTag, $latestPosition); + $this->em()->flush(); + } - return $this->redirectToRoute('tagsTreePage', ['tagId' => $parentTagId]); - } catch (\InvalidArgumentException $e) { - $form->addError(new FormError($e->getMessage())); + /* + * Dispatch event and msg + */ + foreach ($tagsArray as $tag) { + /* + * Dispatch event + */ + $this->dispatchEvent(new TagCreatedEvent($tag)); + $msg = $this->getTranslator()->trans('child.tag.%name%.created', ['%name%' => $tag->getTagName()]); + $this->publishConfirmMessage($request, $msg); + } + + return $this->redirectToRoute('tagsTreePage', ['tagId' => $parentTagId]); + } catch (\InvalidArgumentException $e) { + $form->addError(new FormError($e->getMessage())); + } } - } - $this->assignation['translation'] = $translation; - $this->assignation['form'] = $form->createView(); - $this->assignation['tag'] = $parentTag; + $this->assignation['translation'] = $translation; + $this->assignation['form'] = $form->createView(); + $this->assignation['tag'] = $parentTag; + + return $this->render('@RoadizRozier/tags/add-multiple.html.twig', $this->assignation); + } - return $this->render('@RoadizRozier/tags/add-multiple.html.twig', $this->assignation); + throw new ResourceNotFoundException(); } } diff --git a/src/Controllers/Tags/TagsController.php b/src/Controllers/Tags/TagsController.php index 9a766e71..dd024d97 100644 --- a/src/Controllers/Tags/TagsController.php +++ b/src/Controllers/Tags/TagsController.php @@ -15,7 +15,6 @@ use RZ\Roadiz\CoreBundle\Event\Tag\TagDeletedEvent; use RZ\Roadiz\CoreBundle\Event\Tag\TagUpdatedEvent; use RZ\Roadiz\CoreBundle\Exception\EntityAlreadyExistsException; -use RZ\Roadiz\CoreBundle\Form\Error\FormErrorSerializer; use RZ\Roadiz\CoreBundle\Repository\TranslationRepository; use RZ\Roadiz\Utils\StringHandler; use Symfony\Component\Form\Extension\Core\Type\HiddenType; @@ -36,16 +35,30 @@ use Themes\Rozier\Widgets\TreeWidgetFactory; use Twig\Error\RuntimeError; +/** + * @package Themes\Rozier\Controllers\Tags + */ class TagsController extends RozierApp { use VersionedControllerTrait; + private HandlerFactoryInterface $handlerFactory; + private FormFactoryInterface $formFactory; + private TreeWidgetFactory $treeWidgetFactory; + + /** + * @param FormFactoryInterface $formFactory + * @param HandlerFactoryInterface $handlerFactory + * @param TreeWidgetFactory $treeWidgetFactory + */ public function __construct( - private readonly FormFactoryInterface $formFactory, - private readonly FormErrorSerializer $formErrorSerializer, - private readonly HandlerFactoryInterface $handlerFactory, - private readonly TreeWidgetFactory $treeWidgetFactory + FormFactoryInterface $formFactory, + HandlerFactoryInterface $handlerFactory, + TreeWidgetFactory $treeWidgetFactory ) { + $this->handlerFactory = $handlerFactory; + $this->formFactory = $formFactory; + $this->treeWidgetFactory = $treeWidgetFactory; } /** @@ -55,7 +68,7 @@ public function __construct( * * @return Response */ - public function indexAction(Request $request): Response + public function indexAction(Request $request) { $this->denyAccessUnlessGranted('ROLE_ACCESS_TAGS'); @@ -92,7 +105,7 @@ public function indexAction(Request $request): Response * @return Response * @throws RuntimeError */ - public function editTranslatedAction(Request $request, int $tagId, ?int $translationId = null): Response + public function editTranslatedAction(Request $request, int $tagId, ?int $translationId = null) { $this->denyAccessUnlessGranted('ROLE_ACCESS_TAGS'); @@ -153,10 +166,6 @@ public function editTranslatedAction(Request $request, int $tagId, ?int $transla 'disabled' => $this->isReadOnly, ]); $form->handleRequest($request); - $isJsonRequest = - $request->isXmlHttpRequest() || - \in_array('application/json', $request->getAcceptableContentTypes()) - ; if ($form->isSubmitted()) { if ($form->isValid()) { @@ -185,31 +194,24 @@ public function editTranslatedAction(Request $request, int $tagId, ?int $transla $msg = $this->getTranslator()->trans('tag.%name%.updated', [ '%name%' => $tagTranslation->getName(), ]); - $this->publishConfirmMessage($request, $msg, $tag); + $this->publishConfirmMessage($request, $msg); /* * Force redirect to avoid resending form when refreshing page */ - if (!$isJsonRequest) { - return $this->getPostUpdateRedirection($tagTranslation); - } - - return new JsonResponse([ - 'status' => 'success', - 'errors' => [], - ], Response::HTTP_PARTIAL_CONTENT); + return $this->getPostUpdateRedirection($tagTranslation); } /* * Handle errors when Ajax POST requests */ - if ($isJsonRequest) { - $errors = $this->formErrorSerializer->getErrorsAsArray($form); + if ($request->isXmlHttpRequest()) { + $errors = $this->getErrorsAsArray($form); return new JsonResponse([ 'status' => 'fail', 'errors' => $errors, 'message' => $this->getTranslator()->trans('form_has_errors.check_you_fields'), - ], Response::HTTP_BAD_REQUEST); + ], JsonResponse::HTTP_BAD_REQUEST); } } /** @var TranslationRepository $translationRepository */ @@ -244,7 +246,7 @@ protected function tagNameExists(string $name): bool * @return Response * @throws RuntimeError */ - public function bulkDeleteAction(Request $request): Response + public function bulkDeleteAction(Request $request) { $this->denyAccessUnlessGranted('ROLE_ACCESS_TAGS_DELETE'); @@ -294,10 +296,10 @@ public function bulkDeleteAction(Request $request): Response /** * @param Request $request + * * @return Response - * @throws RuntimeError */ - public function addAction(Request $request): Response + public function addAction(Request $request) { $this->denyAccessUnlessGranted('ROLE_ACCESS_TAGS'); @@ -331,7 +333,7 @@ public function addAction(Request $request): Response $this->dispatchEvent(new TagCreatedEvent($tag)); $msg = $this->getTranslator()->trans('tag.%name%.created', ['%name%' => $tag->getTagName()]); - $this->publishConfirmMessage($request, $msg, $tag); + $this->publishConfirmMessage($request, $msg); /* * Force redirect to avoid resending form when refreshing page */ @@ -351,9 +353,8 @@ public function addAction(Request $request): Response * @param int $tagId * * @return Response - * @throws RuntimeError */ - public function editSettingsAction(Request $request, int $tagId): Response + public function editSettingsAction(Request $request, int $tagId) { $this->denyAccessUnlessGranted('ROLE_ACCESS_TAGS'); @@ -371,10 +372,6 @@ public function editSettingsAction(Request $request, int $tagId): Response ]); $form->handleRequest($request); - $isJsonRequest = - $request->isXmlHttpRequest() || - \in_array('application/json', $request->getAcceptableContentTypes()) - ; if ($form->isSubmitted()) { if ($form->isValid()) { @@ -385,7 +382,7 @@ public function editSettingsAction(Request $request, int $tagId): Response $this->dispatchEvent(new TagUpdatedEvent($tag)); $msg = $this->getTranslator()->trans('tag.%name%.updated', ['%name%' => $tag->getTagName()]); - $this->publishConfirmMessage($request, $msg, $tag); + $this->publishConfirmMessage($request, $msg); /* * Force redirect to avoid resending form when refreshing page @@ -398,8 +395,8 @@ public function editSettingsAction(Request $request, int $tagId): Response /* * Handle errors when Ajax POST requests */ - if ($isJsonRequest) { - $errors = $this->formErrorSerializer->getErrorsAsArray($form); + if ($request->isXmlHttpRequest()) { + $errors = $this->getErrorsAsArray($form); return new JsonResponse([ 'status' => 'fail', 'errors' => $errors, @@ -421,9 +418,8 @@ public function editSettingsAction(Request $request, int $tagId): Response * @param int|null $translationId * * @return Response - * @throws RuntimeError */ - public function treeAction(Request $request, int $tagId, ?int $translationId = null): Response + public function treeAction(Request $request, int $tagId, ?int $translationId = null) { $this->denyAccessUnlessGranted('ROLE_ACCESS_TAGS'); @@ -453,12 +449,11 @@ public function treeAction(Request $request, int $tagId, ?int $translationId = n * Return a deletion form for requested tag. * * @param Request $request - * @param int $tagId + * @param int $tagId * * @return Response - * @throws RuntimeError */ - public function deleteAction(Request $request, int $tagId): Response + public function deleteAction(Request $request, int $tagId) { $this->denyAccessUnlessGranted('ROLE_ACCESS_TAGS_DELETE'); @@ -487,12 +482,8 @@ public function deleteAction(Request $request, int $tagId): Response $this->em()->remove($tag); $this->em()->flush(); - $msg = $this->getTranslator()->trans('tag.%name%.deleted', [ - '%name%' => $tag->getTranslatedTags()->first() ? - $tag->getTranslatedTags()->first()->getName() : - $tag->getTagName(), - ]); - $this->publishConfirmMessage($request, $msg, $tag); + $msg = $this->getTranslator()->trans('tag.%name%.deleted', ['%name%' => $tag->getTranslatedTags()->first()->getName()]); + $this->publishConfirmMessage($request, $msg); /* * Force redirect to avoid resending form when refreshing page @@ -516,9 +507,8 @@ public function deleteAction(Request $request, int $tagId): Response * @param int|null $translationId * * @return Response - * @throws RuntimeError */ - public function addChildAction(Request $request, int $tagId, ?int $translationId = null): Response + public function addChildAction(Request $request, int $tagId, ?int $translationId = null) { $this->denyAccessUnlessGranted('ROLE_ACCESS_TAGS'); @@ -560,7 +550,7 @@ public function addChildAction(Request $request, int $tagId, ?int $translationId $this->dispatchEvent(new TagCreatedEvent($tag)); $msg = $this->getTranslator()->trans('child.tag.%name%.created', ['%name%' => $tag->getTagName()]); - $this->publishConfirmMessage($request, $msg, $tag); + $this->publishConfirmMessage($request, $msg); return $this->redirectToRoute( 'tagsEditPage', @@ -590,7 +580,7 @@ public function addChildAction(Request $request, int $tagId, ?int $translationId * @return Response * @throws RuntimeError */ - public function editNodesAction(Request $request, int $tagId): Response + public function editNodesAction(Request $request, int $tagId) { $this->denyAccessUnlessGranted('ROLE_ACCESS_TAGS'); @@ -628,7 +618,7 @@ public function editNodesAction(Request $request, int $tagId): Response * * @return FormInterface */ - private function buildDeleteForm(Tag $tag): FormInterface + private function buildDeleteForm(Tag $tag) { $builder = $this->createFormBuilder() ->add('tagId', HiddenType::class, [ @@ -643,15 +633,15 @@ private function buildDeleteForm(Tag $tag): FormInterface } /** - * @param null|string $referer + * @param false|string $referer * @param array $tagsIds * * @return FormInterface */ private function buildBulkDeleteForm( - ?string $referer = null, + $referer = false, array $tagsIds = [] - ): FormInterface { + ) { $builder = $this->formFactory ->createNamedBuilder('deleteForm') ->add('tagsIds', HiddenType::class, [ @@ -663,7 +653,7 @@ private function buildBulkDeleteForm( ], ]); - if (null !== $referer && (new UnicodeString($referer))->startsWith('/')) { + if (false !== $referer && (new UnicodeString($referer))->startsWith('/')) { $builder->add('referer', HiddenType::class, [ 'data' => $referer, ]); @@ -677,7 +667,7 @@ private function buildBulkDeleteForm( * * @return string */ - private function bulkDeleteTags(array $data): string + private function bulkDeleteTags(array $data) { if (!empty($data['tagsIds'])) { $tagsIds = trim($data['tagsIds']); @@ -721,7 +711,7 @@ protected function onPostUpdate(PersistableInterface $entity, Request $request): $msg = $this->getTranslator()->trans('tag.%name%.updated', [ '%name%' => $entity->getName(), ]); - $this->publishConfirmMessage($request, $msg, $entity); + $this->publishConfirmMessage($request, $msg); } } diff --git a/src/Controllers/Tags/TagsUtilsController.php b/src/Controllers/Tags/TagsUtilsController.php index 71f96693..c60cfb7a 100644 --- a/src/Controllers/Tags/TagsUtilsController.php +++ b/src/Controllers/Tags/TagsUtilsController.php @@ -12,20 +12,30 @@ use Symfony\Component\HttpFoundation\Response; use Themes\Rozier\RozierApp; +/** + * @package Themes\Rozier\Controllers\Tags + */ class TagsUtilsController extends RozierApp { - public function __construct(private readonly SerializerInterface $serializer) + private SerializerInterface $serializer; + + /** + * @param SerializerInterface $serializer + */ + public function __construct(SerializerInterface $serializer) { + $this->serializer = $serializer; } /** * Export a Tag in a Json file * * @param Request $request - * @param int $tagId - * @return JsonResponse + * @param int $tagId + * + * @return Response */ - public function exportAction(Request $request, int $tagId): JsonResponse + public function exportAction(Request $request, int $tagId) { $this->denyAccessUnlessGranted('ROLE_ACCESS_TAGS'); @@ -37,7 +47,7 @@ public function exportAction(Request $request, int $tagId): JsonResponse 'json', SerializationContext::create()->setGroups(['tag', 'position']) ), - Response::HTTP_OK, + JsonResponse::HTTP_OK, [ 'Content-Disposition' => sprintf( 'attachment; filename="%s"', @@ -54,9 +64,9 @@ public function exportAction(Request $request, int $tagId): JsonResponse * @param Request $request * @param int $tagId * - * @return JsonResponse + * @return Response */ - public function exportAllAction(Request $request, int $tagId): JsonResponse + public function exportAllAction(Request $request, int $tagId) { $this->denyAccessUnlessGranted('ROLE_ACCESS_TAGS'); @@ -70,7 +80,7 @@ public function exportAllAction(Request $request, int $tagId): JsonResponse 'json', SerializationContext::create()->setGroups(['tag', 'position']) ), - Response::HTTP_OK, + JsonResponse::HTTP_OK, [ 'Content-Disposition' => sprintf( 'attachment; filename="%s"', diff --git a/src/Controllers/TranslationsController.php b/src/Controllers/TranslationsController.php index 601c24b1..94aacc18 100644 --- a/src/Controllers/TranslationsController.php +++ b/src/Controllers/TranslationsController.php @@ -4,12 +4,12 @@ namespace Themes\Rozier\Controllers; -use RZ\Roadiz\Core\Handlers\HandlerFactoryInterface; use RZ\Roadiz\CoreBundle\Entity\Translation; -use RZ\Roadiz\CoreBundle\EntityHandler\TranslationHandler; use RZ\Roadiz\CoreBundle\Event\Translation\TranslationCreatedEvent; use RZ\Roadiz\CoreBundle\Event\Translation\TranslationDeletedEvent; use RZ\Roadiz\CoreBundle\Event\Translation\TranslationUpdatedEvent; +use RZ\Roadiz\Core\Handlers\HandlerFactoryInterface; +use RZ\Roadiz\CoreBundle\EntityHandler\TranslationHandler; use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\FormError; use Symfony\Component\HttpFoundation\Request; @@ -23,8 +23,11 @@ class TranslationsController extends RozierApp { public const ITEM_PER_PAGE = 5; - public function __construct(private readonly HandlerFactoryInterface $handlerFactory) + private HandlerFactoryInterface $handlerFactory; + + public function __construct(HandlerFactoryInterface $handlerFactory) { + $this->handlerFactory = $handlerFactory; } /** @@ -58,7 +61,7 @@ public function indexAction(Request $request): Response $handler = $this->handlerFactory->getHandler($translation); $handler->makeDefault(); $msg = $this->getTranslator()->trans('translation.%name%.made_default', ['%name%' => $translation->getName()]); - $this->publishConfirmMessage($request, $msg, $translation); + $this->publishConfirmMessage($request, $msg); $this->dispatchEvent(new TranslationUpdatedEvent($translation)); /* * Force redirect to avoid resending form when refreshing page @@ -103,7 +106,7 @@ public function editAction(Request $request, int $translationId): Response if ($form->isSubmitted() && $form->isValid()) { $this->em()->flush(); $msg = $this->getTranslator()->trans('translation.%name%.updated', ['%name%' => $translation->getName()]); - $this->publishConfirmMessage($request, $msg, $translation); + $this->publishConfirmMessage($request, $msg); $this->dispatchEvent(new TranslationUpdatedEvent($translation)); /* @@ -141,7 +144,7 @@ public function addAction(Request $request): Response $this->em()->flush(); $msg = $this->getTranslator()->trans('translation.%name%.created', ['%name%' => $translation->getName()]); - $this->publishConfirmMessage($request, $msg, $translation); + $this->publishConfirmMessage($request, $msg); $this->dispatchEvent(new TranslationCreatedEvent($translation)); /* @@ -181,7 +184,7 @@ public function deleteAction(Request $request, int $translationId): Response $this->em()->remove($translation); $this->em()->flush(); $msg = $this->getTranslator()->trans('translation.%name%.deleted', ['%name%' => $translation->getName()]); - $this->publishConfirmMessage($request, $msg, $translation); + $this->publishConfirmMessage($request, $msg); $this->dispatchEvent(new TranslationDeletedEvent($translation)); return $this->redirectToRoute('translationsHomePage'); diff --git a/src/Controllers/Users/UsersController.php b/src/Controllers/Users/UsersController.php index a2ded208..62732744 100644 --- a/src/Controllers/Users/UsersController.php +++ b/src/Controllers/Users/UsersController.php @@ -4,311 +4,239 @@ namespace Themes\Rozier\Controllers\Users; -use JMS\Serializer\SerializerInterface; -use RZ\Roadiz\Core\AbstractEntities\PersistableInterface; use RZ\Roadiz\CoreBundle\Entity\Role; use RZ\Roadiz\CoreBundle\Entity\User; -use Symfony\Component\Form\FormFactoryInterface; -use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Generator\UrlGeneratorInterface; -use Themes\Rozier\Controllers\AbstractAdminWithBulkController; +use Symfony\Component\Routing\Exception\ResourceNotFoundException; +use Themes\Rozier\Forms\AddUserType; use Themes\Rozier\Forms\UserDetailsType; use Themes\Rozier\Forms\UserType; +use Themes\Rozier\RozierApp; +use Themes\Rozier\Utils\SessionListFilters; use Twig\Error\RuntimeError; -class UsersController extends AbstractAdminWithBulkController +class UsersController extends RozierApp { - public function __construct( - FormFactoryInterface $formFactory, - SerializerInterface $serializer, - UrlGeneratorInterface $urlGenerator, - private readonly bool $useGravatar - ) { - parent::__construct($formFactory, $serializer, $urlGenerator); - } - - - protected function supports(PersistableInterface $item): bool - { - return $item instanceof User; - } - - protected function getNamespace(): string - { - return 'user'; - } - - protected function createEmptyItem(Request $request): User - { - $user = new User(); - $user->sendCreationConfirmationEmail(true); - return $user; - } - - protected function getTemplateFolder(): string - { - return '@RoadizRozier/users'; - } - - protected function getRequiredRole(): string - { - return 'ROLE_ACCESS_USERS'; - } - - protected function getRequiredEditionRole(): string - { - // Allow any backoffice user to access user edition before - // checking if current editing item is the same as current user. - return 'ROLE_BACKEND_USER'; - } - - protected function getRequiredDeletionRole(): string - { - return 'ROLE_ACCESS_USERS_DELETE'; - } - - protected function getEntityClass(): string - { - return User::class; - } - - protected function getFormType(): string + /** + * @param Request $request + * + * @return Response + * @throws RuntimeError + */ + public function indexAction(Request $request): Response { - return UserType::class; - } + $this->denyAccessUnlessGranted('ROLE_ACCESS_USERS'); - protected function getDefaultRouteName(): string - { - return 'usersHomePage'; - } + /* + * Manage get request to filter list + */ + $listManager = $this->createEntityListManager( + User::class, + [], + ['username' => 'ASC'] + ); + $listManager->setDisplayingNotPublishedNodes(true); + /* + * Stored in session + */ + $sessionListFilter = new SessionListFilters('user_item_per_page'); + $sessionListFilter->handleItemPerPage($request, $listManager); + $listManager->handle(); - protected function getEditRouteName(): string - { - return 'usersEditPage'; - } + $this->assignation['filters'] = $listManager->getAssignation(); + $this->assignation['users'] = $listManager->getEntities(); - protected function getBulkDeleteRouteName(): ?string - { - return 'usersBulkDeletePage'; + return $this->render('@RoadizRozier/users/list.html.twig', $this->assignation); } - protected function denyAccessUnlessItemGranted(PersistableInterface $item): void + /** + * @param Request $request + * @param int $userId + * + * @return Response + * @throws RuntimeError + */ + public function editAction(Request $request, int $userId): Response { - parent::denyAccessUnlessItemGranted($item); + $this->denyAccessUnlessGranted('ROLE_BACKEND_USER'); - if (!$item instanceof User) { - throw new \RuntimeException('Invalid item type.'); - } - $requestUser = $this->getUser(); if ( !( - $this->isGranted('ROLE_ACCESS_USERS') || - ($requestUser instanceof User && $requestUser->getId() === $item->getId()) + $this->isGranted('ROLE_ACCESS_USERS') || + ($this->getUser() instanceof User && $this->getUser()->getId() == $userId) ) ) { throw $this->createAccessDeniedException("You don't have access to this page: ROLE_ACCESS_USERS"); } - if (!$this->isGranted(Role::ROLE_SUPERADMIN) && $item->isSuperAdmin()) { + $user = $this->em()->find(User::class, $userId); + if ($user === null) { + throw new ResourceNotFoundException(); + } + if (!$this->isGranted(Role::ROLE_SUPERADMIN) && $user->isSuperAdmin()) { throw $this->createAccessDeniedException("You cannot edit a super admin."); } - } - protected function getEntityName(PersistableInterface $item): string - { - if (!$item instanceof User) { - throw new \RuntimeException('Invalid item type.'); - } - return $item->getUsername(); - } - - protected function getDefaultOrder(Request $request): array - { - return ['username' => 'ASC']; - } + $form = $this->createForm(UserType::class, $user); + $form->handleRequest($request); - protected function createUpdateEvent(PersistableInterface $item) - { - if (!$item instanceof User) { - throw new \RuntimeException('Invalid item type.'); - } - /* - * If pictureUrl is empty, use default Gravatar image. - */ - if ($item->getPictureUrl() == '' && $this->useGravatar) { - $item->setPictureUrl($item->getGravatarUrl()); + if ($form->isSubmitted() && $form->isValid()) { + $this->em()->flush(); + $msg = $this->getTranslator()->trans( + 'user.%name%.updated', + ['%name%' => $user->getUsername()] + ); + $this->publishConfirmMessage($request, $msg); + /* + * Force redirect to avoid resending form when refreshing page + */ + return $this->redirectToRoute( + 'usersEditPage', + ['userId' => $user->getId()] + ); } - return parent::createUpdateEvent($item); // TODO: Change the autogenerated stub + $this->assignation['user'] = $user; + $this->assignation['form'] = $form->createView(); + + return $this->render('@RoadizRozier/users/edit.html.twig', $this->assignation); } /** * @param Request $request - * @param int $id + * @param int $userId * * @return Response * @throws RuntimeError */ - public function editDetailsAction(Request $request, int $id): Response + public function editDetailsAction(Request $request, int $userId): Response { - $this->denyAccessUnlessGranted($this->getRequiredEditionRole()); - $this->additionalAssignation($request); + $this->denyAccessUnlessGranted('ROLE_BACKEND_USER'); - /** @var mixed|object|null $item */ - $item = $this->em()->find($this->getEntityClass(), $id); - if (!($item instanceof PersistableInterface)) { - throw $this->createNotFoundException(); + if ( + !( + $this->isGranted('ROLE_ACCESS_USERS') || + ($this->getUser() instanceof User && $this->getUser()->getId() === $userId) + ) + ) { + throw $this->createAccessDeniedException("You don't have access to this page: ROLE_ACCESS_USERS"); } + $user = $this->em()->find(User::class, $userId); - $this->prepareWorkingItem($item); - $this->denyAccessUnlessItemGranted($item); + if ($user === null) { + throw new ResourceNotFoundException(); + } + if (!$this->isGranted(Role::ROLE_SUPERADMIN) && $user->isSuperAdmin()) { + throw $this->createAccessDeniedException("You cannot edit a super admin."); + } - $form = $this->createForm(UserDetailsType::class, $item); + $form = $this->createForm(UserDetailsType::class, $user); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { /* - * Events are dispatched before entity manager is flushed - * to be able to throw exceptions before it is persisted. + * If pictureUrl is empty, use default Gravatar image. */ - $event = $this->createUpdateEvent($item); - $this->dispatchSingleOrMultipleEvent($event); - $this->em()->flush(); + if ($user->getPictureUrl() == '') { + $user->setPictureUrl($user->getGravatarUrl()); + } - /* - * Event that requires that EM is flushed - */ - $postEvent = $this->createPostUpdateEvent($item); - $this->dispatchSingleOrMultipleEvent($postEvent); + $this->em()->flush(); $msg = $this->getTranslator()->trans( - '%namespace%.%item%.was_updated', - [ - '%item%' => $this->getEntityName($item), - '%namespace%' => $this->getTranslator()->trans($this->getNamespace()) - ] + 'user.%name%.updated', + ['%name%' => $user->getUsername()] ); - $this->publishConfirmMessage($request, $msg, $item); + $this->publishConfirmMessage($request, $msg); /* * Force redirect to avoid resending form when refreshing page */ return $this->redirectToRoute( 'usersEditDetailsPage', - ['id' => $item->getId()] + ['userId' => $user->getId()] ); } + $this->assignation['user'] = $user; $this->assignation['form'] = $form->createView(); - $this->assignation['item'] = $item; - return $this->render( - $this->getTemplateFolder() . '/editDetails.html.twig', - $this->assignation, - null, - $this->getTemplateNamespace() - ); + return $this->render('@RoadizRozier/users/editDetails.html.twig', $this->assignation); } + /** + * @param Request $request + * + * @return Response + * @throws RuntimeError + */ + public function addAction(Request $request): Response + { + $this->denyAccessUnlessGranted('ROLE_ACCESS_USERS'); + + $user = new User(); + $user->sendCreationConfirmationEmail(true); + $this->assignation['user'] = $user; + $form = $this->createForm(AddUserType::class, $user); + $form->handleRequest($request); - protected function additionalAssignation(Request $request): void - { - parent::additionalAssignation($request); + if ($form->isSubmitted() && $form->isValid()) { + $this->em()->persist($user); + $this->em()->flush(); - if (null !== $this->getBulkEnableRouteName()) { - $bulkEnableForm = $this->createEnableBulkForm(true); - $this->assignation['bulkEnableForm'] = $bulkEnableForm->createView(); - $this->assignation['hasBulkActions'] = true; - } + $msg = $this->getTranslator()->trans('user.%name%.created', ['%name%' => $user->getUsername()]); + $this->publishConfirmMessage($request, $msg); - if (null !== $this->getBulkDisableRouteName()) { - $bulkDisableForm = $this->createDisableBulkForm(true); - $this->assignation['bulkDisableForm'] = $bulkDisableForm->createView(); - $this->assignation['hasBulkActions'] = true; + return $this->redirectToRoute('usersHomePage'); } - } - /* - * User specific bulk actions - */ + $this->assignation['form'] = $form->createView(); - protected function createEnableBulkForm(bool $get = false, ?array $data = null): FormInterface - { - return $this->createBulkForm( - $this->getBulkEnableRouteName(), - 'bulk-enable', - $get, - $data - ); + return $this->render('@RoadizRozier/users/add.html.twig', $this->assignation); } - protected function createDisableBulkForm(bool $get = false, ?array $data = null): FormInterface + /** + * @param Request $request + * @param int $userId + * + * @return Response + * @throws RuntimeError + */ + public function deleteAction(Request $request, int $userId): Response { - return $this->createBulkForm( - $this->getBulkDisableRouteName(), - 'bulk-disable', - $get, - $data - ); - } + $this->denyAccessUnlessGranted('ROLE_ACCESS_USERS_DELETE'); + $user = $this->em()->find(User::class, (int) $userId); - private function getBulkEnableRouteName(): string - { - return 'usersBulkEnablePage'; - } + if ($user === null) { + throw new ResourceNotFoundException(); + } - private function getBulkDisableRouteName(): string - { - return 'usersBulkDisablePage'; - } + if (!$this->isGranted(Role::ROLE_SUPERADMIN) && $user->isSuperAdmin()) { + throw $this->createAccessDeniedException("You cannot edit a super admin."); + } - public function bulkEnableAction(Request $request): Response - { - return $this->bulkAction( - $request, - $this->getRequiredRole(), - $this->createEnableBulkForm(true), - $this->createEnableBulkForm(), - function (string $ids) { - return $this->createEnableBulkForm(false, [ - 'id' => $ids, - ]); - }, - $this->getTemplateFolder() . '/bulk_enable.html.twig', - '%namespace%.%item%.was_enabled', - function (PersistableInterface $item) { - if (!$item instanceof User) { - throw new \RuntimeException('Invalid item type.'); - } - $item->setEnabled(true); - }, - 'bulkEnableForm' - ); - } + $form = $this->createForm(FormType::class); + $form->handleRequest($request); - public function bulkDisableAction(Request $request): Response - { - return $this->bulkAction( - $request, - $this->getRequiredRole(), - $this->createDisableBulkForm(true), - $this->createDisableBulkForm(), - function (string $ids) { - return $this->createDisableBulkForm(false, [ - 'id' => $ids, - ]); - }, - $this->getTemplateFolder() . '/bulk_disable.html.twig', - '%namespace%.%item%.was_disabled', - function (PersistableInterface $item) { - if (!$item instanceof User) { - throw new \RuntimeException('Invalid item type.'); - } - $item->setEnabled(false); - }, - 'bulkDisableForm' - ); + if ($form->isSubmitted() && $form->isValid()) { + $this->em()->remove($user); + $this->em()->flush(); + $msg = $this->getTranslator()->trans( + 'user.%name%.deleted', + ['%name%' => $user->getUsername()] + ); + $this->publishConfirmMessage($request, $msg); + /* + * Force redirect to avoid resending form when refreshing page + */ + return $this->redirectToRoute('usersHomePage'); + } + + $this->assignation['user'] = $user; + $this->assignation['form'] = $form->createView(); + + return $this->render('@RoadizRozier/users/delete.html.twig', $this->assignation); } } diff --git a/src/Controllers/Users/UsersGroupsController.php b/src/Controllers/Users/UsersGroupsController.php index 62103ffc..4c5ba264 100644 --- a/src/Controllers/Users/UsersGroupsController.php +++ b/src/Controllers/Users/UsersGroupsController.php @@ -27,52 +27,53 @@ public function editGroupsAction(Request $request, int $userId): Response /** @var User|null $user */ $user = $this->em()->find(User::class, $userId); - if ($user === null) { - throw new ResourceNotFoundException(); - } - - $this->assignation['user'] = $user; - - $form = $this->buildEditGroupsForm($user); - $form->handleRequest($request); - - if ($form->isSubmitted() && $form->isValid()) { - $data = $form->getData(); - if ($data['userId'] == $user->getId()) { - if (array_key_exists('group', $data) && $data['group'][0] instanceof Group) { - $group = $data['group'][0]; - } elseif (array_key_exists('group', $data) && is_numeric($data['group'])) { - $group = $this->em()->find(Group::class, $data['group']); - } else { - $group = null; - } - if ($group !== null) { - $user->addGroup($group); - $this->em()->flush(); - - $this->dispatchEvent(new UserJoinedGroupEvent($user, $group)); - - $msg = $this->getTranslator()->trans('user.%user%.group.%group%.linked', [ - '%user%' => $user->getUserName(), - '%group%' => $group->getName(), - ]); - $this->publishConfirmMessage($request, $msg, $user); - - /* - * Force redirect to avoid resending form when refreshing page - */ - return $this->redirectToRoute( - 'usersEditGroupsPage', - ['userId' => $user->getId()] - ); + if ($user !== null) { + $this->assignation['user'] = $user; + + $form = $this->buildEditGroupsForm($user); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $data = $form->getData(); + if ($data['userId'] == $user->getId()) { + if (array_key_exists('group', $data) && $data['group'][0] instanceof Group) { + $group = $data['group'][0]; + } elseif (array_key_exists('group', $data) && is_numeric($data['group'])) { + $group = $this->em()->find(Group::class, $data['group']); + } else { + $group = null; + } + + if ($group !== null) { + $user->addGroup($group); + $this->em()->flush(); + + $this->dispatchEvent(new UserJoinedGroupEvent($user, $group)); + + $msg = $this->getTranslator()->trans('user.%user%.group.%group%.linked', [ + '%user%' => $user->getUserName(), + '%group%' => $group->getName(), + ]); + $this->publishConfirmMessage($request, $msg); + + /* + * Force redirect to avoid resending form when refreshing page + */ + return $this->redirectToRoute( + 'usersEditGroupsPage', + ['userId' => $user->getId()] + ); + } } } - } - $this->assignation['form'] = $form->createView(); + $this->assignation['form'] = $form->createView(); + + return $this->render('@RoadizRozier/users/groups.html.twig', $this->assignation); + } - return $this->render('@RoadizRozier/users/groups.html.twig', $this->assignation); + throw new ResourceNotFoundException(); } public function removeGroupAction(Request $request, int $userId, int $groupId): Response @@ -84,47 +85,44 @@ public function removeGroupAction(Request $request, int $userId, int $groupId): /** @var Group|null $group */ $group = $this->em()->find(Group::class, $groupId); - if ($user === null) { - throw new ResourceNotFoundException(); - } - if ($group === null) { - throw new ResourceNotFoundException(); - } - if (!$this->isGranted($group)) { throw $this->createAccessDeniedException(); } - $this->assignation['user'] = $user; - $this->assignation['group'] = $group; + if ($user !== null) { + $this->assignation['user'] = $user; + $this->assignation['group'] = $group; - $form = $this->createForm(FormType::class); - $form->handleRequest($request); + $form = $this->createForm(FormType::class); + $form->handleRequest($request); - if ($form->isSubmitted() && $form->isValid()) { - $user->removeGroup($group); - $this->em()->flush(); + if ($form->isSubmitted() && $form->isValid()) { + $user->removeGroup($group); + $this->em()->flush(); - $this->dispatchEvent(new UserLeavedGroupEvent($user, $group)); + $this->dispatchEvent(new UserLeavedGroupEvent($user, $group)); - $msg = $this->getTranslator()->trans('user.%user%.group.%group%.removed', [ - '%user%' => $user->getUserName(), - '%group%' => $group->getName(), - ]); - $this->publishConfirmMessage($request, $msg, $user); + $msg = $this->getTranslator()->trans('user.%user%.group.%group%.removed', [ + '%user%' => $user->getUserName(), + '%group%' => $group->getName(), + ]); + $this->publishConfirmMessage($request, $msg); - /* - * Force redirect to avoid resending form when refreshing page - */ - return $this->redirectToRoute( - 'usersEditGroupsPage', - ['userId' => $user->getId()] - ); - } + /* + * Force redirect to avoid resending form when refreshing page + */ + return $this->redirectToRoute( + 'usersEditGroupsPage', + ['userId' => $user->getId()] + ); + } + + $this->assignation['form'] = $form->createView(); - $this->assignation['form'] = $form->createView(); + return $this->render('@RoadizRozier/users/removeGroup.html.twig', $this->assignation); + } - return $this->render('@RoadizRozier/users/removeGroup.html.twig', $this->assignation); + throw new ResourceNotFoundException(); } /** diff --git a/src/Controllers/Users/UsersRolesController.php b/src/Controllers/Users/UsersRolesController.php index ba80f0af..04b0a57a 100644 --- a/src/Controllers/Users/UsersRolesController.php +++ b/src/Controllers/Users/UsersRolesController.php @@ -53,7 +53,7 @@ public function editRolesAction(Request $request, int $userId): Response '%role%' => $role->getRole(), ]); - $this->publishConfirmMessage($request, $msg, $user); + $this->publishConfirmMessage($request, $msg); /* * Force redirect to avoid resending form when refreshing page @@ -114,7 +114,7 @@ public function removeRoleAction(Request $request, int $userId, int $roleId): Re 'user.%name%.role_removed', ['%name%' => $role->getRole()] ); - $this->publishConfirmMessage($request, $msg, $user); + $this->publishConfirmMessage($request, $msg); /* * Force redirect to avoid resending form when refreshing page diff --git a/src/Controllers/Users/UsersSecurityController.php b/src/Controllers/Users/UsersSecurityController.php index 35949690..27c178c4 100644 --- a/src/Controllers/Users/UsersSecurityController.php +++ b/src/Controllers/Users/UsersSecurityController.php @@ -45,7 +45,7 @@ public function securityAction(Request $request, int $userId): Response ['%name%' => $user->getUsername()] ); - $this->publishConfirmMessage($request, $msg, $user); + $this->publishConfirmMessage($request, $msg); /* * Force redirect to avoid resending form when refreshing page diff --git a/src/Controllers/WebhookController.php b/src/Controllers/WebhookController.php index 60bccc70..1c60d4ce 100644 --- a/src/Controllers/WebhookController.php +++ b/src/Controllers/WebhookController.php @@ -12,20 +12,21 @@ use RZ\Roadiz\CoreBundle\Webhook\WebhookDispatcher; use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\FormError; -use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; -final class WebhookController extends AbstractAdminWithBulkController +final class WebhookController extends AbstractAdminController { + private WebhookDispatcher $webhookDispatcher; + public function __construct( - private readonly WebhookDispatcher $webhookDispatcher, - FormFactoryInterface $formFactory, + WebhookDispatcher $webhookDispatcher, SerializerInterface $serializer, UrlGeneratorInterface $urlGenerator ) { - parent::__construct($formFactory, $serializer, $urlGenerator); + parent::__construct($serializer, $urlGenerator); + $this->webhookDispatcher = $webhookDispatcher; } public function triggerAction(Request $request, string $id): Response @@ -56,7 +57,7 @@ public function triggerAction(Request $request, string $id): Response '%seconds%' => $item->getThrottleSeconds(), ] ); - $this->publishConfirmMessage($request, $msg, $item); + $this->publishConfirmMessage($request, $msg); return $this->redirect($this->urlGenerator->generate( $this->getDefaultRouteName(), @@ -132,8 +133,4 @@ protected function getEntityName(PersistableInterface $item): string } return ''; } - protected function getBulkDeleteRouteName(): ?string - { - return 'webhooksBulkDeletePage'; - } } diff --git a/src/Event/UserActionsMenuEvent.php b/src/Event/UserActionsMenuEvent.php deleted file mode 100644 index f5284c61..00000000 --- a/src/Event/UserActionsMenuEvent.php +++ /dev/null @@ -1,29 +0,0 @@ -actions[] = [ - 'label' => $label, - 'path' => $path, - 'icon' => $icon, - ]; - } - - public function getActions(): array - { - return $this->actions; - } -} diff --git a/src/Explorer/ConfigurableExplorerItem.php b/src/Explorer/ConfigurableExplorerItem.php index a18fe152..4fb67bfa 100644 --- a/src/Explorer/ConfigurableExplorerItem.php +++ b/src/Explorer/ConfigurableExplorerItem.php @@ -17,14 +17,27 @@ final class ConfigurableExplorerItem extends AbstractExplorerItem { + private PersistableInterface $entity; + private array $configuration; + private RendererInterface $renderer; + private DocumentUrlGeneratorInterface $documentUrlGenerator; + private UrlGeneratorInterface $urlGenerator; + private ?EmbedFinderFactory $embedFinderFactory; + public function __construct( - private readonly PersistableInterface $entity, - private readonly array $configuration, - private readonly RendererInterface $renderer, - private readonly DocumentUrlGeneratorInterface $documentUrlGenerator, - private readonly UrlGeneratorInterface $urlGenerator, - private readonly ?EmbedFinderFactory $embedFinderFactory = null + PersistableInterface $entity, + array &$configuration, + RendererInterface $renderer, + DocumentUrlGeneratorInterface $documentUrlGenerator, + UrlGeneratorInterface $urlGenerator, + ?EmbedFinderFactory $embedFinderFactory = null ) { + $this->entity = $entity; + $this->configuration = $configuration; + $this->renderer = $renderer; + $this->documentUrlGenerator = $documentUrlGenerator; + $this->urlGenerator = $urlGenerator; + $this->embedFinderFactory = $embedFinderFactory; } /** @@ -42,12 +55,9 @@ public function getAlternativeDisplayable(): ?string { $alt = $this->configuration['classname']; if (!empty($this->configuration['alt_displayable'])) { - $altDisplayableCallable = [$this->entity, $this->configuration['alt_displayable']]; - if (\is_callable($altDisplayableCallable)) { - $alt = call_user_func($altDisplayableCallable); - if ($alt instanceof \DateTimeInterface) { - $alt = $alt->format('c'); - } + $alt = call_user_func([$this->entity, $this->configuration['alt_displayable']]); + if ($alt instanceof \DateTimeInterface) { + $alt = $alt->format('c'); } } return (new UnicodeString($alt ?? ''))->truncate(30, '…')->toString(); @@ -58,12 +68,9 @@ public function getAlternativeDisplayable(): ?string */ public function getDisplayable(): string { - $displayableCallable = [$this->entity, $this->configuration['displayable']]; - if (\is_callable($displayableCallable)) { - $displayable = call_user_func($displayableCallable); - if ($displayable instanceof \DateTimeInterface) { - $displayable = $displayable->format('c'); - } + $displayable = call_user_func([$this->entity, $this->configuration['displayable']]); + if ($displayable instanceof \DateTimeInterface) { + $displayable = $displayable->format('c'); } return (new UnicodeString($displayable ?? ''))->truncate(30, '…')->toString(); } @@ -81,14 +88,11 @@ protected function getThumbnail(): ?array /** @var DocumentInterface|null $thumbnail */ $thumbnail = null; if (!empty($this->configuration['thumbnail'])) { - $thumbnailCallable = [$this->entity, $this->configuration['thumbnail']]; - if (\is_callable($thumbnailCallable)) { - $thumbnail = call_user_func($thumbnailCallable); - if ($thumbnail instanceof Collection && $thumbnail->count() > 0 && $thumbnail->first() instanceof DocumentInterface) { - $thumbnail = $thumbnail->first(); - } elseif (is_array($thumbnail) && count($thumbnail) > 0 && $thumbnail[0] instanceof DocumentInterface) { - $thumbnail = $thumbnail[0]; - } + $thumbnail = call_user_func([$this->entity, $this->configuration['thumbnail']]); + if ($thumbnail instanceof Collection && $thumbnail->count() > 0 && $thumbnail->first() instanceof DocumentInterface) { + $thumbnail = $thumbnail->first(); + } elseif (is_array($thumbnail) && count($thumbnail) > 0 && $thumbnail[0] instanceof DocumentInterface) { + $thumbnail = $thumbnail[0]; } } diff --git a/src/Explorer/FolderExplorerItem.php b/src/Explorer/FolderExplorerItem.php index f2ffe508..10588ae0 100644 --- a/src/Explorer/FolderExplorerItem.php +++ b/src/Explorer/FolderExplorerItem.php @@ -10,10 +10,13 @@ final class FolderExplorerItem extends AbstractExplorerItem { - public function __construct( - private readonly Folder $folder, - private readonly UrlGeneratorInterface $urlGenerator - ) { + private Folder $folder; + private UrlGeneratorInterface $urlGenerator; + + public function __construct(Folder $folder, UrlGeneratorInterface $urlGenerator) + { + $this->folder = $folder; + $this->urlGenerator = $urlGenerator; } /** @@ -32,9 +35,7 @@ public function getAlternativeDisplayable(): ?string /** @var Folder|null $parent */ $parent = $this->folder->getParent(); if (null !== $parent) { - return $parent->getTranslatedFolders()->first() ? - $parent->getTranslatedFolders()->first()->getName() : - $parent->getName(); + return $parent->getTranslatedFolders()->first()->getName(); } return ''; } @@ -44,9 +45,7 @@ public function getAlternativeDisplayable(): ?string */ public function getDisplayable(): string { - return $this->folder->getTranslatedFolders()->first() ? - $this->folder->getTranslatedFolders()->first()->getName() : - $this->folder->getName(); + return $this->folder->getTranslatedFolders()->first()->getName(); } /** diff --git a/src/Explorer/SettingExplorerItem.php b/src/Explorer/SettingExplorerItem.php index 9145f861..5c0fd190 100644 --- a/src/Explorer/SettingExplorerItem.php +++ b/src/Explorer/SettingExplorerItem.php @@ -10,10 +10,13 @@ final class SettingExplorerItem extends AbstractExplorerItem { - public function __construct( - private readonly Setting $setting, - private readonly UrlGeneratorInterface $urlGenerator - ) { + private Setting $setting; + private UrlGeneratorInterface $urlGenerator; + + public function __construct(Setting $setting, UrlGeneratorInterface $urlGenerator) + { + $this->setting = $setting; + $this->urlGenerator = $urlGenerator; } /** diff --git a/src/Explorer/UserExplorerItem.php b/src/Explorer/UserExplorerItem.php index 60b1a32a..eb63dc4c 100644 --- a/src/Explorer/UserExplorerItem.php +++ b/src/Explorer/UserExplorerItem.php @@ -10,10 +10,13 @@ final class UserExplorerItem extends AbstractExplorerItem { - public function __construct( - private readonly User $user, - private readonly UrlGeneratorInterface $urlGenerator - ) { + private User $user; + private UrlGeneratorInterface $urlGenerator; + + public function __construct(User $user, UrlGeneratorInterface $urlGenerator) + { + $this->user = $user; + $this->urlGenerator = $urlGenerator; } /** @@ -59,7 +62,7 @@ public function getOriginal(): User protected function getEditItemPath(): ?string { return $this->urlGenerator->generate('usersEditPage', [ - 'id' => $this->user->getId() + 'userId' => $this->user->getId() ]); } } diff --git a/src/Forms/AddUserType.php b/src/Forms/AddUserType.php index 2cbff0d7..debb57c3 100644 --- a/src/Forms/AddUserType.php +++ b/src/Forms/AddUserType.php @@ -7,6 +7,9 @@ use RZ\Roadiz\CoreBundle\Form\GroupsType; use Symfony\Component\Form\FormBuilderInterface; +/** + * @package Themes\Rozier\Forms + */ class AddUserType extends UserType { public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Forms/CustomFormFieldType.php b/src/Forms/CustomFormFieldType.php index f0e25cd8..998ad6f6 100644 --- a/src/Forms/CustomFormFieldType.php +++ b/src/Forms/CustomFormFieldType.php @@ -5,33 +5,19 @@ namespace Themes\Rozier\Forms; use RZ\Roadiz\CoreBundle\Entity\CustomFormField; -use RZ\Roadiz\CoreBundle\Form\DataListTextType; use RZ\Roadiz\CoreBundle\Form\MarkdownType; -use RZ\Roadiz\CoreBundle\Repository\CustomFormFieldRepository; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; -use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @package Themes\Rozier\Forms + */ class CustomFormFieldType extends AbstractType { - public function __construct( - private readonly CustomFormFieldRepository $customFormFieldRepository - ) { - } - - /** - * @param CustomFormField $field - * @return string[] - */ - protected function getAllGroupsNames(CustomFormField $field): array - { - return $this->customFormFieldRepository->findDistinctGroupNamesInCustomForm($field->getCustomForm()); - } - public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->add('label', TextType::class, [ @@ -64,60 +50,19 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ]) ->add( 'defaultValues', - TextareaType::class, + TextType::class, [ - 'label' => 'customFormField.defaultValues', + 'label' => 'defaultValues', 'required' => false, 'attr' => [ 'placeholder' => 'enter_values_comma_separated', ], ] ) - ->add('groupName', DataListTextType::class, [ + ->add('groupName', TextType::class, [ 'label' => 'groupName', 'required' => false, 'help' => 'use_the_same_group_names_over_fields_to_gather_them_in_tabs', - 'list' => $this->getAllGroupsNames($builder->getData()), - 'listName' => 'group-names', - 'attr' => [ - 'autocomplete' => 'off', - ], - ]) - ->add('autocomplete', ChoiceType::class, [ - 'label' => 'customForm.autocomplete', - 'help' => 'customForm.autocomplete.help', - 'choices' => [ - 'off', - 'name', - 'honorific-prefix', - 'honorific-suffix', - 'given-name', - 'additional-name', - 'family-name', - 'nickname', - 'email', - 'username', - 'organization-title', - 'organization', - 'street-address', - 'country', - 'country-name', - 'postal-code', - 'bday', - 'bday-day', - 'bday-month', - 'bday-year', - 'sex', - 'tel', - 'tel-national', - 'url', - 'photo', - ], - 'placeholder' => 'autocomplete.no_autocomplete', - 'choice_label' => function ($choice, $key, $value) { - return 'autocomplete.' . $value; - }, - 'required' => false, ]); } diff --git a/src/Forms/CustomFormType.php b/src/Forms/CustomFormType.php deleted file mode 100644 index 57858f3b..00000000 --- a/src/Forms/CustomFormType.php +++ /dev/null @@ -1,121 +0,0 @@ -security = $security; - } - - public function buildForm(FormBuilderInterface $builder, array $options): void - { - $builder->add('displayName', TextType::class, [ - 'label' => 'customForm.displayName', - 'empty_data' => '', - ]) - ->add('description', MarkdownType::class, [ - 'label' => 'description', - 'required' => false, - ]) - ->add('email', TextType::class, [ - 'label' => 'email', - 'required' => false, - 'constraints' => [ - new Callback(function ($value, ExecutionContextInterface $context) { - $emails = array_filter( - array_map('trim', explode(',', $value ?? '')) - ); - foreach ($emails as $email) { - if (false === filter_var($email, FILTER_VALIDATE_EMAIL)) { - $context->buildViolation('{{ value }} is not a valid email address.') - ->setParameter('{{ value }}', $email) - ->setCode(Email::INVALID_FORMAT_ERROR) - ->addViolation(); - } - } - }), - ], - ]) - ; - if ($this->security->isGranted('ROLE_ACCESS_CUSTOMFORMS_RETENTION')) { - $builder->add('retentionTime', ChoiceType::class, [ - 'label' => 'customForm.retentionTime', - 'help' => 'customForm.retentionTime.help', - 'required' => false, - 'placeholder' => 'customForm.retentionTime.always', - 'choices' => [ - 'customForm.retentionTime.one_week' => 'P7D', - 'customForm.retentionTime.two_weeks' => 'P14D', - 'customForm.retentionTime.one_month' => 'P1M', - 'customForm.retentionTime.three_months' => 'P3M', - 'customForm.retentionTime.six_months' => 'P6M', - 'customForm.retentionTime.one_year' => 'P1Y', - 'customForm.retentionTime.two_years' => 'P2Y', - ] - ]); - } - $builder->add('open', CheckboxType::class, [ - 'label' => 'customForm.open', - 'required' => false, - ]) - ->add('closeDate', DateTimeType::class, [ - 'label' => 'customForm.closeDate', - 'required' => false, - 'date_widget' => 'single_text', - 'date_format' => 'yyyy-MM-dd', - 'attr' => [ - 'class' => 'rz-datetime-field', - ], - 'placeholder' => [ - 'hour' => 'hour', - 'minute' => 'minute', - ], - ]) - ->add('color', ColorType::class, [ - 'label' => 'customForm.color', - 'required' => false, - ]); - } - - public function getBlockPrefix(): string - { - return 'customform'; - } - - public function configureOptions(OptionsResolver $resolver): void - { - $resolver->setDefaults([ - 'label' => false, - 'name' => '', - 'data_class' => CustomForm::class, - 'attr' => [ - 'class' => 'uk-form custom-form-form', - ], - ]); - $resolver->setAllowedTypes('name', 'string'); - } -} diff --git a/src/Forms/DataTransformer/TagTransformer.php b/src/Forms/DataTransformer/TagTransformer.php index f90162b2..a5519023 100644 --- a/src/Forms/DataTransformer/TagTransformer.php +++ b/src/Forms/DataTransformer/TagTransformer.php @@ -4,11 +4,16 @@ namespace Themes\Rozier\Forms\DataTransformer; +use Doctrine\Common\Collections\Collection; use Doctrine\Persistence\ObjectManager; +use RZ\Roadiz\Core\AbstractEntities\AbstractEntity; use RZ\Roadiz\CoreBundle\Entity\Tag; use Symfony\Component\Form\DataTransformerInterface; use Symfony\Component\Form\Exception\TransformationFailedException; +/** + * @package Themes\Rozier\Forms\DataTransformer + */ class TagTransformer implements DataTransformerInterface { private ObjectManager $manager; diff --git a/src/Forms/DocumentEditType.php b/src/Forms/DocumentEditType.php index b016ee93..09bf4c1b 100644 --- a/src/Forms/DocumentEditType.php +++ b/src/Forms/DocumentEditType.php @@ -17,7 +17,7 @@ use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\Security\Core\Security; use Symfony\Component\Validator\Constraints\File; use Symfony\Component\Validator\Constraints\LessThanOrEqual; use Symfony\Component\Validator\Constraints\NotBlank; @@ -118,14 +118,6 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ]); } - if ($document->isProcessable()) { - $builder->add('imageCropAlignment', ImageCropAlignmentType::class, [ - 'label' => 'document.imageCropAlignment', - 'help' => 'document.imageCropAlignment.help', - 'required' => false, - ]); - } - /* * Display thumbnails only if current Document is original. */ diff --git a/src/Forms/DocumentTranslationType.php b/src/Forms/DocumentTranslationType.php index ea1fd10a..946c5821 100644 --- a/src/Forms/DocumentTranslationType.php +++ b/src/Forms/DocumentTranslationType.php @@ -4,8 +4,8 @@ namespace Themes\Rozier\Forms; -use RZ\Roadiz\CoreBundle\Entity\DocumentTranslation; use RZ\Roadiz\CoreBundle\Form\MarkdownType; +use RZ\Roadiz\CoreBundle\Entity\DocumentTranslation; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\HiddenType; use Symfony\Component\Form\Extension\Core\Type\TextType; @@ -29,11 +29,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'required' => false, ]) ->add('copyright', TextType::class, [ - 'label' => 'document.copyrightHolder', - 'required' => false, - ]) - ->add('externalUrl', TextType::class, [ - 'label' => 'document.externalUrl', + 'label' => 'copyright', 'required' => false, ]); } diff --git a/src/Forms/DynamicType.php b/src/Forms/DynamicType.php index dbaf6999..3a65e897 100644 --- a/src/Forms/DynamicType.php +++ b/src/Forms/DynamicType.php @@ -9,6 +9,11 @@ use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * Class DynamicType + * + * @package Themes\Rozier\Forms + */ class DynamicType extends AbstractType { /** diff --git a/src/Forms/FolderCollectionType.php b/src/Forms/FolderCollectionType.php index c580bf84..aa1db351 100644 --- a/src/Forms/FolderCollectionType.php +++ b/src/Forms/FolderCollectionType.php @@ -17,8 +17,14 @@ final class FolderCollectionType extends AbstractType { - public function __construct(private readonly ManagerRegistry $managerRegistry) + protected ManagerRegistry $managerRegistry; + + /** + * @param ManagerRegistry $managerRegistry + */ + public function __construct(ManagerRegistry $managerRegistry) { + $this->managerRegistry = $managerRegistry; } /** diff --git a/src/Forms/FolderTranslationType.php b/src/Forms/FolderTranslationType.php index ba030249..16b25967 100644 --- a/src/Forms/FolderTranslationType.php +++ b/src/Forms/FolderTranslationType.php @@ -10,6 +10,9 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @package Themes\Rozier\Forms + */ class FolderTranslationType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Forms/FolderType.php b/src/Forms/FolderType.php index d05469dc..83ab4f95 100644 --- a/src/Forms/FolderType.php +++ b/src/Forms/FolderType.php @@ -12,6 +12,9 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @package Themes\Rozier\Forms + */ class FolderType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Forms/ImageCropAlignmentType.php b/src/Forms/ImageCropAlignmentType.php deleted file mode 100644 index 5a79bd2d..00000000 --- a/src/Forms/ImageCropAlignmentType.php +++ /dev/null @@ -1,44 +0,0 @@ -setDefaults([ - 'label' => 'image_crop_alignment', - 'required' => false, - 'placeholder' => 'image_crop_alignment.none', - 'expanded' => true, - 'choices' => [ - 'image_crop_alignment.top-left' => 'top-left', - 'image_crop_alignment.top' => 'top', - 'image_crop_alignment.top-right' => 'top-right', - 'image_crop_alignment.left' => 'left', - 'image_crop_alignment.center' => 'center', - 'image_crop_alignment.right' => 'right', - 'image_crop_alignment.bottom-left' => 'bottom-left', - 'image_crop_alignment.bottom' => 'bottom', - 'image_crop_alignment.bottom-right' => 'bottom-right', - ] - ]); - } - - public function getBlockPrefix(): string - { - return 'image_crop_alignment'; - } - - - public function getParent(): string - { - return ChoiceType::class; - } -} diff --git a/src/Forms/LoginType.php b/src/Forms/LoginType.php index 3f88811c..37b0804e 100644 --- a/src/Forms/LoginType.php +++ b/src/Forms/LoginType.php @@ -65,7 +65,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ], ]); - if ($this->requestStack->getMainRequest()?->query->has('_home')) { + if ($this->requestStack->getMasterRequest()->query->has('_home')) { $builder->add('_target_path', HiddenType::class, [ 'data' => $this->urlGenerator->generate('adminHomePage') ]); diff --git a/src/Forms/Node/AddNodeType.php b/src/Forms/Node/AddNodeType.php index ffc863f6..336d4dea 100644 --- a/src/Forms/Node/AddNodeType.php +++ b/src/Forms/Node/AddNodeType.php @@ -9,17 +9,22 @@ use RZ\Roadiz\CoreBundle\Form\DataTransformer\NodeTypeTransformer; use RZ\Roadiz\CoreBundle\Form\NodeTypesType; use Symfony\Component\Form\AbstractType; -use Symfony\Component\Form\Event\SubmitEvent; +use Symfony\Component\Form\CallbackTransformer; +use Symfony\Component\Form\Event\PostSubmitEvent; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\Form\FormEvents; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\NotNull; +use Symfony\Component\Form\FormEvents; +use Symfony\Component\Form\Event\SubmitEvent; +/** + * @package Themes\Rozier\Forms\Node + */ class AddNodeType extends AbstractType { protected ManagerRegistry $managerRegistry; diff --git a/src/Forms/NodeSource/NodeSourceCustomFormType.php b/src/Forms/NodeSource/NodeSourceCustomFormType.php index be552197..b46f3a90 100644 --- a/src/Forms/NodeSource/NodeSourceCustomFormType.php +++ b/src/Forms/NodeSource/NodeSourceCustomFormType.php @@ -19,11 +19,16 @@ */ final class NodeSourceCustomFormType extends AbstractNodeSourceFieldType { - public function __construct( - ManagerRegistry $managerRegistry, - private readonly NodeHandler $nodeHandler - ) { + protected NodeHandler $nodeHandler; + + /** + * @param ManagerRegistry $managerRegistry + * @param NodeHandler $nodeHandler + */ + public function __construct(ManagerRegistry $managerRegistry, NodeHandler $nodeHandler) + { parent::__construct($managerRegistry); + $this->nodeHandler = $nodeHandler; } /** @@ -80,7 +85,7 @@ public function onPreSetData(FormEvent $event): void $event->setData($this->managerRegistry ->getRepository(CustomForm::class) - ->findByNodeAndFieldName($nodeSource->getNode(), $nodeTypeField->getName())); + ->findByNodeAndField($nodeSource->getNode(), $nodeTypeField)); } /** diff --git a/src/Forms/NodeSource/NodeSourceDocumentType.php b/src/Forms/NodeSource/NodeSourceDocumentType.php index efcce674..0278872f 100644 --- a/src/Forms/NodeSource/NodeSourceDocumentType.php +++ b/src/Forms/NodeSource/NodeSourceDocumentType.php @@ -19,11 +19,16 @@ */ final class NodeSourceDocumentType extends AbstractNodeSourceFieldType { - public function __construct( - ManagerRegistry $managerRegistry, - private readonly NodesSourcesHandler $nodesSourcesHandler - ) { + protected NodesSourcesHandler $nodesSourcesHandler; + + /** + * @param ManagerRegistry $managerRegistry + * @param NodesSourcesHandler $nodesSourcesHandler + */ + public function __construct(ManagerRegistry $managerRegistry, NodesSourcesHandler $nodesSourcesHandler) + { parent::__construct($managerRegistry); + $this->nodesSourcesHandler = $nodesSourcesHandler; } /** @@ -83,9 +88,9 @@ public function onPreSetData(FormEvent $event): void $event->setData($this->managerRegistry ->getRepository(Document::class) - ->findByNodeSourceAndFieldName( + ->findByNodeSourceAndField( $nodeSource, - $nodeTypeField->getName() + $nodeTypeField )); } diff --git a/src/Forms/NodeSource/NodeSourceJoinType.php b/src/Forms/NodeSource/NodeSourceJoinType.php index b7810c44..27b90477 100644 --- a/src/Forms/NodeSource/NodeSourceJoinType.php +++ b/src/Forms/NodeSource/NodeSourceJoinType.php @@ -55,9 +55,8 @@ public function buildView(FormView $view, FormInterface $form, array $options): $configuration = $this->getFieldConfiguration($options); $displayableData = []; - /** @var callable $callable */ - $callable = [$options['nodeSource'], $options['nodeTypeField']->getGetterName()]; - $entities = call_user_func($callable); + + $entities = call_user_func([$options['nodeSource'], $options['nodeTypeField']->getGetterName()]); if ($entities instanceof \Traversable) { /** @var PersistableInterface $entity */ @@ -69,9 +68,8 @@ public function buildView(FormView $view, FormInterface $form, array $options): 'id' => $entity->getId(), 'classname' => $configuration['classname'], ]; - $displayableCallable = [$entity, $configuration['displayable']]; - if (\is_callable($displayableCallable)) { - $data['name'] = call_user_func($displayableCallable); + if (is_callable([$entity, $configuration['displayable']])) { + $data['name'] = call_user_func([$entity, $configuration['displayable']]); } $displayableData[] = $data; } @@ -83,9 +81,8 @@ public function buildView(FormView $view, FormInterface $form, array $options): 'id' => $entities->getId(), 'classname' => $configuration['classname'], ]; - $displayableCallable = [$entities, $configuration['displayable']]; - if (\is_callable($displayableCallable)) { - $data['name'] = call_user_func($displayableCallable); + if (is_callable([$entities, $configuration['displayable']])) { + $data['name'] = call_user_func([$entities, $configuration['displayable']]); } $displayableData[] = $data; } diff --git a/src/Forms/NodeSource/NodeSourceNodeType.php b/src/Forms/NodeSource/NodeSourceNodeType.php index 779badd0..472f3dd2 100644 --- a/src/Forms/NodeSource/NodeSourceNodeType.php +++ b/src/Forms/NodeSource/NodeSourceNodeType.php @@ -20,9 +20,16 @@ */ final class NodeSourceNodeType extends AbstractNodeSourceFieldType { - public function __construct(ManagerRegistry $managerRegistry, private readonly NodeHandler $nodeHandler) + protected NodeHandler $nodeHandler; + + /** + * @param ManagerRegistry $managerRegistry + * @param NodeHandler $nodeHandler + */ + public function __construct(ManagerRegistry $managerRegistry, NodeHandler $nodeHandler) { parent::__construct($managerRegistry); + $this->nodeHandler = $nodeHandler; } /** diff --git a/src/Forms/NodeSource/NodeSourceProviderType.php b/src/Forms/NodeSource/NodeSourceProviderType.php index 852728bd..3471cfdb 100644 --- a/src/Forms/NodeSource/NodeSourceProviderType.php +++ b/src/Forms/NodeSource/NodeSourceProviderType.php @@ -19,9 +19,16 @@ final class NodeSourceProviderType extends AbstractConfigurableNodeSourceFieldType { - public function __construct(ManagerRegistry $managerRegistry, private readonly ContainerInterface $container) + protected ContainerInterface $container; + + /** + * @param ManagerRegistry $managerRegistry + * @param ContainerInterface $container + */ + public function __construct(ManagerRegistry $managerRegistry, ContainerInterface $container) { parent::__construct($managerRegistry); + $this->container = $container; } /** @@ -98,9 +105,7 @@ public function buildView(FormView $view, FormInterface $form, array $options): $provider = $this->getProvider($configuration, $options); $displayableData = []; - /** @var callable $callable */ - $callable = [$options['nodeSource'], $options['nodeTypeField']->getGetterName()]; - $ids = call_user_func($callable); + $ids = call_user_func([$options['nodeSource'], $options['nodeTypeField']->getGetterName()]); if (!is_array($ids)) { $entities = $provider->getItemsById([$ids]); } else { diff --git a/src/Forms/NodeSource/NodeSourceType.php b/src/Forms/NodeSource/NodeSourceType.php index 22aab62a..366408a4 100644 --- a/src/Forms/NodeSource/NodeSourceType.php +++ b/src/Forms/NodeSource/NodeSourceType.php @@ -18,7 +18,6 @@ use RZ\Roadiz\CoreBundle\Form\MultipleEnumerationType; use RZ\Roadiz\CoreBundle\Form\YamlType; use RZ\Roadiz\CoreBundle\Security\Authorization\Voter\NodeTypeFieldVoter; -use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\CountryType; @@ -32,6 +31,7 @@ use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Security\Core\Security; use Symfony\Component\Validator\Constraints\Email; use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\Type; @@ -41,10 +41,13 @@ final class NodeSourceType extends AbstractType { - public function __construct( - private readonly ManagerRegistry $managerRegistry, - private readonly Security $security - ) { + protected ManagerRegistry $managerRegistry; + private Security $security; + + public function __construct(ManagerRegistry $managerRegistry, Security $security) + { + $this->managerRegistry = $managerRegistry; + $this->security = $security; } /** diff --git a/src/Forms/NodeTagsType.php b/src/Forms/NodeTagsType.php new file mode 100644 index 00000000..cecf98ac --- /dev/null +++ b/src/Forms/NodeTagsType.php @@ -0,0 +1,63 @@ +managerRegistry = $managerRegistry; + } + + /** + * {@inheritdoc} + * + * @param FormBuilderInterface $builder + * @param array $options + */ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder->add('tags', TagsType::class, [ + 'by_reference' => false + ]); + $builder->get('tags') + ->addModelTransformer(new TagTransformer($this->managerRegistry->getManager())); + } + + /** + * {@inheritdoc} + * + * @param OptionsResolver $resolver + */ + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefault('data_class', Node::class); + } + + /** + * {@inheritdoc} + */ + public function getBlockPrefix(): string + { + return 'node_tags'; + } +} diff --git a/src/Forms/NodeType.php b/src/Forms/NodeType.php index 9cf491af..d3585452 100644 --- a/src/Forms/NodeType.php +++ b/src/Forms/NodeType.php @@ -5,6 +5,7 @@ namespace Themes\Rozier\Forms; use RZ\Roadiz\CoreBundle\Entity\Node; +use RZ\Roadiz\CoreBundle\Form\Constraint\UniqueNodeName; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; @@ -21,6 +22,11 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'label' => 'nodeName', 'empty_data' => '', 'help' => 'node.nodeName.help', + 'constraints' => [ + new UniqueNodeName([ + 'currentValue' => $options['nodeName'], + ]), + ] ]) ->add('dynamicNodeName', CheckboxType::class, [ 'label' => 'node.dynamicNodeName', @@ -29,10 +35,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ]) ; - /** @var Node|null $node */ - $node = $builder->getData(); - $isReachable = null !== $node && $node->getNodeType()->isReachable(); - if ($isReachable) { + if (null !== $builder->getData() && $builder->getData()->getNodeType()->isReachable()) { $builder->add('home', CheckboxType::class, [ 'label' => 'node.isHome', 'required' => false, @@ -53,7 +56,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ]) ; - if ($isReachable) { + if (null !== $builder->getData() && $builder->getData()->getNodeType()->isReachable()) { $builder->add('ttl', IntegerType::class, [ 'label' => 'node.ttl', 'help' => 'node_time_to_live_cache_on_front_controller', @@ -77,6 +80,7 @@ public function configureOptions(OptionsResolver $resolver): void 'class' => 'uk-form node-form', ], ]); - $resolver->setAllowedTypes('nodeName', ['string', 'null']); + + $resolver->setAllowedTypes('nodeName', 'string'); } } diff --git a/src/Forms/NodeTypeFieldType.php b/src/Forms/NodeTypeFieldType.php index 2d779998..46d15fa1 100644 --- a/src/Forms/NodeTypeFieldType.php +++ b/src/Forms/NodeTypeFieldType.php @@ -13,6 +13,9 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @package Themes\Rozier\Forms + */ class NodeTypeFieldType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void @@ -72,7 +75,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'required' => false, ]) ->add('defaultValues', DynamicType::class, [ - 'label' => 'nodeTypeField.defaultValues', + 'label' => 'defaultValues', 'required' => false, 'help' => 'for_children_node_and_node_references_enter_node_type_names_comma_separated', 'attr' => [ diff --git a/src/Forms/NodeTypeType.php b/src/Forms/NodeTypeType.php index beb37c1a..3601c424 100644 --- a/src/Forms/NodeTypeType.php +++ b/src/Forms/NodeTypeType.php @@ -41,16 +41,6 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'required' => false, 'help' => 'enables_published_at_field_for_time_based_publication', ]) - ->add('attributable', CheckboxType::class, [ - 'label' => 'attributable', - 'required' => false, - 'help' => 'enables_node_attributes_for_this_type', - ]) - ->add('sortingAttributesByWeight', CheckboxType::class, [ - 'label' => 'sortingAttributesByWeight', - 'required' => false, - 'help' => 'sort_attributes_by_weight_for_this_type', - ]) ->add('reachable', CheckboxType::class, [ 'label' => 'reachable', 'required' => false, diff --git a/src/Forms/RedirectionType.php b/src/Forms/RedirectionType.php index 1839e8ce..b0d22a9b 100644 --- a/src/Forms/RedirectionType.php +++ b/src/Forms/RedirectionType.php @@ -13,6 +13,9 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @package Themes\Rozier\Forms + */ class RedirectionType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Forms/TranstypeType.php b/src/Forms/TranstypeType.php index bb32d3b5..2ba0eba5 100644 --- a/src/Forms/TranstypeType.php +++ b/src/Forms/TranstypeType.php @@ -14,6 +14,9 @@ use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\NotNull; +/** + * @package Themes\Rozier\Forms + */ class TranstypeType extends AbstractType { protected ManagerRegistry $managerRegistry; diff --git a/src/Forms/UserType.php b/src/Forms/UserType.php index 0f4f8c9b..65bd4b39 100644 --- a/src/Forms/UserType.php +++ b/src/Forms/UserType.php @@ -12,6 +12,9 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @package Themes\Rozier\Forms + */ class UserType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Models/CustomFormModel.php b/src/Models/CustomFormModel.php index c5f63d45..fe4f00f5 100644 --- a/src/Models/CustomFormModel.php +++ b/src/Models/CustomFormModel.php @@ -8,16 +8,28 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Contracts\Translation\TranslatorInterface; +/** + * @package Themes\Rozier\Models + */ final class CustomFormModel implements ModelInterface { - public function __construct( - private readonly CustomForm $customForm, - private readonly UrlGeneratorInterface $urlGenerator, - private readonly TranslatorInterface $translator - ) { + private CustomForm $customForm; + private UrlGeneratorInterface $urlGenerator; + private TranslatorInterface $translator; + + /** + * @param CustomForm $customForm + * @param UrlGeneratorInterface $urlGenerator + * @param TranslatorInterface $translator + */ + public function __construct(CustomForm $customForm, UrlGeneratorInterface $urlGenerator, TranslatorInterface $translator) + { + $this->customForm = $customForm; + $this->urlGenerator = $urlGenerator; + $this->translator = $translator; } - public function toArray(): array + public function toArray() { $countFields = strip_tags($this->translator->trans( '{0} no.customFormField|{1} 1.customFormField|]1,Inf] %count%.customFormFields', diff --git a/src/Models/DocumentModel.php b/src/Models/DocumentModel.php index 8b8fbbed..237c4376 100644 --- a/src/Models/DocumentModel.php +++ b/src/Models/DocumentModel.php @@ -13,8 +13,17 @@ use RZ\Roadiz\Documents\UrlGenerators\DocumentUrlGeneratorInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +/** + * @package Themes\Rozier\Models + */ final class DocumentModel implements ModelInterface { + public static array $thumbnailArray = [ + "fit" => "40x40", + "quality" => 50, + "sharpen" => 5, + "inline" => false, + ]; public static array $thumbnail80Array = [ "fit" => "80x80", "quality" => 50, @@ -27,14 +36,35 @@ final class DocumentModel implements ModelInterface "inline" => false, "embed" => true, ]; + public static array $largeArray = [ + "noProcess" => true, + ]; + + private DocumentInterface $document; + private RendererInterface $renderer; + private DocumentUrlGeneratorInterface $documentUrlGenerator; + private UrlGeneratorInterface $urlGenerator; + private ?EmbedFinderFactory $embedFinderFactory; + /** + * @param DocumentInterface $document + * @param RendererInterface $renderer + * @param DocumentUrlGeneratorInterface $documentUrlGenerator + * @param UrlGeneratorInterface $urlGenerator + * @param EmbedFinderFactory|null $embedFinderFactory + */ public function __construct( - private readonly DocumentInterface $document, - private readonly RendererInterface $renderer, - private readonly DocumentUrlGeneratorInterface $documentUrlGenerator, - private readonly UrlGeneratorInterface $urlGenerator, - private readonly ?EmbedFinderFactory $embedFinderFactory = null + DocumentInterface $document, + RendererInterface $renderer, + DocumentUrlGeneratorInterface $documentUrlGenerator, + UrlGeneratorInterface $urlGenerator, + ?EmbedFinderFactory $embedFinderFactory = null ) { + $this->document = $document; + $this->renderer = $renderer; + $this->documentUrlGenerator = $documentUrlGenerator; + $this->urlGenerator = $urlGenerator; + $this->embedFinderFactory = $embedFinderFactory; } public function toArray(): array @@ -82,10 +112,10 @@ public function toArray(): array $editUrl = null; } - $embedFinder = $this->embedFinderFactory?->createForPlatform( + $embedFinder = $this->embedFinderFactory->createForPlatform( $this->document->getEmbedPlatform(), $this->document->getEmbedId() - ) ?? null; + ); return [ 'id' => $id, @@ -113,7 +143,7 @@ public function toArray(): array : $this->document->getShortType(), 'shortMimeType' => $this->document->getShortMimeType(), 'thumbnail_80' => $thumbnail80Url, - 'url' => $previewUrl ?? $thumbnail80Url, + 'url' => $previewUrl ?? $thumbnail80Url ?? null, ]; } } diff --git a/src/Models/ModelInterface.php b/src/Models/ModelInterface.php index 9670e57c..60726121 100644 --- a/src/Models/ModelInterface.php +++ b/src/Models/ModelInterface.php @@ -4,6 +4,9 @@ namespace Themes\Rozier\Models; +/** + * @package Themes\Rozier\Models + */ interface ModelInterface { /** diff --git a/src/Models/NodeModel.php b/src/Models/NodeModel.php index 50a1bc0c..f90bb280 100644 --- a/src/Models/NodeModel.php +++ b/src/Models/NodeModel.php @@ -9,20 +9,25 @@ use RZ\Roadiz\CoreBundle\Entity\NodesSources; use RZ\Roadiz\CoreBundle\Entity\NodesSourcesDocuments; use RZ\Roadiz\CoreBundle\Entity\Translation; -use RZ\Roadiz\CoreBundle\Security\Authorization\Voter\NodeVoter; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; -use Symfony\Bundle\SecurityBundle\Security; /** + * @package Themes\Rozier\Models * @Serializer\ExclusionPolicy("all") */ final class NodeModel implements ModelInterface { - public function __construct( - private readonly Node $node, - private readonly UrlGeneratorInterface $urlGenerator, - private readonly Security $security - ) { + private Node $node; + private UrlGeneratorInterface $urlGenerator; + + /** + * @param Node $node + * @param UrlGeneratorInterface $urlGenerator + */ + public function __construct(Node $node, UrlGeneratorInterface $urlGenerator) + { + $this->node = $node; + $this->urlGenerator = $urlGenerator; } public function toArray(): array @@ -31,21 +36,18 @@ public function toArray(): array $nodeSource = $this->node->getNodeSources()->first(); if (false === $nodeSource) { - $result = [ + return [ 'id' => $this->node->getId(), 'title' => $this->node->getNodeName(), 'nodeName' => $this->node->getNodeName(), 'isPublished' => $this->node->isPublished(), + 'nodesEditPage' => $this->urlGenerator->generate('nodesEditPage', [ + 'nodeId' => $this->node->getId(), + ]), 'nodeType' => [ - 'color' => $this->node->getNodeType()->getColor() ?? '#000000', + 'color' => $this->node->getNodeType()->getColor() ] ]; - if ($this->security->isGranted(NodeVoter::EDIT_SETTING, $this->node)) { - $result['nodesEditPage'] = $this->urlGenerator->generate('nodesEditPage', [ - 'nodeId' => $this->node->getId(), - ]); - } - return $result; } /** @var NodesSourcesDocuments|false $thumbnail */ @@ -59,32 +61,25 @@ public function toArray(): array 'thumbnail' => $thumbnail ? $thumbnail->getDocument() : null, 'nodeName' => $this->node->getNodeName(), 'isPublished' => $this->node->isPublished(), + 'nodesEditPage' => $this->urlGenerator->generate('nodesEditSourcePage', [ + 'nodeId' => $this->node->getId(), + 'translationId' => $translation->getId(), + ]), 'nodeType' => [ - 'color' => $this->node->getNodeType()->getColor() ?? '#000000', + 'color' => $this->node->getNodeType()->getColor() ] ]; - if ($this->security->isGranted(NodeVoter::EDIT_CONTENT, $nodeSource)) { - $result['nodesEditPage'] = $this->urlGenerator->generate('nodesEditSourcePage', [ - 'nodeId' => $this->node->getId(), - 'translationId' => $translation->getId(), - ]); - } - $parent = $this->node->getParent(); if ($parent instanceof Node) { $result['parent'] = [ - 'title' => $parent->getNodeSources()->first() ? - $parent->getNodeSources()->first()->getTitle() : - $parent->getNodeName() + 'title' => $parent->getNodeSources()->first()->getTitle() ]; $subParent = $parent->getParent(); if ($subParent instanceof Node) { $result['subparent'] = [ - 'title' => $subParent->getNodeSources()->first() ? - $subParent->getNodeSources()->first()->getTitle() : - $subParent->getNodeName() + 'title' => $subParent->getNodeSources()->first()->getTitle() ]; } } diff --git a/src/Models/NodeSourceModel.php b/src/Models/NodeSourceModel.php index 831105e4..73fef8c1 100644 --- a/src/Models/NodeSourceModel.php +++ b/src/Models/NodeSourceModel.php @@ -8,25 +8,28 @@ use RZ\Roadiz\CoreBundle\Entity\NodesSources; use RZ\Roadiz\CoreBundle\Entity\NodesSourcesDocuments; use RZ\Roadiz\CoreBundle\Entity\Translation; -use RZ\Roadiz\CoreBundle\Security\Authorization\Voter\NodeVoter; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; -use Symfony\Bundle\SecurityBundle\Security; /** * @Serializer\ExclusionPolicy("all") */ final class NodeSourceModel implements ModelInterface { - public function __construct( - private readonly NodesSources $nodeSource, - private readonly UrlGeneratorInterface $urlGenerator, - private readonly Security $security - ) { + private NodesSources $nodeSource; + private UrlGeneratorInterface $urlGenerator; + + public function __construct(NodesSources $nodeSource, UrlGeneratorInterface $urlGenerator) + { + $this->nodeSource = $nodeSource; + $this->urlGenerator = $urlGenerator; } public function toArray(): array { $node = $this->nodeSource->getNode(); + if (null === $node) { + throw new \RuntimeException('Node-source does not have a Node.'); + } /** @var NodesSourcesDocuments|false $thumbnail */ $thumbnail = $this->nodeSource->getDocumentsByFields()->first(); @@ -39,18 +42,15 @@ public function toArray(): array 'nodeName' => $node->getNodeName(), 'thumbnail' => $thumbnail ? $thumbnail->getDocument() : null, 'isPublished' => $node->isPublished(), + 'nodesEditPage' => $this->urlGenerator->generate('nodesEditSourcePage', [ + 'nodeId' => $node->getId(), + 'translationId' => $translation->getId(), + ]), 'nodeType' => [ - 'color' => $node->getNodeType()->getColor() ?? '#000000', + 'color' => $node->getNodeType()->getColor() ] ]; - if ($this->security->isGranted(NodeVoter::EDIT_CONTENT, $node)) { - $result['nodesEditPage'] = $this->urlGenerator->generate('nodesEditSourcePage', [ - 'nodeId' => $node->getId(), - 'translationId' => $translation->getId(), - ]); - } - $parent = $this->nodeSource->getParent(); if ($parent instanceof NodesSources) { diff --git a/src/Models/NodeTypeModel.php b/src/Models/NodeTypeModel.php index 240eb02b..bf9e93b0 100644 --- a/src/Models/NodeTypeModel.php +++ b/src/Models/NodeTypeModel.php @@ -6,10 +6,19 @@ use RZ\Roadiz\CoreBundle\Entity\NodeType; +/** + * @package Themes\Rozier\Models + */ final class NodeTypeModel implements ModelInterface { - public function __construct(private readonly NodeType $nodeType) + private NodeType $nodeType; + + /** + * @param NodeType $nodeType + */ + public function __construct(NodeType $nodeType) { + $this->nodeType = $nodeType; } public function toArray(): array diff --git a/src/Models/TagModel.php b/src/Models/TagModel.php index fd561900..10fe026c 100644 --- a/src/Models/TagModel.php +++ b/src/Models/TagModel.php @@ -7,12 +7,18 @@ use RZ\Roadiz\CoreBundle\Entity\Tag; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +/** + * @package Themes\Rozier\Models + */ final class TagModel implements ModelInterface { - public function __construct( - private readonly Tag $tag, - private readonly UrlGeneratorInterface $urlGenerator - ) { + private Tag $tag; + private UrlGeneratorInterface $urlGenerator; + + public function __construct(Tag $tag, UrlGeneratorInterface $urlGenerator) + { + $this->tag = $tag; + $this->urlGenerator = $urlGenerator; } public function toArray(): array diff --git a/src/Resources/app/Lazyload.js b/src/Resources/app/Lazyload.js index 1b6ddfe9..0e1f3d36 100644 --- a/src/Resources/app/Lazyload.js +++ b/src/Resources/app/Lazyload.js @@ -6,8 +6,10 @@ import TagsBulk from './components/bulk-edits/TagsBulk' import DocumentUploader from './components/documents/DocumentUploader' import NodeTypeFieldsPosition from './components/node-type-fields/NodeTypeFieldsPosition' import AttributeValuePosition from './components/attribute-values/AttributeValuePosition' +import NodeTypeFieldEdit from './components/node-type-fields/NodeTypeFieldEdit' import CustomFormFieldsPosition from './components/custom-form-fields/CustomFormFieldsPosition' import NodeTreeContextActions from './components/trees/NodeTreeContextActions' +import Import from './components/import/Import' import NodeEditSource from './components/node/NodeEditSource' import InputLengthWatcher from './widgets/InputLengthWatcher' import ChildrenNodesField from './widgets/ChildrenNodesField' @@ -27,6 +29,9 @@ import MultiLeafletGeotagField from './widgets/MultiLeafletGeotagField' import TagEdit from './components/tag/TagEdit' import MainTreeTabs from './components/tabs/MainTreeTabs' +/** + * Lazyload + */ export default class Lazyload { constructor() { this.$linksSelector = null @@ -49,6 +54,7 @@ export default class Lazyload { this.attributeValuesPosition = null this.customFormFieldsPosition = null this.settingsSaveButtons = null + this.nodeTypeFieldEdit = null this.nodeEditSource = null this.tagEdit = null this.markdownEditors = [] @@ -110,10 +116,14 @@ export default class Lazyload { ) { event.preventDefault() - window.requestAnimationFrame(() => { + if (this.clickTimeout) { + clearTimeout(this.clickTimeout) + } + + this.clickTimeout = window.setTimeout(() => { window.history.pushState({}, null, $link.attr('href')) this.onPopState(null) - }) + }, 0) return false } @@ -150,10 +160,10 @@ export default class Lazyload { * Delay loading if user is click like devil */ if (this.currentTimeout) { - window.cancelAnimationFrame(this.currentTimeout) + clearTimeout(this.currentTimeout) } - this.currentTimeout = window.requestAnimationFrame(async () => { + this.currentTimeout = window.setTimeout(() => { /* * Trigger event on window to notify open * widgets to close. @@ -161,71 +171,48 @@ export default class Lazyload { let pageChangeEvent = new CustomEvent('pagechange') window.dispatchEvent(pageChangeEvent) - try { - let url = '' - const path = location.href.split('?')[0] - const params = new URLSearchParams(location.href.split('?')[1]) - if (state.headerData) { - /** - * @param {string} key - * @param {string|number|Array} value - */ - for (let [key, value] of Object.entries(state.headerData)) { - if (Array.isArray(value)) { - value.forEach((v, i) => { - params.append(key + '[' + i + ']', v) + this.currentRequest = $.ajax({ + url: location.href, + type: 'get', + dataType: 'html', + cache: false, + data: state.headerData, + beforeSend: (xhr) => { + xhr.setRequestHeader('X-Partial', true) + }, + }) + .done((data) => { + this.applyContent(data) + this.canvasLoader.hide() + let pageLoadEvent = new CustomEvent('pageload', { detail: data }) + window.dispatchEvent(pageLoadEvent) + }) + .fail((data) => { + if (typeof data.responseText !== 'undefined') { + try { + let exception = JSON.parse(data.responseText) + window.UIkit.notify({ + message: exception.message, + status: 'danger', + timeout: 3000, + pos: 'top-center', }) - } else { - params.set(key, value) + } catch (e) { + // No valid JsonResponse, need to refresh page + window.location.href = location.href } - } - } - if (params.toString() !== '') { - url = path + '?' + params.toString() - } else { - url = path - } - - const response = await fetch(url, { - method: 'GET', - headers: { - 'X-Partial': true, - Accept: 'text/html', - }, - }) - if (!response.ok) { - throw response - } - const data = await response.text() - this.applyContent(data) - let pageLoadEvent = new CustomEvent('pageload', { detail: data }) - window.dispatchEvent(pageLoadEvent) - } catch (response) { - const data = await response.text() - if (data) { - try { - let exception = JSON.parse(data) + } else { window.UIkit.notify({ - message: exception.message, + message: window.Rozier.messages.forbiddenPage, status: 'danger', timeout: 3000, pos: 'top-center', }) - } catch (e) { - // No valid JsonResponse, need to refresh page - window.location.href = location.href } - } else { - window.UIkit.notify({ - message: window.Rozier.messages.forbiddenPage, - status: 'danger', - timeout: 3000, - pos: 'top-center', - }) - } - } - this.canvasLoader.hide() - }) + + this.canvasLoader.hide() + }) + }, 100) } refreshCodemirrorEditor() { @@ -277,10 +264,10 @@ export default class Lazyload { $tempData = $container.find('.new-content-global') - $old.eq(0).fadeOut(100, () => { + $old.fadeOut(100, () => { $old.remove() this.generalBind() - $tempData.eq(0).fadeIn(200, () => { + $tempData.fadeIn(200, () => { $tempData.removeClass('new-content-global') let pageShowEndEvent = new CustomEvent('pageshowend') window.dispatchEvent(pageShowEndEvent) @@ -317,6 +304,7 @@ export default class Lazyload { this.attributeValuesPosition, this.customFormFieldsPosition, this.settingsSaveButtons, + this.nodeTypeFieldEdit, this.nodeEditSource, this.tagEdit, this.nodeTree, @@ -351,6 +339,7 @@ export default class Lazyload { this.customFormFieldsPosition = new CustomFormFieldsPosition() this.nodeTreeContextActions = new NodeTreeContextActions() this.settingsSaveButtons = new SettingsSaveButtons() + this.nodeTypeFieldEdit = new NodeTypeFieldEdit() this.nodeEditSource = new NodeEditSource() this.tagEdit = new TagEdit() this.nodeTree = new NodeTree() @@ -375,7 +364,13 @@ export default class Lazyload { // Switch checkboxes this.initBootstrapSwitches() + window.Rozier.getMessages() + + if (typeof window.Rozier.importRoutes !== 'undefined' && window.Rozier.importRoutes !== null) { + window.Rozier.import = new Import(window.Rozier.importRoutes) + window.Rozier.importRoutes = null + } } generalUnbind(objects) { diff --git a/src/Resources/app/Rozier.js b/src/Resources/app/Rozier.js index 0f6f998c..c13e2dc6 100644 --- a/src/Resources/app/Rozier.js +++ b/src/Resources/app/Rozier.js @@ -6,7 +6,6 @@ import { PointerEventsPolyfill } from './utils/plugins' import { TweenLite, Expo } from 'gsap' import NodeTreeContextActions from './components/trees/NodeTreeContextActions' import RozierMobile from './RozierMobile' -import bulkActions from './widgets/GenericBulkActions' require('gsap/ScrollToPlugin') /** @@ -21,7 +20,6 @@ export default class Rozier { this.windowHeight = null this.resizeFirst = true this.mobile = null - this.ajaxToken = null this.nodeTrees = [] this.treeTrees = [] @@ -155,14 +153,6 @@ export default class Rozier { this.refreshMainNodeTree() this.refreshMainTagTree() this.refreshMainFolderTree() - - /* - * init generic bulk actions widget - */ - bulkActions() - window.addEventListener('pageshowend', () => { - bulkActions() - }) } saveCollapsedNestableState(state = null) { @@ -233,22 +223,16 @@ export default class Rozier { bindMainTrees() { // TREES let $nodeTree = $('.nodetree-widget .root-tree') - if ($nodeTree.length) { - $nodeTree.off('change.uk.nestable') - $nodeTree.on('change.uk.nestable', this.onNestableNodeTreeChange) - } + $nodeTree.off('change.uk.nestable') + $nodeTree.on('change.uk.nestable', this.onNestableNodeTreeChange) let $tagTree = $('.tagtree-widget .root-tree') - if ($tagTree.length) { - $tagTree.off('change.uk.nestable') - $tagTree.on('change.uk.nestable', this.onNestableTagTreeChange) - } + $tagTree.off('change.uk.nestable') + $tagTree.on('change.uk.nestable', this.onNestableTagTreeChange) let $folderTree = $('.foldertree-widget .root-tree') - if ($folderTree.length) { - $folderTree.off('change.uk.nestable') - $folderTree.on('change.uk.nestable', this.onNestableFolderTreeChange) - } + $folderTree.off('change.uk.nestable') + $folderTree.on('change.uk.nestable', this.onNestableFolderTreeChange) // Tree element name this.$mainTreeElementName = this.$mainTrees.find('.tree-element-name') @@ -288,71 +272,61 @@ export default class Rozier { }) } - fetchSessionMessages() { - return new Promise(async (resolve, reject) => { - const query = new URLSearchParams({ - _action: 'messages', - _token: this.ajaxToken, - }) - const url = this.routes.ajaxSessionMessages + '?' + query.toString() - try { - const response = await fetch(url, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, - }) - const data = await response.json() - if (!data.messages) { - reject() - } - resolve(data.messages) - } catch (e) { - reject() - } - }) - } /** * Get messages. */ - async getMessages() { - const messages = await this.fetchSessionMessages() - if (typeof messages.confirm !== 'undefined' && messages.confirm.length > 0) { - for (let i = messages.confirm.length - 1; i >= 0; i--) { - window.UIkit.notify({ - message: messages.confirm[i], - status: 'success', - timeout: 2000, - pos: 'top-center', - }) - } - } - - if (typeof messages.error !== 'undefined' && messages.error.length > 0) { - for (let j = data.messages.error.length - 1; j >= 0; j--) { - window.UIkit.notify({ - message: data.messages.error[j], - status: 'error', - timeout: 2000, - pos: 'top-center', - }) - } - } + getMessages() { + $.ajax({ + url: this.routes.ajaxSessionMessages, + type: 'GET', + dataType: 'json', + cache: false, + data: { + _action: 'messages', + _token: this.ajaxToken, + }, + }) + .done((data) => { + if (typeof data.messages !== 'undefined') { + if (typeof data.messages.confirm !== 'undefined' && data.messages.confirm.length > 0) { + for (let i = data.messages.confirm.length - 1; i >= 0; i--) { + window.UIkit.notify({ + message: data.messages.confirm[i], + status: 'success', + timeout: 2000, + pos: 'top-center', + }) + } + } + + if (typeof data.messages.error !== 'undefined' && data.messages.error.length > 0) { + for (let j = data.messages.error.length - 1; j >= 0; j--) { + window.UIkit.notify({ + message: data.messages.error[j], + status: 'error', + timeout: 2000, + pos: 'top-center', + }) + } + } + } + }) + .fail(() => { + console.log('[Rozier.getMessages] error') + }) } /** * @param translationId */ refreshAllNodeTrees(translationId) { - const promises = [] - promises.push(this.refreshMainNodeTree(translationId)) + this.refreshMainNodeTree(translationId) /* * Stack trees */ if (this.lazyload.stackNodeTrees.treeAvailable()) { - promises.push(this.lazyload.stackNodeTrees.refreshNodeTree()) + this.lazyload.stackNodeTrees.refreshNodeTree() } /* @@ -361,10 +335,9 @@ export default class Rozier { if (this.lazyload.childrenNodesFields.treeAvailable()) { for (let i = this.lazyload.childrenNodesFields.$nodeTrees.length - 1; i >= 0; i--) { let $nodeTree = this.lazyload.childrenNodesFields.$nodeTrees.eq(i) - promises.push(this.lazyload.childrenNodesFields.refreshNodeTree($nodeTree)) + this.lazyload.childrenNodesFields.refreshNodeTree($nodeTree) } } - return Promise.all(promises) } /** @@ -372,163 +345,151 @@ export default class Rozier { * * @param {Number|null|undefined} translationId */ - async refreshMainNodeTree(translationId = undefined) { - let $currentNodeTree = $('#tree-container').find('.nodetree-widget').eq(0) - if (!$currentNodeTree.length) { - console.debug('No main node-tree available.') - return - } - + refreshMainNodeTree(translationId) { + let $currentNodeTree = $('#tree-container').find('.nodetree-widget') let $currentRootTree = $currentNodeTree.find('.root-tree').eq(0) if ($currentRootTree.length && !translationId) { translationId = $currentRootTree.attr('data-translation-id') } - try { - const query = new URLSearchParams({ + if ($currentNodeTree.length) { + let postData = { _token: this.ajaxToken, _action: 'requestMainNodeTree', translationId: translationId || null, - }) - const url = this.routes.nodesTreeAjax + '?' + query.toString() - const response = await fetch(url, { - method: 'GET', - headers: { - Accept: 'application/json', - }, - }) - if (!response.ok) { - throw response - } - const data = await response.json() - if (typeof data.nodeTree !== 'undefined') { - await this.fadeOut($currentNodeTree) - $currentNodeTree.replaceWith(data.nodeTree) - $currentNodeTree = $('#tree-container').find('.nodetree-widget') - - await this.fadeIn($currentNodeTree) - this.initNestables() - this.bindMainTrees() - this.resize() - this.lazyload.bindAjaxLink() - - if (this.lazyload.nodeTreeContextActions) { - this.lazyload.nodeTreeContextActions.unbind() - } - - this.lazyload.nodeTreeContextActions = new NodeTreeContextActions() } - } catch (e) { - console.log('[Rozier.refreshMainNodeTree] Retrying in 3 seconds') - // Wait for background jobs to be done - window.setTimeout(() => { - this.refreshMainNodeTree(translationId) - }, 3000) + let url = this.routes.nodesTreeAjax + + $.ajax({ + url: url, + type: 'get', + cache: false, + dataType: 'json', + data: postData, + }) + .done((data) => { + if ($currentNodeTree.length && typeof data.nodeTree !== 'undefined') { + $currentNodeTree.fadeOut('slow', () => { + $currentNodeTree.replaceWith(data.nodeTree) + $currentNodeTree = $('#tree-container').find('.nodetree-widget') + $currentNodeTree.fadeIn() + this.initNestables() + this.bindMainTrees() + this.resize() + this.lazyload.bindAjaxLink() + + if (this.lazyload.nodeTreeContextActions) { + this.lazyload.nodeTreeContextActions.unbind() + } + + this.lazyload.nodeTreeContextActions = new NodeTreeContextActions() + }) + } + }) + .fail(() => { + console.log('[Rozier.refreshMainNodeTree] Retrying in 3 seconds') + // Wait for background jobs to be done + setTimeout(() => { + this.refreshMainNodeTree(translationId) + }, 3000) + }) + .always(() => { + this.lazyload.canvasLoader.hide() + }) + } else { + console.debug('No main node-tree available.') } - - this.lazyload.canvasLoader.hide() } /** * Refresh only main tagTree. * */ - async refreshMainTagTree() { + refreshMainTagTree() { let $currentTagTree = $('#tree-container').find('.tagtree-widget') - if (!$currentTagTree.length) { + if ($currentTagTree.length) { + let postData = { + _token: this.ajaxToken, + _action: 'requestMainTagTree', + } + + let url = this.routes.tagsTreeAjax + + $.ajax({ + url: url, + type: 'get', + cache: false, + dataType: 'json', + data: postData, + }) + .done((data) => { + if ($currentTagTree.length && typeof data.tagTree !== 'undefined') { + $currentTagTree.fadeOut('slow', () => { + $currentTagTree.replaceWith(data.tagTree) + $currentTagTree = $('#tree-container').find('.tagtree-widget') + $currentTagTree.fadeIn() + this.initNestables() + this.bindMainTrees() + this.resize() + this.lazyload.bindAjaxLink() + }) + } + }) + .always(() => { + this.lazyload.canvasLoader.hide() + }) + } else { console.debug('No main tag-tree available.') - return } - - const query = new URLSearchParams({ - _token: this.ajaxToken, - _action: 'requestMainTagTree', - }) - const url = this.routes.tagsTreeAjax + '?' + query.toString() - const response = await fetch(url, { - method: 'GET', - headers: { - Accept: 'application/json', - }, - }) - const data = await response.json() - if (typeof data.tagTree !== 'undefined') { - await this.fadeOut($currentTagTree) - $currentTagTree.replaceWith(data.tagTree) - $currentTagTree = $('#tree-container').find('.tagtree-widget') - await this.fadeIn($currentTagTree) - this.initNestables() - this.bindMainTrees() - this.resize() - this.lazyload.bindAjaxLink() - } - this.lazyload.canvasLoader.hide() } /** * Refresh only main folderTree. */ - async refreshMainFolderTree() { + refreshMainFolderTree() { let $currentFolderTree = $('#tree-container').find('.foldertree-widget') - if (!$currentFolderTree.length) { - console.debug('No main folder-tree available.') - return - } + if ($currentFolderTree.length) { + let postData = { + _token: this.ajaxToken, + _action: 'requestMainFolderTree', + } - const query = new URLSearchParams({ - _token: this.ajaxToken, - _action: 'requestMainFolderTree', - }) - const url = this.routes.foldersTreeAjax + '?' + query.toString() - const response = await fetch(url, { - method: 'GET', - headers: { - Accept: 'application/json', - }, - }) - const data = await response.json() - if (typeof data.folderTree !== 'undefined') { - await this.fadeOut($currentFolderTree) - $currentFolderTree.replaceWith(data.folderTree) - $currentFolderTree = $('#tree-container').find('.foldertree-widget') - await this.fadeIn($currentFolderTree) - this.initNestables() - this.bindMainTrees() - this.resize() - this.lazyload.bindAjaxLink() - } - - this.lazyload.canvasLoader.hide() - } + let url = this.routes.foldersTreeAjax - /** - * @param {jQuery} element - * @return {Promise} - */ - async fadeIn(element) { - return new Promise((resolve, reject) => { - element.fadeIn(() => { - resolve() + $.ajax({ + url: url, + type: 'get', + cache: false, + dataType: 'json', + data: postData, }) - }) - } - - /** - * @param {jQuery} element - * @return {Promise} - */ - async fadeOut(element) { - return new Promise((resolve, reject) => { - element.fadeOut('slow', () => { - resolve() - }) - }) + .done((data) => { + if ($currentFolderTree.length && typeof data.folderTree !== 'undefined') { + $currentFolderTree.fadeOut('slow', () => { + $currentFolderTree.replaceWith(data.folderTree) + $currentFolderTree = $('#tree-container').find('.foldertree-widget') + $currentFolderTree.fadeIn() + this.initNestables() + this.bindMainTrees() + this.resize() + this.lazyload.bindAjaxLink() + }) + } + }) + .always(() => { + this.lazyload.canvasLoader.hide() + }) + } else { + console.debug('No main folder-tree available.') + } } /** * Toggle trees panel + * @param {[type]} event [description] + * @return {[type]} [description] */ toggleTreesPanel() { $('#main-container-inner').toggleClass('trees-panel--minified') @@ -549,6 +510,8 @@ export default class Rozier { /** * Toggle user panel + * @param {[type]} event [description] + * @return {[type]} [description] */ toggleUserPanel() { $('#user-panel').toggleClass('minified') @@ -602,12 +565,12 @@ export default class Rozier { /** * @param event - * @param {HTMLElement} rootEl - * @param {HTMLElement} el - * @param {string|null|undefined} status - * @returns {false|undefined} + * @param rootEl + * @param el + * @param status + * @returns {boolean} */ - async onNestableNodeTreeChange(event, rootEl, el, status) { + onNestableNodeTreeChange(event, rootEl, el, status) { let element = $(el) /* * If node removed, do not do anything, the other change.uk.nestable nodeTree will be triggered @@ -643,7 +606,7 @@ export default class Rozier { return false } - const postData = { + let postData = { _token: this.ajaxToken, _action: 'updatePosition', nodeId: nodeId, @@ -659,43 +622,39 @@ export default class Rozier { postData.prevNodeId = parseInt(element.prev().attr('data-node-id')) } - try { - const response = await fetch(this.routes.nodeAjaxEdit.replace('%nodeId%', nodeId), { - method: 'POST', - headers: { - Accept: 'application/json', - }, - body: new URLSearchParams(postData), - }) - if (!response.ok) { - throw response - } - const data = await response.json() - window.UIkit.notify({ - message: data.responseText || data.detail, - status: data.status, - timeout: 3000, - pos: 'top-center', + $.ajax({ + url: this.routes.nodeAjaxEdit.replace('%nodeId%', nodeId), + type: 'POST', + dataType: 'json', + data: postData, + }) + .done((data) => { + window.UIkit.notify({ + message: data.responseText, + status: data.status, + timeout: 3000, + pos: 'top-center', + }) }) - } catch (response) { - const data = await response.json() - window.UIkit.notify({ - message: data.error_message || data.detail, - status: 'danger', - timeout: 3000, - pos: 'top-center', + .fail((data) => { + data = JSON.parse(data.responseText) + window.UIkit.notify({ + message: data.error_message, + status: 'danger', + timeout: 3000, + pos: 'top-center', + }) }) - } } /** * @param event - * @param {HTMLElement} rootEl - * @param {HTMLElement} el - * @param {string|null|undefined} status - * @returns {false|undefined} + * @param rootEl + * @param el + * @param status + * @returns {boolean} */ - async onNestableTagTreeChange(event, rootEl, el, status) { + onNestableTagTreeChange(event, rootEl, el, status) { let element = $(el) /* @@ -747,43 +706,29 @@ export default class Rozier { postData.prevTagId = parseInt(element.prev().attr('data-tag-id')) } - try { - const response = await fetch(this.routes.tagAjaxEdit.replace('%tagId%', tagId), { - method: 'POST', - headers: { - Accept: 'application/json', - }, - body: new URLSearchParams(postData), - }) - if (!response.ok) { - throw response - } - const data = await response.json() + $.ajax({ + url: this.routes.tagAjaxEdit.replace('%tagId%', tagId), + type: 'POST', + dataType: 'json', + data: postData, + }).done((data) => { window.UIkit.notify({ message: data.responseText, status: data.status, timeout: 3000, pos: 'top-center', }) - } catch (response) { - const data = await response.json() - window.UIkit.notify({ - message: data.error_message || data.detail, - status: 'danger', - timeout: 3000, - pos: 'top-center', - }) - } + }) } /** + * * @param event - * @param {HTMLElement} rootEl - * @param {HTMLElement} el - * @param {string|null|undefined} status - * @returns {false|undefined} + * @param element + * @param status + * @returns {boolean} */ - async onNestableFolderTreeChange(event, rootEl, el, status) { + onNestableFolderTreeChange(event, rootEl, el, status) { let element = $(el) /* * If folder removed, do not do anything, the other folderTree will be triggered @@ -836,33 +781,19 @@ export default class Rozier { postData.prevFolderId = parseInt(element.prev().attr('data-folder-id')) } - try { - const response = await fetch(this.routes.folderAjaxEdit.replace('%folderId%', folderId), { - method: 'POST', - headers: { - Accept: 'application/json', - }, - body: new URLSearchParams(postData), - }) - if (!response.ok) { - throw response - } - const data = await response.json() + $.ajax({ + url: this.routes.folderAjaxEdit.replace('%folderId%', folderId), + type: 'POST', + dataType: 'json', + data: postData, + }).done((data) => { window.UIkit.notify({ message: data.responseText, status: data.status, timeout: 3000, pos: 'top-center', }) - } catch (response) { - const data = await response.json() - window.UIkit.notify({ - message: data.error_message || data.detail, - status: 'danger', - timeout: 3000, - pos: 'top-center', - }) - } + }) } /** @@ -886,9 +817,9 @@ export default class Rozier { if (this.windowWidth >= 768 && this.windowWidth <= 1200 && this.$mainTrees.length && this.resizeFirst) { this.$mainTrees[0].style.display = 'none' this.$minifyTreePanelButton.trigger('click') - window.requestAnimationFrame(() => { + window.setTimeout(() => { this.$mainTrees[0].style.display = 'table-cell' - }) + }, 1000) } // Check if mobile @@ -935,6 +866,9 @@ export default class Rozier { this.lazyload.resize() this.entriesPanel.replaceSubNavs() + // Documents list + // if(this.lazyload !== null && !this.resizeFirst) this.lazyload.documentsList.resize(); + // Set resize first to false if (this.resizeFirst) this.resizeFirst = false } diff --git a/src/Resources/app/api/DocumentApi.js b/src/Resources/app/api/DocumentApi.js index e76828c9..9729e172 100644 --- a/src/Resources/app/api/DocumentApi.js +++ b/src/Resources/app/api/DocumentApi.js @@ -29,6 +29,7 @@ export function getDocumentsByIds({ ids = [] }) { } }) .catch((error) => { + // TODO // Log request error or display a message throw new Error(error.response.data.humanMessage) }) @@ -76,6 +77,7 @@ export function getDocuments({ searchTerms, filters, filterExplorerSelection, mo } }) .catch((error) => { + // TODO // Log request error or display a message throw new Error(error) }) diff --git a/src/Resources/app/assets/img/apple-touch-icon-114x114.png b/src/Resources/app/assets/img/apple-touch-icon-114x114.png new file mode 100644 index 0000000000000000000000000000000000000000..a0742a986501a5b14dfb91e365f1cc731fb9015c GIT binary patch literal 2125 zcmZ`)X*ApW7X2GzuGTeH4XvmtM2wZ>)*PYI8mdt>q$H-GC1`NfH9m@3ik55EQZ0(y zP*PM|b1MWf#n5++jj1)>DsH;g`|{pe@54F!oc&w-ob%oTr2mk<4w2h@B z2Sa~`052zlNA34G!0T&fZw3Gj4~2i=_&B|iw~eDc07R(*KwKgKeCJScD*zA<1%NMJ z0ATV20Hp5}wmKj<58SuyP?o?+%EONxoKEnLjcXVHNP&L_7ur!}i$j7)XnQNrtO(Z` zSpkVL;a42yEZWk{`QFqz%Psx-*twpsbes%7=$e|>t`g(Ctv?%#ELZg?uRWOOn@ZN_ zzeyeSfY`Rk1bP-sRiZ~ugKaaWUS_oUb-g&}ZA(=Vm~E1)?zU8){}(DT?UydDB}R&@ zW3nD)JfE_ENzY<$6Q{6ud@_D>j>7a{cQB7Pm7OZDpbT5Zp5ix|!IIQd%*%9N1uAEbY?j*ayA3Qa@|<#~S!Ei-(z zyk9A3qSa+xIa2I2v!PHmASvc%BZF(|z?LdQK6VNS_PBjUW!i$m>dhH`scu(nc*3^K z9hYqfGOlJ`dZOHyzj@L6VwXHCYeB2t#vS&tQw-e(CPan^aK?0hFO$grB}>Kh)GD$T z+;A6D6oq=ec>`T_P;8adWLq&6U~YKWka?yzDnqqkgQRD#PCF*M{EaGmH{Vu}MPaR{ z&PGV`Dd@6Pq+8#)TIV=0jmNRe@B8JlwM;u$qcV>iR0Er;^oO)HG$Ixv$(;$a(3u06 z24Yap&_$~0;_t+;8hY1;P1QtDkf*4oMt^e}0@o;!0H5YBQeZkb^_C-E!KW8wDhg$p zfvbo6I{lQH0Et4m4~VIX*=lta@~>OPcHSk`x|zc@)XhrC#Yyj4b&Qy^akn*FQ?_1E z>A^Q5>{E3E!6|6p`PN9yudm7Tbz0`rK=kN0-r>*w_b@xtPlS(4B|QbKj7GHonx;;P|5RCZ%#*d;O3JxiB!aEyMwd`JikqDTy6l5^0_bq%&oUdQQQPn>+sXK zPwl4B1Cvj#gU+nor8M&x`eJ2_^U^$9;F|Xo3lw~KuO40>+6pMikZo*v`+-px4k**A zO5kr~pl3I&QTya(U2*t2g_fMfmvjq7csZCT{MQ`(czhErX**{tj6qu zj11#J`XKg?Op09!kqZ+^tl1RXWJioM|} zHNPDzo=#v!hj}TrFv#E$da!*2?+*RS>{RWJ?NC_V%NZXlukba7VuGF1b!E1DU{v2& zMzAR6AS0IGzxH{I>uBJ&+{&lVBx#sPcb#E+NsU;Pf&()ZM}zbD@*BkY6uWW`(YDV` z#I>!IY}6vCP-Jg~7P}<}Ro}hoq!vvp;m3#s@bgXZmc|knn`qSi$1OZuuFG;& zIUeuQ^F}K9${~4Oty1irr`v8kqJf zvqw|%N0R07{`4ede9uu?Im6>CL`UW71m?2q>J`sT^6S&XgrxEB?u>0I>|<+8)c34Q zb5M#;Z~K?MSibQh7uvg#)u%HzF02!?pUItS7w<$ebKKu~MA8@o{=d0iD`K?{#56qB znHzIn(we;8U^ScOczCbeB zTW{ec8=_aYvN-COWuB)N-@{VWmk)6za*ui?^G44&b}Mu&jQR-<43Nx1s3JF z)EaE|3B;~fmwvver5~eqzxz-7NBapb+un2NQ^CCg1+_wQnUs$x#`C+S?ZW=lTXsWV zVmvVwIzKy6z#Me$ONZoOTROQgk zw=`MUmx~vczoWRLL84NUPjqI2u)%$D&HT@>;_e_p+f=O0bk`IoAH}MX|a{z!r zb@d^-FbGuFS=ZPEW?%v})`aSsK%s$T!SMeQ1cmtc6UhHxAg9vM#}UZ>+~5>KAVuOr k@qn36$Zfn5Itb^Bcf{j-$ankjKRq5mTVX8g%)R3O0TtHUl>h($ literal 0 HcmV?d00001 diff --git a/src/Resources/app/assets/img/apple-touch-icon-120x120.png b/src/Resources/app/assets/img/apple-touch-icon-120x120.png new file mode 100644 index 0000000000000000000000000000000000000000..914125f10613693c58e99b95086bcdd087cf5f45 GIT binary patch literal 2189 zcmZ`*c{J1w7yb>##2{N4*;BF&Gqw=Eh!_SFlaOREBxEpRc-4?KnzFAE5z5*k%V36N zUqX>d_DQnuN%^Kf-|w97o^$VW?sLy`?_c-a8;`Lv<>fxX4FCWy%FNh?g&}|a2nQ>s z`2KNV0f(odr6B;+JcJ(Lz^q)_&CJFU07B&efE)z?`z(sQ002P<09bSZ0NqRg5Fz9? zT+(9|*jz14je*11hrKPVjEi9A5C{OrPyclg%I4G>i-ZtSmL`y|JRm{QBSIgcRV?Nz z%GeMaGQOM}<|BMrIJTuZJ+`7)IxgiC83X6nz#$C8-W@riS-Kt($FXKY#y-aguL;b` zA`8eI*ImpWJ-O&JcoA%3D3h#6;Wh%%H!zK^1#EUVd>emAnN>4z19+xYu*J&iz;_+b zmABtkmh^-B;V{vE+hZ^D^;w?_iV}K_rZ);jbG5)$yWj63RYQ#_Lo@+C8{gG}`%NQ* zgU8qIM7&ZcN*RC&P&Upn%3PX$%iiToSPnKniYxY;N_p<4q@>^pePC!<^3%WeiO6np zfv<*4Kz$4YMw3zU(TrsWtazoLUEg-$QL>NoEg2ubcjiir^>f(_g1SW6Yl_1{FHy~7 zFG@LY8qRsIU+;`Mq6nEAEQ-oDn49C|TXfpemzuDUi^ds zxj@|->DV#*q6ujGtk?1JPZHxOVs4!+eNuXv3&yRm^UHGQDjfA35B*?w#kTW_kPdOS zd|3JVirX=7AE|BZAZ@#MCdUpsCOp(RK>brXNoOSkIL_DR44k6=#*^4 z@@&-KLvCHBen-B?Fq=GfuIkOHWn-M4He)v{!a)3TXl>Vz)s)Lc!`5*`CsFqe&;w^E zcMC1Z$x3RP4_scr9AW1Xdav7gbH_EMg22f0dD898J1nq`jtUTsxO4Hfs67acFGh<-o0ccHrTk|PNMSpv!n(Hcv1G%kyc5XBzZ?!d*#2|1D3;rwAucSR)d zOXs9fDrTlcgC^%JB6ZD6uebQ*OY!#Mu{UhzfP^xB-oPxwbcBjmtJY91!3Ee+H_fFG zgxqGBbwWBuQK2VNfHBZs+_6GjoT(@bQs1~27M>ZS1*xqWUe5Hjg)C8%G9abE5x1}UIg}lZ}n2$1%UPqBlzpZB0!~I{P0$G z*t+Iqzgyju7%ukhV~ZA=Kum+doz~vz(uIk9ubQmapp~hU^i6&Sm3K^)kjiheErp23u$6bk;HU=pUu6GI|$BjYk ziF2G8UQJ17h5vzELht)u7c)ZRix!4yp3al{3!whOC`q`g4nIC=sPw(M_|j}$JNn9v zd*hYW*-&9vB@uq)4qj!u&`-o;NT_p$E040t_{U~`$y$S;C?q-}_!$(Ee>BaY616pd zO!7)=#m=Uh<>Zjs!!ngsfkO|zmV{;nG}G;((#}@Jl<8{QNPT*9+Eu3jC%P+mvT{zT zx#4%Ti7~Z>f0XM>R@q!Fb?$uX=ubn2exb5QNT0k8cAIYNiZO$;F?d_RG|}R z9^m78UOV@fNX~}$aMUj9d==s$oHbt2P|icE*^^Z9|nx&3XzuZ?ao@ru^6<`S<+ar(Lg&%vXXVKTN(_apMNs;Cjo_hLhm z9Zi3Hph`1hO(j&9th+%Wu>)yTrTT=o+frRnLG2vDhPMr&nV-#nU7BdmfTJKh(TRaw z>lwex@5O$L?6#|hp-kYYd{4& zTo7tibQ%UD`%=pXOOmVves4`^;Ang_n@M(XF-j!NVoAo_?g2qFVW#;##pZN0p&FT3UE=4!v8s@-j$=9|M6SeN%S(ZrtUjw9lA-2(6|0Mrmj z4LDK_jzD6O+PZ3*x(ID01X34)@FsBu{Y&8M=kDb}`u~Cxr)s)b0@1${Z2dfl!MFfC jVCe4WikC+D;ym#-c$_=QzYG8OM*vVJR>qY^E|LEQm(JrZ literal 0 HcmV?d00001 diff --git a/src/Resources/app/assets/img/apple-touch-icon-144x144.png b/src/Resources/app/assets/img/apple-touch-icon-144x144.png new file mode 100644 index 0000000000000000000000000000000000000000..8d1d92b1ffd09acbd5b56bc61de71e0be82fd720 GIT binary patch literal 2607 zcmZ`*c{tQ-8~%+YllAD(VXP_AXc%NjViYp=#*ng{7-N}eY=d+#MktO*!x3c}#aObB z-9(I%;S?h!CZ(~AHA_k4OxOAAyT0%H<9YA*x$o|y(3hg4?_Ot_T~Ukmm#*}EzIv#vDRpN0Ep250MZQr*ydBDIRGHS0f2WI z0E}}1;J1+bZRbt+f`E_hISXJfC4=>Z-|Y{vz8DSw2i1QT2!%ef$|pr4Q1+H09C6SA zdGN2JVl@B&-bGoMW1`2GCSz+|T;!j9{=2n{%M}oy&Vgim#jz(gGYRv#_NnEMUgsK1 z=H=ZxvQN~;HRlHwdnkZP&CZ=R>V-2T0`3GtXNvO8^#?y$il0tU)lm!4nXrvVI=?tM zL0@_MJ!F}f8A3G}?_Jyquavsb( zfgiSORCpD1A&UgDyYSMZt{jHAM9BWy5qw9TL+3P2CRK!!o5J?VXN7#vtEyw2Sr=W0 zjk@icX8TI<0;{oTynlnN7vf#v@evPu>m}irk#sk|rj(AV@=KY<>NriuPB94^I{jp< z5;v&lG3&Q=7?b_k=|YlmmaExs(Y-P~qN3#&UMqvT3#_hBaAQi-X`KS(#EBVYk+OFg z(Ee*UHNbh@;NW%iultQ`3&TBV~ zMO7g??A8PERH7nsBeX$BBb{~_X%N?!BHw|LtP>{D3fqG7CE_E&=6SE;gwBArMVdF3wVa$34*LwawSro1k$hM)Uk_;$SWlSNDv+&a=iB{R51(Ua>5>LTw-hTBmCDvjeu76b<*Lmwo|HRZGDFxn@tFI5{> zXpl^|hdx=yNNZwJx!cwh44oL@WOHm|l$jdM?hv=8{Qb$y+TGjSgvjZ38`EM6V7Z!i ziZ0{@XGMD^&K47MgtHf9Mns&>ly4S+mko8c$m{oIOS-i60IvAi=AI8KRa5E(0PJjEM^7?2hm zqrNG|si7xb+kk1cs-Pq6K0da&P1%iX(GeNH=wlB9sm=NvrF>g@A6}W>onzHTqdX8G zY6rz0t36wTuYe(8+j&keP_o@vWw9pqTeowRuAWeBaN{fAIPPa$zuAmqXxu&1#{N%J zf7$KJcT+o1@ZloHenP-;&Nl#sD@nq2LSDd`5RoAKCNnYpEXW@+5LfSZSwBR#9uQ!59pH7F)9npOJcw7|ZLN{_M_ zm6u4obQtl6DOtXtqHE2nxn(G?Q4j5+LyKzB#i28+HV3-&Q%+q;x-vmod@(y+;m2O5 zq1%_GcbJL6^(|eRBg2FKuJ*`XJ55=OU+PAcIDZBPH_Q*|8ZL>aji%?D_0#z8GneKd5)YSmwgS*y=>+nhIS-X_JgSFkG}?mI82aybl~?{;iJ)DgpzMQmvnm;*Pk zLVV!#T~Nlq%)@Jbw`IYhMjU3~*J6mkjc9KSyY=bph$Y6E6nW$Pu4$ioVtVDyiQQ^G zaWfz+UtL%Go8Q~XW(y3K)OBS!Rr-vR-&feNQN^M+-WbK{;f-ylLC~2FI5dp$7e}tr z99h>xllz39H1llrm`*w$nxj7=?`N=IQ_V|G_OwUvWk_&23K~KAO;TYp>5?6{z$C{ovZC?oPtB!z#8qXU@fMu}!ErD-IRfuE#zRmWf*V{(vwnI>bGxk=ZCRfSk-B+k>2=(Wamf@#z zf4qx|+}mp!_@DXW-^77AyDjDY?r89e{n>Z?Y@{Ax=@#MZ9f32(hT-@C=)rYQz;yLs za9xb9k+GhEG2BQCu4@d3U%9rQ_#cK~f^VSTwf}E`9I5N)8{~g(a3=UgM0tnd0CQi0 f4^9;o?Cp<30nWP<}&#o(TUi1 literal 0 HcmV?d00001 diff --git a/src/Resources/app/assets/img/apple-touch-icon-152x152.png b/src/Resources/app/assets/img/apple-touch-icon-152x152.png new file mode 100644 index 0000000000000000000000000000000000000000..363df791cf7d4bbfa838e9e61104ce05e8ae1d83 GIT binary patch literal 2736 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 literal 0 HcmV?d00001 diff --git a/src/Resources/app/assets/img/apple-touch-icon-180x180.png b/src/Resources/app/assets/img/apple-touch-icon-180x180.png new file mode 100644 index 0000000000000000000000000000000000000000..d55df84065feb590d9752d64a288b23be84bc9a6 GIT binary patch literal 3208 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 literal 0 HcmV?d00001 diff --git a/src/Resources/app/assets/img/apple-touch-icon-57x57.png b/src/Resources/app/assets/img/apple-touch-icon-57x57.png new file mode 100644 index 0000000000000000000000000000000000000000..90bf6f5ee182e4241992e1543130378d5534af70 GIT binary patch literal 1220 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 literal 0 HcmV?d00001 diff --git a/src/Resources/app/assets/img/apple-touch-icon-76x76.png b/src/Resources/app/assets/img/apple-touch-icon-76x76.png new file mode 100644 index 0000000000000000000000000000000000000000..6074a9d37671c7b25215cb1fade8aece7550f0f9 GIT binary patch literal 1543 zcmZ`(dpy$%9Q_%2jMO~m5z4DX+ceE)affBQ)?}V5X&bX7X*OL-nGsgx)iPq1ywb}f z%5Gg&nmn7dqw>d24YnQ5X0Zk7x%wn^1z`9ex4Hn z#X|Q$Cd106?TofdoJ85!PeRt}p&skm$)HI`(9r#dCyoK@iZg{1)Up zTt`&nt&JygrA)S0(!rQ>>1<-UT6FU%%zIx>*0yCDurNy2VUraOntF-#RT^3rm_S;= zfF@Wu2me2Pz*`@%5DW7= zph5lmU!dSkVSisCMZr+n=(6^b9dR)zz?JHa{tkIFPSn}{h1O8oQ5gZgYx$ORQQS$0 z{5t9P;Wg?=F<$&_)zrL;BX@QXWDobkSTZ4DneEqVPs)G7CcE=bh?kwm-?a9(A$8lnXs4B zZ?LF-L-$S(LQ`E*{3{z_9bgFSayA-k(3g>Tn&ZY5HAW}nSsTpw%_Cp#)M&=g_RAGZ z!hQZysCDx?tY4>90fW=BU5{o9Ik}QmW0&`-!3T&epIZ2l>0N;r#THq*r=3HS24Mj{ z-DkW1z+XMk>ReWGE8o$ju9M#OtTGiymFs?PF!@-u8G9p|2+v;hv_`sU^4y_!W#_W8#;tS=T?OnoF4T-KS_y>4KX)crlYCKTcUPtL)gn@c_a0j~tHgk9l0W?*z| zE2ZZ--$`7z!75-Z8+#k0_?Mo1_u(o$XU2_T_Q8hgZA}HY8~2pSlbomAgialpr(Rg{ zr}|)tAjFzCj3v|-xFe;CuFRKZao5`4s0e){O>HQ~X=8xehmWeCXV1llbED~8CMGJ8 zsS>~yjzGE~TwUOZV1zry)eQr8-wa1!;PAKLyDu1ER literal 0 HcmV?d00001 diff --git a/src/Resources/app/assets/img/apple-touch-icon-precomposed.png b/src/Resources/app/assets/img/apple-touch-icon-precomposed.png new file mode 100644 index 0000000000000000000000000000000000000000..9fb65ed709276cd6eb7169e145433238fbcb5eae GIT binary patch literal 3153 zcmaKuX*kqv8^-@0vJ68M#>kQ-(O^c1DXL*?*`tgtV+>N37+ESLlF824%~;BoL}VQ# z3^fyDNtTIhg|fyY^5~t8J=?Gf#b{N;vmnBslcM3nn1H)yTB5vy=gMOfsi`_Ym?x4Y*L zr;s4F6-3fIo>OWkRS*z3f(9`>YG)WNto8GPBbd8{YIitJ7Q_#$q3dsQsCISbOn$Z8 zVYN&lWN&?~jM+cyWnnP25;4NP=>t(;~CN5Gc&?@BL8nL1P( z&338;xf3zhpe`j2r>8M_%Hn;gz3yV0rBlmHf7pAAMfLH{z%PhoQga|e`Q_xjfMenl z2O2_Ve8%vM)Ga}MA?;XQ+1o*)(84G|0S|>lu3J~ zog}ARCX*b$Zz(l1;Y&XPB{pT}RK9sWoOx37HHKl`+e{dQ!%@#9t$W z<00u1su*twiiGS=e3q%InJ}MKITBlvsYc+qdV1o|N3(uLe3|gekIJ;}e_Gm6!_$E;@U1*xVN~n_abx#rPhuE5^kVOlj4XxJfmu`Mdy-6YsvDKc3jiFFdpk0kD z%WwJAP2OoCv7z{bS&S6z)Qsao!w1`1npwEMzBH2i_DnYWB(6q+iO9un7q&vrUXS#Oa z_Ay>?r;lYBi&>r$K^2LB{eWn5naavwX2fRCf~2 zS3h?{OBg`Iq=HhasL0Kr;HJK5&$&tLH);m~ScI1HlDR&6_}Qi%x&khxC(pt-+jx!_ zm_NN5X6?EYEh53ImR4EtYBv9Z#NL+BZ)iQmcej(Hx5?8_q_3PPuX8x(&ifHQZU|kZ`?Tg z{cX3PoL=U7N}pdgHYq~c1+1r;8rp&*qP}sLZusBFBIZWoxmPBG%8TOkAZ2UasXlTo zu5>*U(AlkdZPM<7>}yR*WYz{J7Ixih|94`?R61?C#0Hx9_y#lagNmPu43{pPlzBdvS zbMo&;d+m?-HN3TA^l5r8hT@ghCd67&U0EXk(1)KjWrbgy_RT3ziuWs?xD|g(&{bYc z#QXcSKweYST2KA%se#9P`p3181qd1>-1$U|rACUQvTQ1%%TI*!+rRm>k5BlyCuDzy zHZwCb8j9OlUp^e|2Fth8GR>JbTceISi5x1a?#e$#u%-|q}V7rr-G+;nAA!) zDdRG-=ok~`p@dF2N=Pd=|(|6F}jdH>u=2kkW!G_prvM$T-n8j|S=hBCU6RVDkA~d5c#%NxJdtUik;6soVV-=qWug?@- zJ#+BuRvl6B%lTehzwsdp26Juln|>lM{b|1Fc5O9IMFuJJK_jo@DR*UJwTfr(rlm<@iO4r z5HWhkJ)fwy1*h~%CF0L^jJ|ZsD;$oo*b+iGC4jqo@`Xw{L^gsNQ)k6xu%?Z zPc=flk5g+wIU?`w$-gfQnHTDkp1C_R>YZGKLm$LXpavgtnqFS~tJ5M}~LQiec6gv`$q64vj&G+u%cmB{BXEoxU!z}>Bshz-WFrtt&t2z|-?Z~gL_W39Y*x_R^2{gpF%D-@fF_N{hP7OM zszq2khU$jFsu(*eZ4TG*&C_WjWVGnZ6<5NDOzNxa5Vdn>ld8GM6R_>dQ-t!Aeg>uNyOwBMgg4lx6 z=b;Do=o&PczrcPszTd7=t(umzgW`-7&ZI;gAMC7CxXz9GHc(OQ=;6q)$RQ~+$eyyA zHIIyAGutGdz0@7eN4!UQx5eFwSx|C2>eULt)g6XzflB%4R)iA85odv)BQNKNl|hdlEvhfq3Ae hhrc^s*3=j4g}23HJ;H9i#sBH~fGOJ2uo~qS{U7CT=c)hz literal 0 HcmV?d00001 diff --git a/src/Resources/app/assets/img/browserconfig.xml b/src/Resources/app/assets/img/browserconfig.xml new file mode 100644 index 00000000..fe44cae8 --- /dev/null +++ b/src/Resources/app/assets/img/browserconfig.xml @@ -0,0 +1,12 @@ + + + + + + + + + #da532c + + + diff --git a/src/Resources/app/assets/img/favicon-160x160.png b/src/Resources/app/assets/img/favicon-160x160.png new file mode 100644 index 0000000000000000000000000000000000000000..e33f50a501ad91770c74c2b989208a5cb48affad GIT binary patch literal 2847 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 literal 0 HcmV?d00001 diff --git a/src/Resources/app/assets/img/matrix@2x.png b/src/Resources/app/assets/img/matrix@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..b856cf4f20fb1cc37462fcb325096bed9001cd3c GIT binary patch literal 1341 zcmeAS@N?(olHy`uVBq!ia0y~yV7bA-z^uT*3=}!Wr@sYASqAuoxc>kDe+o9Rdi83E zFhs$fJ9mKGj*bo>36uf~D!km81vG@eB*-tAK|sO4Az{LT1se{Wxp4DCrpPa#49gNv z7srr_TW@b3%xek|alP0ccC^Xnh|NM9B+m59jICnVg z-1H9_4{8uv)TIDSFG%BvUL1Z wnQG#2gQ0~NMqqc>sOV@2jD`TML*QShdL*aVvzuGbKLJ_g>FVdQ&MBb@0P%Uk2mk;8 literal 0 HcmV?d00001 diff --git a/src/Resources/app/assets/img/mstile-144x144.png b/src/Resources/app/assets/img/mstile-144x144.png new file mode 100644 index 0000000000000000000000000000000000000000..97bd51763790c8f91995b7c17392da51d20d9d1a GIT binary patch literal 2499 zcmZ`*Sv=H@_x_sf%g@#rB557QHe)x0u}`uLgR&21N+iZuvNnu0*^{gjZwPruqM5vs zt+MYClh6<%B^ui=KVAGU-i!amInU=g=R6nZ=J`CSb~YA5{3rMU01!f3n%W;>)}P|# zJnGiAF!dwgyoRzy0YF2hzyXfyh|Bp{+FJua)&Wqkpsjd0SbS0*A?&{cT5#hlI8^AQHmhTXHEQ1=b+)`Eg0OU}4U~bMMY57Tte*IE6h>vm3dT9xUT6 zX~sj-%EhL02p=;&!B-~|m6Q{oid`vxt5bv{_Mk3^7B zMYwL;EO-xm72h&Uqi5hy?lfJ}0p35OjOyUlBU9?+%xJX?k>GVv&+oob{cNXNH+M-{ zQA3hggcZXOG&ySO-1!eH* zS2Ou%MMx~fHrI@eYmQNnl%Yeu!$kgh5y2%CzT!HYPuW^-N9%0B%aD98Gh&VqMf9*B znqR(LykupV^=|qn)#d5o$7rrzS}A&bsm^pU+k&H;vo!}iQ^(~)- zyR~D!#~J@BWYrw>G*p?eG0TwH59 zv>l)#oEu|h9%sh9U-Bq+RB($q@=OR;G!5%ZE2SQuZ&b01(4Xp5el9BYAP64a*A+!e z{N@JLQ1SMotA>J5v;?aaRkeX(XoMb9fqjyY081J}%MZKGe{ztwD-dc%U<+Q5sQxs4 zjq8_q+1&2C+b3ZG(dm#Y?(66Oc7oK$n&iQ)+2>7?Y3x zSstTbBV2ak`EEy-p+@>@z@Fg1ha2b#XbSfp0sla4jpu zgikL+7QW$|pm;{#_q}tr4IXRI&Y-;cCjSU?*XyOA7q@=CK zdurHahXJv2nZQ7n1>&@4N`Baag`t z8bTa+d@xw4Hwo^Rz3iZSY6#hvdoZxKJ^NWy`D3d1{43VpcJ)aI_imXrSpizvsa5N+ z&Tv&bS6RCU=5@+TnG%0v`+Ss|PR5Fv?fWyd!%Hxy2;2=bN`{r>+x=tjh4s+{XCIKZ zY280P#){7GHkC40r)DVEa?_0<4)w%eSC_d<<$jy!_8G&r9!U5MXz(ks=3Kciw5)^zoYZphAI zS{@Enp^2-vqgnO)o)NFmvXh9?-M}GrD|_l-UTv`n=N@Oshk)gE9Ra$v^GgF9;say1 zNU|&nb(qUos7KuK!NM`OLQWB%fttK?_T0LrfBq9V_arM<60I9{1ApigX literal 0 HcmV?d00001 diff --git a/src/Resources/app/assets/img/mstile-150x150.png b/src/Resources/app/assets/img/mstile-150x150.png new file mode 100644 index 0000000000000000000000000000000000000000..9fc6336fc77ce7aa7d8515d2e9de384e07d71b1f GIT binary patch literal 2551 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 literal 0 HcmV?d00001 diff --git a/src/Resources/app/assets/img/mstile-70x70.png b/src/Resources/app/assets/img/mstile-70x70.png new file mode 100644 index 0000000000000000000000000000000000000000..9c98673d0e21733f26abcda64db57b15f82a5431 GIT binary patch literal 1777 zcmZ`)XH=7i8vY^>HX>39Lk*E4n}iJ|9D)fWNCGJ`6gli2P(&mGVwnacgc@Xz$|xgJ z2%td378D30GDQ-?Pz05sQMv4K5un)-u?W0Ua~XJPFg}q0ssJM2YWO@fOmhG zn6SWyn$}qhKsXrXgaUx(9LarO5kU_1wDi;0Ic``fJFfS zC|oP)bg>jPj`=y-p@E}}oZ-I(nP`lI6Gn7S3M8i}3v@i#0RWL52Qzm2BSF|&1PX1=(Wi*P)mTP(hL7hm>hlt3-#cajLlR-%Z{1iwv%(PegQL4R? zqz1UzCOKMs0gdi>`DE02L*vwCY_%@&>ET#A^RpDuJQzv*uybpKbYS_{c92i0 zpIAMQ4GURkU5Dm29#4>r6L+ ze1$xUnsX1E^@MFi?ZGG-Z4tMBF?Vsl1M>0Dqh@I(eHjT1&v{HaoWKUtZ8loihrJzz zoew33B<3o-Rd)-9LWtXudRjEsU2}81>6>GiHRrHT&(v;*_zgM#-1U_=ijtzX$zQ{1 z&a-u#_!uo((JM23ML@-;i;xvu){d$3e*28zxDE%ipsQx}G(%5VrzY%*b5#D$b=mw? z5Thi|%VluOP`4BMPE4b6a*;FwR8UK??2%&_wrPc@g7>#pP|x|EK3cv;1%Z)3f!JBECBamiMlt=CwWV`TM;UQO*l?M|pAM9b=0))k+5QN0NJ>PIZa?inNJlQE zr#Q7-vld%%i55R|7wY*w?TEgl={8n-A7XO{-Fr*wB&};``}GXin(Ay?2IYY<5o>+)A}C6C0Xd!aS^;Tf_M`Z^t`ct@_G{Ru%rwT5K#MG7&6t z10ClZeq7o3;6{PIoV8a!p=?TJW?MWXJo)nysHOefhKor{`h%iFL&*7EM*4@gLhoGq zOBbFYG$rjVxlzZrO})FT%$+mnx{kwWrs41t?2Fg-lb-X7TaUV5KPr1z^D|?Ngzlw*;HA7#JI>26EEj>J zKHcVJi+2*OkDnqpRoRvWNE7wLllsCxtJL01GPitKcS2X0Gtr0G%+()xis&;0tHJS7 zGDNC^hVxWH<@JIN#_;1$z3)<4*2{J}v6&p+%Ezbz)ufoH=w@@+cgR+{)u650_4=Q# zv*o{4Sx#k^oiSao5Ku|$lJ1-SwW~!BQXFAL-}>`Z!v#WRf#MkBw?ytei88-BdUU*B zvjLP6+{+&Ydk#bsNfZ5r9y~SjETZG^J(*9~!mW?1KS{MEc%s z)m&iIyto+oWfBa`aB5r(Sqh<#&orD>v3Gez1`2w zg`O8fUtKlzu9n@@d%+^q^jX<1+V~Fd%q&{{?%&#bR2F6H7?Rn@6{=&~O7>}3$-
-
+
+ v-if="getThumbnail() && !item.thumbnail.processable" + :style="{ 'background-image': 'url(' + getThumbnail() + ')' }">
+ v-else-if="getThumbnail() && item.thumbnail.processable"> - - + +
-

-