diff --git a/README.md b/README.md
index 45d6d38..6142fb8 100644
--- a/README.md
+++ b/README.md
@@ -40,4 +40,4 @@ codefog_tags:
## Insert Tags
-The extension also provides two new insert tags: `{{insert_node::*}}` and `{{insert_nodes::*}}`. The former expects the ID of a node and will then generate the output of that node. The latter expects a comma separated list of node IDs and will then generate the output of all those nodes. Example: `{{insert_nodes::1,2,3}}`
+The extension also provides two new insert tags: `{{insert_node::*}}` and `{{insert_nodes::*}}`. The former expects the ID of a node and will then generate the output of that node. The latter expects a comma separated list of node IDs or aliases and will then generate the output of all those nodes. Example: `{{insert_nodes::1,2,3}}`
diff --git a/composer-dependency-analyser.php b/composer-dependency-analyser.php
index dc4dda8..7746b2c 100644
--- a/composer-dependency-analyser.php
+++ b/composer-dependency-analyser.php
@@ -6,6 +6,4 @@
return (new Configuration())
// Optional integrations
->ignoreErrorsOnPackage('terminal42/contao-geoip2-country', [ErrorType::DEV_DEPENDENCY_IN_PROD])
-
- ->ignoreUnknownClasses(['Haste\Model\Model'])
;
diff --git a/composer.json b/composer.json
index 6e4a82e..3773df6 100755
--- a/composer.json
+++ b/composer.json
@@ -25,17 +25,21 @@
"source": "https://github.com/terminal42/contao-node"
},
"require": {
- "php": "^8.0",
- "contao/core-bundle": "^4.13 || ^5.0",
- "codefog/contao-haste": "^4.21 || ^5.0",
+ "php": "^8.1",
+ "contao/core-bundle": "^5.3",
+ "codefog/contao-haste": "^5.2",
"codefog/tags-bundle": "^3.3",
- "doctrine/dbal": "^3.3 || ^4.0",
+ "doctrine/dbal": "^3.3 || ^4.3",
"psr/log": "^1.0 || ^2.0 || ^3.0",
"symfony/config": "^5.0 || ^6.0 || ^7.0",
"symfony/dependency-injection": "^5.0 || ^6.0 || ^7.0",
"symfony/http-foundation": "^5.0 || ^6.0 || ^7.0",
"symfony/http-kernel": "^5.0 || ^6.0 || ^7.0",
- "symfony/security-core": "^5.0 || ^6.0 || ^7.0"
+ "symfony/security-core": "^5.0 || ^6.0 || ^7.0",
+ "symfony/security-bundle": "^5.0 || ^6.0 || ^7.0",
+ "symfony/service-contracts": "^3.0",
+ "symfony/translation-contracts": "^3.0",
+ "twig/twig": "^3.0"
},
"require-dev": {
"terminal42/contao-geoip2-country": "^1.4",
diff --git a/config/controllers.php b/config/controllers.php
new file mode 100755
index 0000000..d734ab4
--- /dev/null
+++ b/config/controllers.php
@@ -0,0 +1,24 @@
+services();
+ $services->defaults()->autoconfigure();
+
+ $services
+ ->set(NodesContentElementController::class)
+ ->arg('$nodeManager', service(NodeManager::class))
+ ;
+
+ $services
+ ->set(NodesFrontendModuleController::class)
+ ->arg('$nodeManager', service(NodeManager::class))
+ ;
+};
diff --git a/config/listeners.php b/config/listeners.php
new file mode 100755
index 0000000..5685c02
--- /dev/null
+++ b/config/listeners.php
@@ -0,0 +1,30 @@
+services();
+ $services->defaults()->autoconfigure();
+
+ $services
+ ->set(DataContainerListener::class)
+ ->arg('$connection', service('database_connection'))
+ ->arg('$finderFactory', service('contao.twig.finder_factory'))
+ ->arg('$locales', service('contao.intl.locales'))
+ ->arg('$logger', service('monolog.logger.contao'))
+ ->arg('$requestStack', service('request_stack'))
+ ->arg('$security', service('security.helper'))
+ ->arg('$tagsManager', service('codefog_tags.manager.terminal42_node'))
+ ->arg('$translator', service('translator'))
+ ;
+
+ $services
+ ->set(ContentListener::class)
+ ->arg('$connection', service('database_connection'))
+ ;
+};
diff --git a/config/migrations.php b/config/migrations.php
new file mode 100755
index 0000000..1012283
--- /dev/null
+++ b/config/migrations.php
@@ -0,0 +1,17 @@
+services();
+ $services->defaults()->autoconfigure();
+
+ $services
+ ->set(GuestsMigration::class)
+ ->arg('$connection', service('database_connection'))
+ ;
+};
diff --git a/config/security.php b/config/security.php
new file mode 100755
index 0000000..c364b63
--- /dev/null
+++ b/config/security.php
@@ -0,0 +1,33 @@
+services();
+ $services->defaults()->autoconfigure();
+
+ $services
+ ->set(BackendAccessVoter::class)
+ ->decorate('contao.security.backend_access_voter')
+ ->arg('$contaoFramework', service('contao.framework'))
+ ->arg('$inner', service('.inner'))
+ ;
+
+ $services
+ ->set(NodeContentVoter::class)
+ ->arg('$accessDecisionManager', service('security.access.decision_manager'))
+ ->arg('$connection', service('database_connection'))
+ ;
+
+ $services
+ ->set(NodePermissionVoter::class)
+ ->arg('$accessDecisionManager', service('security.access.decision_manager'))
+ ->arg('$contaoFramework', service('contao.framework'))
+ ;
+};
diff --git a/config/services.php b/config/services.php
new file mode 100755
index 0000000..7b4ba38
--- /dev/null
+++ b/config/services.php
@@ -0,0 +1,32 @@
+services();
+ $services->defaults()->autoconfigure();
+
+ $services
+ ->set(NodeInsertTag::class)
+ ->arg('$manager', service(NodeManager::class))
+ ->arg('$logger', service('monolog.logger.contao'))
+ ;
+
+ $services
+ ->set(NodeManager::class)
+ ->arg('$twig', service('twig'))
+ ;
+
+ $services
+ ->set(NodePickerProvider::class)
+ ->arg('$menuFactory', service('knp_menu.factory'))
+ ->arg('$router', service('router'))
+ ->arg('$translator', service('translator'))
+ ;
+};
diff --git a/config/services.yml b/config/services.yml
deleted file mode 100755
index c48a0cd..0000000
--- a/config/services.yml
+++ /dev/null
@@ -1,49 +0,0 @@
-services:
- Terminal42\NodeBundle\NodeManager: '@terminal42_node.manager'
-
- terminal42_node.manager:
- class: Terminal42\NodeBundle\NodeManager
- public: true
-
- terminal42_node.listener.content:
- class: Terminal42\NodeBundle\EventListener\ContentListener
- public: true
- arguments:
- - "@database_connection"
- - "@terminal42_node.permission_checker"
-
- terminal42_node.listener.data_container:
- class: Terminal42\NodeBundle\EventListener\DataContainerListener
- public: true
- arguments:
- - "@database_connection"
- - "@contao.intl.locales"
- - "@monolog.logger.contao"
- - "@terminal42_node.permission_checker"
- - "@request_stack"
- - "@codefog_tags.manager.terminal42_node"
-
- terminal42_node.listener.insert_tags:
- class: Terminal42\NodeBundle\EventListener\InsertTagsListener
- public: true
- arguments:
- - "@terminal42_node.manager"
- - "@?logger"
-
- terminal42_node.permission_checker:
- class: Terminal42\NodeBundle\PermissionChecker
- public: false
- arguments:
- - "@database_connection"
- - "@security.authorization_checker"
- - "@security.token_storage"
-
- terminal42_node.picker:
- class: Terminal42\NodeBundle\Picker\NodePickerProvider
- public: false
- arguments:
- - "@knp_menu.factory"
- - "@router"
- - "@translator"
- tags:
- - { name: contao.picker_provider, priority: 132 }
diff --git a/contao/config/config.php b/contao/config/config.php
index 7d4483a..187a8c1 100755
--- a/contao/config/config.php
+++ b/contao/config/config.php
@@ -1,47 +1,21 @@
array_values(array_unique(array_merge(['tl_node', 'tl_content'], $GLOBALS['BE_MOD']['content']['article']['tables'] ?? []))),
'table' => &$GLOBALS['BE_MOD']['content']['article']['table'],
'list' => &$GLOBALS['BE_MOD']['content']['article']['list'],
];
-/*
- * Back end form fields
- */
+// Back end form fields
$GLOBALS['BE_FFL']['nodePicker'] = NodePickerWidget::class;
-/*
- * Frontend modules
- */
-$GLOBALS['FE_MOD']['miscellaneous']['nodes'] = NodesModule::class;
-
-/*
- * Content elements
- */
-$GLOBALS['TL_CTE']['includes']['nodes'] = NodesContentElement::class;
-
-/*
- * Models
- */
+// Models
$GLOBALS['TL_MODELS']['tl_node'] = NodeModel::class;
-/*
- * Hooks
- */
-$GLOBALS['TL_HOOKS']['executePostActions'][] = ['terminal42_node.listener.data_container', 'onExecutePostActions'];
-$GLOBALS['TL_HOOKS']['replaceInsertTags'][] = ['terminal42_node.listener.insert_tags', 'onReplace'];
-
-/*
- * User permissions
- */
+// User permissions
$GLOBALS['TL_PERMISSIONS'][] = 'nodeMounts';
$GLOBALS['TL_PERMISSIONS'][] = 'nodePermissions';
diff --git a/contao/dca/tl_content.php b/contao/dca/tl_content.php
index 0d23f92..a3323b9 100644
--- a/contao/dca/tl_content.php
+++ b/contao/dca/tl_content.php
@@ -1,39 +1,24 @@
&$GLOBALS['TL_LANG']['tl_content']['nodes'],
- 'exclude' => true,
'inputType' => 'nodePicker',
'eval' => ['mandatory' => true, 'multiple' => true, 'fieldType' => 'checkbox', 'tl_class' => 'clr'],
- 'sql' => ['type' => 'blob', 'notnull' => false],
- 'save_callback' => [
- ['terminal42_node.listener.content', 'onNodesSaveCallback'],
- ],
+ 'sql' => ['type' => Types::BLOB, 'notnull' => false],
];
$GLOBALS['TL_DCA']['tl_content']['fields']['nodesWrapper'] = [
'label' => &$GLOBALS['TL_LANG']['tl_content']['nodesWrapper'],
- 'exclude' => true,
'inputType' => 'checkbox',
'eval' => ['submitOnChange' => true, 'tl_class' => 'clr'],
- 'sql' => ['type' => 'string', 'length' => 1, 'default' => ''],
+ 'sql' => ['type' => Types::BOOLEAN, 'default' => false],
];
diff --git a/contao/dca/tl_module.php b/contao/dca/tl_module.php
index e2916bc..ab5f9fb 100644
--- a/contao/dca/tl_module.php
+++ b/contao/dca/tl_module.php
@@ -6,16 +6,12 @@
Controller::loadDataContainer('tl_content');
System::loadLanguageFile('tl_content');
-/*
- * Palettes
- */
+// Palettes
$GLOBALS['TL_DCA']['tl_module']['palettes']['__selector__'][] = 'nodesWrapper';
-$GLOBALS['TL_DCA']['tl_module']['palettes']['nodes'] = '{title_legend},name,type;{include_legend},nodes,nodesWrapper;{template_legend:hide},customTpl;{protected_legend:hide},protected;{expert_legend:hide},guests';
+$GLOBALS['TL_DCA']['tl_module']['palettes']['nodes'] = '{title_legend},name,type;{include_legend},nodes,nodesWrapper;{template_legend:collapsed},customTpl;{protected_legend:collapsed},protected;{expert_legend:collapsed},guests';
$GLOBALS['TL_DCA']['tl_module']['subpalettes']['nodesWrapper'] = &$GLOBALS['TL_DCA']['tl_content']['subpalettes']['nodesWrapper'];
-/*
- * Fields
- */
+// Fields
$GLOBALS['TL_DCA']['tl_module']['fields']['nodes'] = &$GLOBALS['TL_DCA']['tl_content']['fields']['nodes'];
$GLOBALS['TL_DCA']['tl_module']['fields']['nodesWrapper'] = &$GLOBALS['TL_DCA']['tl_content']['fields']['nodesWrapper'];
diff --git a/contao/dca/tl_node.php b/contao/dca/tl_node.php
index 504e593..b841070 100644
--- a/contao/dca/tl_node.php
+++ b/contao/dca/tl_node.php
@@ -1,8 +1,8 @@
['tl_content'],
'enableVersioning' => true,
'markAsCopy' => 'name',
- 'onload_callback' => [
- ['terminal42_node.listener.data_container', 'onLoadCallback'],
- ],
'sql' => [
'keys' => [
'id' => 'primary',
@@ -30,80 +27,28 @@
'mode' => DataContainer::MODE_TREE,
'icon' => 'folderC.svg',
'rootPaste' => true,
- 'paste_button_callback' => ['terminal42_node.listener.data_container', 'onPasteButtonCallback'],
'panelLayout' => 'filter;search',
],
'label' => [
'fields' => ['name'],
'format' => '%s',
- 'label_callback' => ['terminal42_node.listener.data_container', 'onLabelCallback'],
],
'global_operations' => [
'toggleNodes' => [
- 'label' => &$GLOBALS['TL_LANG']['MSC']['toggleAll'],
'href' => 'ptg=all',
'class' => 'header_toggle',
'showOnSelect' => true,
],
- 'all' => [
- 'label' => &$GLOBALS['TL_LANG']['MSC']['all'],
- 'href' => 'act=select',
- 'class' => 'header_edit_all',
- 'attributes' => 'onclick="Backend.getScrollOffset()" accesskey="e"',
- ],
- ],
- 'operations' => [
- 'edit' => [
- 'label' => &$GLOBALS['TL_LANG']['tl_node']['edit'],
- 'href' => 'table=tl_content',
- 'icon' => 'edit.svg',
- 'button_callback' => ['terminal42_node.listener.data_container', 'onEditButtonCallback'],
- ],
- 'editheader' => [
- 'label' => &$GLOBALS['TL_LANG']['tl_node']['editheader'],
- 'href' => 'act=edit',
- 'icon' => 'header.svg',
- 'button_callback' => ['terminal42_node.listener.data_container', 'onEditHeaderButtonCallback'],
- ],
- 'copy' => [
- 'label' => &$GLOBALS['TL_LANG']['tl_node']['copy'],
- 'href' => 'act=paste&mode=copy',
- 'icon' => 'copy.svg',
- 'attributes' => 'onclick="Backend.getScrollOffset()"',
- 'button_callback' => ['terminal42_node.listener.data_container', 'onCopyButtonCallback'],
- ],
- 'copyChilds' => [
- 'label' => &$GLOBALS['TL_LANG']['tl_node']['copyChilds'],
- 'href' => 'act=paste&mode=copy&childs=1',
- 'icon' => 'copychilds.svg',
- 'attributes' => 'onclick="Backend.getScrollOffset()"',
- 'button_callback' => ['terminal42_node.listener.data_container', 'onCopyChildsButtonCallback'],
- ],
- 'cut' => [
- 'label' => &$GLOBALS['TL_LANG']['tl_node']['cut'],
- 'href' => 'act=paste&mode=cut',
- 'icon' => 'cut.svg',
- 'attributes' => 'onclick="Backend.getScrollOffset()"',
- ],
- 'delete' => [
- 'label' => &$GLOBALS['TL_LANG']['tl_node']['delete'],
- 'href' => 'act=delete',
- 'icon' => 'delete.svg',
- 'attributes' => 'onclick="if(!confirm(\''.($GLOBALS['TL_LANG']['MSC']['deleteConfirm'] ?? null).'\'))return false;Backend.getScrollOffset()"',
- 'button_callback' => ['terminal42_node.listener.data_container', 'onDeleteButtonCallback'],
- ],
- 'show' => [
- 'label' => &$GLOBALS['TL_LANG']['tl_node']['show'],
- 'href' => 'act=show',
- 'icon' => 'show.svg',
- ],
+ 'all',
],
],
// Palettes
'palettes' => [
- '__selector__' => ['wrapper', 'protected'],
- 'default' => '{name_legend},name,type;{wrapper_legend},wrapper;{filter_legend},languages,tags;{protected_legend:hide},protected,guests',
+ '__selector__' => ['type', 'wrapper', 'protected'],
+ 'default' => '{name_legend},name,type;{wrapper_legend},wrapper;{filter_legend},languages,tags;{protected_legend:collapsed},protected,guests',
+ NodeModel::TYPE_FOLDER => '{name_legend},name,type;{filter_legend},languages,tags',
+ NodeModel::TYPE_CONTENT => '{name_legend},name,type,alias;{wrapper_legend},wrapper;{filter_legend},languages,tags;{protected_legend:collapsed},protected,guests',
],
// Subpalettes
@@ -115,31 +60,27 @@
// Fields
'fields' => [
'id' => [
- 'sql' => ['type' => 'integer', 'unsigned' => true, 'autoincrement' => true],
+ 'sql' => ['type' => Types::INTEGER, 'unsigned' => true, 'autoincrement' => true],
],
'pid' => [
'label' => &$GLOBALS['TL_LANG']['tl_node']['pid'],
'foreignKey' => 'tl_node.name',
- 'sql' => ['type' => 'integer', 'unsigned' => true, 'default' => 0],
+ 'sql' => ['type' => Types::INTEGER, 'unsigned' => true, 'default' => 0],
],
'sorting' => [
- 'sql' => ['type' => 'integer', 'unsigned' => true, 'default' => 0],
+ 'sql' => ['type' => Types::INTEGER, 'unsigned' => true, 'default' => 0],
],
'tstamp' => [
'label' => &$GLOBALS['TL_LANG']['tl_node']['tstamp'],
- 'sql' => ['type' => 'integer', 'unsigned' => true, 'default' => 0],
+ 'sql' => ['type' => Types::INTEGER, 'unsigned' => true, 'default' => 0],
],
'name' => [
- 'label' => &$GLOBALS['TL_LANG']['tl_node']['name'],
- 'exclude' => true,
'search' => true,
'inputType' => 'text',
'eval' => ['mandatory' => true, 'maxlength' => 255, 'tl_class' => 'w50'],
- 'sql' => ['type' => 'string', 'length' => 255, 'default' => ''],
+ 'sql' => ['type' => Types::STRING, 'length' => 255, 'default' => ''],
],
'type' => [
- 'label' => &$GLOBALS['TL_LANG']['tl_node']['type'],
- 'exclude' => true,
'filter' => true,
'inputType' => 'select',
'options' => [
@@ -147,68 +88,51 @@
NodeModel::TYPE_FOLDER,
],
'reference' => &$GLOBALS['TL_LANG']['tl_node']['typeRef'],
- 'eval' => ['tl_class' => 'w50'],
- 'sql' => ['type' => 'string', 'length' => 7, 'default' => ''],
+ 'eval' => ['submitOnChange' => true, 'tl_class' => 'w50'],
+ 'sql' => ['type' => Types::STRING, 'length' => 7, 'default' => ''],
+ ],
+ 'alias' => [
+ 'search' => true,
+ 'inputType' => 'text',
+ 'eval' => ['rgxp' => 'alias', 'doNotCopy' => true, 'unique' => true, 'maxlength' => 255, 'tl_class' => 'w50'],
+ 'sql' => ['type' => Types::STRING, 'length' => 255, 'default' => ''],
],
'wrapper' => [
- 'label' => &$GLOBALS['TL_LANG']['tl_node']['wrapper'],
- 'exclude' => true,
'inputType' => 'checkbox',
'eval' => ['submitOnChange' => true, 'tl_class' => 'clr'],
- 'sql' => ['type' => 'string', 'length' => 1, 'default' => ''],
+ 'sql' => ['type' => Types::STRING, 'length' => 1, 'default' => ''],
],
'nodeTpl' => [
- 'exclude' => true,
'inputType' => 'select',
- 'options_callback' => static fn () => Controller::getTemplateGroup('node_'),
'eval' => ['includeBlankOption' => true, 'chosen' => true, 'tl_class' => 'w50'],
- 'sql' => ['type' => 'string', 'length' => 64, 'default' => ''],
+ 'sql' => ['type' => Types::STRING, 'length' => 64, 'default' => ''],
],
'cssID' => [
- 'label' => &$GLOBALS['TL_LANG']['tl_node']['cssID'],
- 'exclude' => true,
'inputType' => 'text',
'eval' => ['multiple' => true, 'size' => 2, 'tl_class' => 'w50'],
- 'sql' => ['type' => 'string', 'length' => 255, 'default' => ''],
+ 'sql' => ['type' => Types::STRING, 'length' => 255, 'default' => ''],
],
'languages' => [
- 'label' => &$GLOBALS['TL_LANG']['tl_node']['languages'],
- 'exclude' => true,
'filter' => true,
'inputType' => 'select',
- 'options_callback' => ['terminal42_node.listener.data_container', 'onLanguagesOptionsCallback'],
'eval' => ['multiple' => true, 'chosen' => true, 'csv' => ',', 'tl_class' => 'clr'],
- 'sql' => ['type' => 'string', 'length' => 255, 'default' => ''],
+ 'sql' => ['type' => Types::STRING, 'length' => 255, 'default' => ''],
],
'tags' => [
- 'label' => &$GLOBALS['TL_LANG']['tl_node']['tags'],
- 'exclude' => true,
'filter' => true,
'inputType' => 'cfgTags',
'eval' => ['tagsManager' => 'terminal42_node', 'tl_class' => 'clr'],
],
'protected' => [
- 'label' => &$GLOBALS['TL_LANG']['tl_node']['protected'],
- 'exclude' => true,
'inputType' => 'checkbox',
'eval' => ['submitOnChange' => true],
- 'sql' => "char(1) NOT NULL default ''",
+ 'sql' => ['type' => Types::BOOLEAN, 'default' => false],
],
'groups' => [
- 'label' => &$GLOBALS['TL_LANG']['tl_node']['groups'],
- 'exclude' => true,
'inputType' => 'checkbox',
- 'foreignKey' => 'tl_member_group.name',
'eval' => ['mandatory' => true, 'multiple' => true],
- 'sql' => 'blob NULL',
- 'relation' => ['type' => 'hasMany', 'load' => 'lazy'],
- ],
- 'guests' => [
- 'label' => &$GLOBALS['TL_LANG']['tl_node']['guests'],
- 'exclude' => true,
- 'inputType' => 'checkbox',
- 'eval' => ['tl_class' => 'clr'],
- 'sql' => "char(1) NOT NULL default ''",
+ 'sql' => ['type' => Types::BLOB, 'notnull' => false],
+ 'relation' => ['type' => 'hasMany', 'load' => 'lazy', 'table' => 'tl_member_group'],
],
],
];
diff --git a/contao/dca/tl_user.php b/contao/dca/tl_user.php
index 3639092..7f533d5 100644
--- a/contao/dca/tl_user.php
+++ b/contao/dca/tl_user.php
@@ -1,42 +1,29 @@
addLegend('node_legend', 'pagemounts_legend', PaletteManipulator::POSITION_AFTER)
- ->addField('nodeMounts', 'node_legend', PaletteManipulator::POSITION_APPEND)
- ->addField('nodePermissions', 'node_legend', PaletteManipulator::POSITION_APPEND)
+ ->addLegend('node_legend', 'pagemounts_legend')
+ ->addField(['nodeMounts', 'nodePermissions'], 'node_legend', PaletteManipulator::POSITION_APPEND)
->applyToPalette('extend', 'tl_user')
->applyToPalette('custom', 'tl_user')
;
-/*
- * Fields
- */
+// Fields
$GLOBALS['TL_DCA']['tl_user']['fields']['nodeMounts'] = [
'label' => &$GLOBALS['TL_LANG']['tl_user']['nodeMounts'],
- 'exclude' => true,
'inputType' => 'nodePicker',
'eval' => ['multiple' => true, 'fieldType' => 'checkbox', 'tl_class' => 'clr'],
- 'sql' => ['type' => 'blob', 'notnull' => false],
+ 'sql' => ['type' => Types::BLOB, 'notnull' => false],
];
$GLOBALS['TL_DCA']['tl_user']['fields']['nodePermissions'] = [
'label' => &$GLOBALS['TL_LANG']['tl_user']['nodePermissions'],
- 'exclude' => true,
'inputType' => 'checkbox',
- 'options' => [
- PermissionChecker::PERMISSION_CREATE,
- PermissionChecker::PERMISSION_EDIT,
- PermissionChecker::PERMISSION_DELETE,
- PermissionChecker::PERMISSION_CONTENT,
- PermissionChecker::PERMISSION_ROOT,
- ],
+ 'options' => ['create', 'edit', 'delete', 'content', 'root'],
'reference' => &$GLOBALS['TL_LANG']['tl_user']['nodePermissionsRef'],
'eval' => ['multiple' => true, 'fieldType' => 'checkbox', 'tl_class' => 'clr'],
- 'sql' => ['type' => 'blob', 'notnull' => false],
+ 'sql' => ['type' => Types::BLOB, 'notnull' => false],
];
diff --git a/contao/dca/tl_user_group.php b/contao/dca/tl_user_group.php
index ffadd2a..362df85 100644
--- a/contao/dca/tl_user_group.php
+++ b/contao/dca/tl_user_group.php
@@ -7,18 +7,13 @@
Controller::loadDataContainer('tl_user');
System::loadLanguageFile('tl_user');
-/*
- * Palettes
- */
+// Palettes
PaletteManipulator::create()
->addLegend('node_legend', 'pagemounts_legend', PaletteManipulator::POSITION_AFTER)
- ->addField('nodeMounts', 'node_legend', PaletteManipulator::POSITION_APPEND)
- ->addField('nodePermissions', 'node_legend', PaletteManipulator::POSITION_APPEND)
+ ->addField(['nodeMounts', 'nodePermissions'], 'node_legend', PaletteManipulator::POSITION_APPEND)
->applyToPalette('default', 'tl_user_group')
;
-/*
- * Fields
- */
+// Fields
$GLOBALS['TL_DCA']['tl_user_group']['fields']['nodeMounts'] = &$GLOBALS['TL_DCA']['tl_user']['fields']['nodeMounts'];
$GLOBALS['TL_DCA']['tl_user_group']['fields']['nodePermissions'] = &$GLOBALS['TL_DCA']['tl_user']['fields']['nodePermissions'];
diff --git a/contao/languages/cs/default.php b/contao/languages/cs/default.php
index 2667de0..29d4d86 100644
--- a/contao/languages/cs/default.php
+++ b/contao/languages/cs/default.php
@@ -1,4 +1,5 @@
'Vytvořit prvky',
- PermissionChecker::PERMISSION_EDIT => 'Upravit prvky',
- PermissionChecker::PERMISSION_DELETE => 'Smazat prvky',
- PermissionChecker::PERMISSION_CONTENT => 'Spravovat prvky',
- PermissionChecker::PERMISSION_ROOT => 'Spravovat klíčové prvky',
+ 'create' => 'Vytvořit prvky',
+ 'edit' => 'Upravit prvky',
+ 'delete' => 'Smazat prvky',
+ 'content' => 'Spravovat prvky',
+ 'root' => 'Spravovat klíčové prvky',
];
/*
diff --git a/contao/languages/de/default.php b/contao/languages/de/default.php
index 1ff787d..5bba044 100644
--- a/contao/languages/de/default.php
+++ b/contao/languages/de/default.php
@@ -1,4 +1,5 @@
'Nodes erstellen',
- PermissionChecker::PERMISSION_EDIT => 'Nodes bearbeiten',
- PermissionChecker::PERMISSION_DELETE => 'Nodes löschen',
- PermissionChecker::PERMISSION_CONTENT => 'Nodes verwalten',
- PermissionChecker::PERMISSION_ROOT => 'Root Nodes verwalten',
+ 'create' => 'Nodes erstellen',
+ 'edit' => 'Nodes bearbeiten',
+ 'delete' => 'Nodes löschen',
+ 'content' => 'Nodes verwalten',
+ 'root' => 'Root Nodes verwalten',
];
/*
diff --git a/contao/languages/en/default.php b/contao/languages/en/default.php
index e84bd31..ddf43c4 100755
--- a/contao/languages/en/default.php
+++ b/contao/languages/en/default.php
@@ -1,4 +1,5 @@
'Create nodes',
- PermissionChecker::PERMISSION_EDIT => 'Edit nodes',
- PermissionChecker::PERMISSION_DELETE => 'Delete nodes',
- PermissionChecker::PERMISSION_CONTENT => 'Manage content',
- PermissionChecker::PERMISSION_ROOT => 'Manage root nodes',
+ 'create' => 'Create nodes',
+ 'edit' => 'Edit nodes',
+ 'delete' => 'Delete nodes',
+ 'content' => 'Manage content',
+ 'root' => 'Manage root nodes',
];
/*
diff --git a/contao/templates/.twig-root b/contao/templates/.twig-root
new file mode 100644
index 0000000..e69de29
diff --git a/contao/templates/backend/nodes_wildcard.html.twig b/contao/templates/backend/nodes_wildcard.html.twig
new file mode 100644
index 0000000..eaf3a53
--- /dev/null
+++ b/contao/templates/backend/nodes_wildcard.html.twig
@@ -0,0 +1,15 @@
+
+
### {{ label }} ###
+ {% if nodes %}
+
+ {% for node in nodes %}
+ {% set node_href = path('contao_backend', {do: 'nodes', table: 'tl_content', id: node.id}) %}
+
{{ node.name}} (ID: {{ node.id }})
+ {% endfor %}
+ {% endif %}
+ {% if hrefParams ?? false %}
+
+ {% set href = path('contao_backend', hrefParams) %}
+ {{ name }} (
ID: {{ id }})
+ {% endif %}
+
diff --git a/contao/templates/content_element/nodes.html.twig b/contao/templates/content_element/nodes.html.twig
new file mode 100644
index 0000000..40ebc37
--- /dev/null
+++ b/contao/templates/content_element/nodes.html.twig
@@ -0,0 +1,17 @@
+{% extends "@Contao/content_element/_base.html.twig" %}
+
+{% block wrapper %}
+ {% if nodes_wrapper ?? false %}
+ <{% block wrapper_tag %}div{% endblock %}{% block attributes %}{{ attributes }}{% endblock %}>
+ {% endif %}
+
+ {%- block inner %}
+ {% for node in nodes %}
+ {{ node|raw }}
+ {% endfor %}
+ {% endblock -%}
+
+ {% if nodes_wrapper ?? false %}
+ {{ block('wrapper_tag') }}>
+ {% endif %}
+{% endblock %}
diff --git a/contao/templates/elements/ce_nodes.html5 b/contao/templates/elements/ce_nodes.html5
deleted file mode 100644
index 9331a1f..0000000
--- a/contao/templates/elements/ce_nodes.html5
+++ /dev/null
@@ -1,9 +0,0 @@
-nodesWrapper): ?>
- class): ?> class="= $this->class ?>"= $this->cssID ?>>
-
-
-= implode("\n", $this->nodes) ?>
-
-nodesWrapper): ?>
-
-
diff --git a/contao/templates/frontend_module/nodes.html.twig b/contao/templates/frontend_module/nodes.html.twig
new file mode 100644
index 0000000..c645881
--- /dev/null
+++ b/contao/templates/frontend_module/nodes.html.twig
@@ -0,0 +1,17 @@
+{% extends "@Contao/frontend_module/_base.html.twig" %}
+
+{% block wrapper %}
+ {% if nodes_wrapper ?? false %}
+ <{% block wrapper_tag %}div{% endblock %}{% block attributes %}{{ attributes }}{% endblock %}>
+ {% endif %}
+
+ {%- block inner %}
+ {% for node in nodes %}
+ {{ node|raw }}
+ {% endfor %}
+ {% endblock -%}
+
+ {% if nodes_wrapper ?? false %}
+ {{ block('wrapper_tag') }}>
+ {% endif %}
+{% endblock %}
diff --git a/contao/templates/modules/mod_nodes.html5 b/contao/templates/modules/mod_nodes.html5
deleted file mode 100644
index 9331a1f..0000000
--- a/contao/templates/modules/mod_nodes.html5
+++ /dev/null
@@ -1,9 +0,0 @@
-nodesWrapper): ?>
- class): ?> class="= $this->class ?>"= $this->cssID ?>>
-
-
-= implode("\n", $this->nodes) ?>
-
-nodesWrapper): ?>
-
-
diff --git a/contao/templates/node/default.html.twig b/contao/templates/node/default.html.twig
new file mode 100644
index 0000000..fe730fc
--- /dev/null
+++ b/contao/templates/node/default.html.twig
@@ -0,0 +1,11 @@
+{% set attributes = attrs()
+ .setIfExists('id', cssID)
+ .addClass(['node_wrapper', class])
+ .mergeWith(attributes|default)
+%}
+
+
+ {% for element in elements %}
+ {{ element.renderedHtml|raw }}
+ {% endfor %}
+
diff --git a/contao/templates/node_default.html5 b/contao/templates/node_default.html5
deleted file mode 100644
index 28f3651..0000000
--- a/contao/templates/node_default.html5
+++ /dev/null
@@ -1,3 +0,0 @@
-cssID): ?> id="= $this->cssID; ?>" class="node_wrapper = $this->class; ?>">
- = $this->buffer; ?>
-
diff --git a/src/ContaoManager/Plugin.php b/src/ContaoManager/Plugin.php
index 067dfed..2b23c59 100755
--- a/src/ContaoManager/Plugin.php
+++ b/src/ContaoManager/Plugin.php
@@ -4,6 +4,7 @@
namespace Terminal42\NodeBundle\ContaoManager;
+use Codefog\HasteBundle\CodefogHasteBundle;
use Contao\CoreBundle\ContaoCoreBundle;
use Contao\ManagerPlugin\Bundle\BundlePluginInterface;
use Contao\ManagerPlugin\Bundle\Config\BundleConfig;
@@ -19,7 +20,7 @@ public function getBundles(ParserInterface $parser): array
{
return [
BundleConfig::create(Terminal42NodeBundle::class)
- ->setLoadAfter([ContaoCoreBundle::class, 'haste', Terminal42Geoip2CountryBundle::class]),
+ ->setLoadAfter([ContaoCoreBundle::class, CodefogHasteBundle::class, Terminal42Geoip2CountryBundle::class]),
];
}
diff --git a/src/ContentElement/NodesContentElement.php b/src/ContentElement/NodesContentElement.php
deleted file mode 100644
index 672f43e..0000000
--- a/src/ContentElement/NodesContentElement.php
+++ /dev/null
@@ -1,130 +0,0 @@
-objModel->nodes, true))) {
- return '';
- }
-
- $ids = array_map('intval', $ids);
-
- // Check for potential circular reference
- if ('tl_node' === $this->objModel->ptable && \in_array((int) $this->objModel->pid, $ids, true)) {
- /** @var Request $request */
- $request = System::getContainer()->get('request_stack')->getCurrentRequest();
-
- if (null !== $request) {
- /** @var ScopeMatcher $scopeMatcher */
- $scopeMatcher = System::getContainer()->get('contao.routing.scope_matcher');
-
- if ($scopeMatcher->isBackendRequest($request)) {
- return \sprintf('%s', $GLOBALS['TL_LANG']['ERR']['circularReference']);
- }
- }
-
- return '';
- }
-
- /** @var Request $request */
- $request = System::getContainer()->get('request_stack')->getCurrentRequest();
-
- // Display the backend wildcard
- if (null !== $request) {
- /** @var ScopeMatcher $scopeMatcher */
- $scopeMatcher = System::getContainer()->get('contao.routing.scope_matcher');
-
- if ($scopeMatcher->isBackendRequest($request)) {
- return static::generateBackendWildcard($this->arrData, $ids);
- }
- }
-
- $this->nodes = System::getContainer()->get('terminal42_node.manager')->generateMultiple($ids);
-
- if (0 === \count($this->nodes)) {
- return '';
- }
-
- return parent::generate();
- }
-
- /**
- * Generate a wildcard in the backend.
- */
- public static function generateBackendWildcard(array $data, array $ids): string
- {
- $nodes = [];
-
- $ids = array_map('intval', $ids);
-
- $nodeModels = NodeModel::findBy(
- ['id IN ('.implode(',', $ids).')', 'type=?'],
- [NodeModel::TYPE_CONTENT, implode(',', $ids)],
- ['order' => 'FIND_IN_SET(`id`, ?)'],
- );
-
- if (null !== $nodeModels) {
- $router = System::getContainer()->get('router');
-
- /** @var NodeModel $nodeModel */
- foreach ($nodeModels as $nodeModel) {
- $nodes[] = \sprintf(
- '%s (ID: %s)',
- $router->generate('contao_backend', ['do' => 'nodes', 'table' => 'tl_content', 'id' => $nodeModel->id]),
- $nodeModel->name,
- $nodeModel->id,
- );
- }
- }
-
- $wildcard = '### '.strtoupper($GLOBALS['TL_LANG']['FMD'][$data['type']][0]).' ###';
-
- // Add nodes
- if (\count($nodes) > 0) {
- $wildcard .= '
'.implode('
', $nodes).'
';
- }
-
- $template = new BackendTemplate('be_wildcard');
- $template->wildcard = $wildcard;
-
- return $template->parse();
- }
-
- /**
- * Generate the module.
- */
- protected function compile(): void
- {
- $this->Template->nodes = $this->nodes;
- }
-}
diff --git a/src/Controller/ContentElement/NodesController.php b/src/Controller/ContentElement/NodesController.php
new file mode 100644
index 0000000..8043b84
--- /dev/null
+++ b/src/Controller/ContentElement/NodesController.php
@@ -0,0 +1,42 @@
+isBackendScope($request)) {
+ return $this->generateNodesBackendResponse($model);
+ }
+
+ return $this->generateNodesResponse($template, $model);
+ }
+}
diff --git a/src/Controller/FrontendModule/NodesController.php b/src/Controller/FrontendModule/NodesController.php
new file mode 100644
index 0000000..95897ef
--- /dev/null
+++ b/src/Controller/FrontendModule/NodesController.php
@@ -0,0 +1,38 @@
+generateNodesResponse($template, $model);
+ }
+}
diff --git a/src/Controller/NodesTrait.php b/src/Controller/NodesTrait.php
new file mode 100644
index 0000000..40054a5
--- /dev/null
+++ b/src/Controller/NodesTrait.php
@@ -0,0 +1,97 @@
+nodes);
+
+ if (!\is_array($ids) || [] === $ids) {
+ return [];
+ }
+
+ return array_map(intval(...), $ids);
+ }
+
+ private function generateNodesResponse(FragmentTemplate $template, Model $model): Response
+ {
+ $ids = $this->getNodeIdsFromModel($model);
+
+ if ([] === $ids || $this->isCircularReference($model, $ids)) {
+ return new Response();
+ }
+
+ $nodes = $this->nodeManager->generateMultiple($ids);
+
+ if ([] === $nodes) {
+ return new Response();
+ }
+
+ $template->set('nodes', $nodes);
+ $template->set('nodes_wrapper', (bool) $model->nodesWrapper);
+
+ return $template->getResponse();
+ }
+
+ private function generateNodesBackendResponse(Model $model): Response
+ {
+ $ids = $this->getNodeIdsFromModel($model);
+
+ if ([] === $ids) {
+ return new Response();
+ }
+
+ if ($this->isCircularReference($model, $ids)) {
+ return new Response(\sprintf('%s', $this->container->get('translator')->trans('ERR.circularReference', [], 'contao_default')));
+ }
+
+ $nodeModels = NodeModel::findBy(
+ ['id IN ('.implode(',', $ids).')', 'type=?'],
+ [NodeModel::TYPE_CONTENT, implode(',', $ids)],
+ ['order' => 'FIND_IN_SET(`id`, ?)'],
+ );
+
+ $context = [
+ 'id' => $model->id,
+ 'nodes' => $nodeModels?->fetchAll() ?? [],
+ ];
+
+ if ($model instanceof ContentModel) {
+ $context['label'] = $this->container->get('translator')->trans('CTE.nodes.0', [], 'contao_default');
+ } elseif ($model instanceof ModuleModel) {
+ $context['label'] = $this->container->get('translator')->trans('FMD.nodes.0', [], 'contao_modules');
+ $context['name'] = $model->name;
+ $context['hrefParams'] = ['do' => 'themes', 'table' => 'tl_module', 'act' => 'edit', 'id' => $model->id, 'rt' => $this->container->get('contao.csrf.token_manager')->getDefaultTokenValue()];
+ }
+
+ return $this->render('@Contao/backend/nodes_wildcard.html.twig', $context);
+ }
+
+ private function isCircularReference(Model $model, array $ids): bool
+ {
+ if (!$model instanceof ContentModel) {
+ return false;
+ }
+
+ if ('tl_node' !== $model->ptable) {
+ return false;
+ }
+
+ return \in_array((int) $model->pid, $ids, true);
+ }
+}
diff --git a/src/DependencyInjection/Terminal42NodeExtension.php b/src/DependencyInjection/Terminal42NodeExtension.php
index dde26c5..4ebdf1b 100755
--- a/src/DependencyInjection/Terminal42NodeExtension.php
+++ b/src/DependencyInjection/Terminal42NodeExtension.php
@@ -7,15 +7,20 @@
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
-use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
+use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
use Terminal42\Geoip2CountryBundle\DependencyInjection\Configuration;
class Terminal42NodeExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container): void
{
- $loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../../config'));
- $loader->load('services.yml');
+ $loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../../config'));
+
+ $loader->load('controllers.php');
+ $loader->load('listeners.php');
+ $loader->load('migrations.php');
+ $loader->load('security.php');
+ $loader->load('services.php');
if (class_exists(Configuration::class)) {
Configuration::addDefaultTable('tl_node');
diff --git a/src/EventListener/ContentListener.php b/src/EventListener/ContentListener.php
index 5315e12..9013b08 100644
--- a/src/EventListener/ContentListener.php
+++ b/src/EventListener/ContentListener.php
@@ -4,89 +4,26 @@
namespace Terminal42\NodeBundle\EventListener;
-use Contao\CoreBundle\Exception\AccessDeniedException;
+use Contao\CoreBundle\DependencyInjection\Attribute\AsCallback;
use Contao\DataContainer;
-use Contao\Input;
use Contao\StringUtil;
use Doctrine\DBAL\Connection;
use Terminal42\NodeBundle\Model\NodeModel;
-use Terminal42\NodeBundle\PermissionChecker;
class ContentListener
{
- /**
- * ContentListener constructor.
- */
- public function __construct(
- private Connection $db,
- private PermissionChecker $permissionChecker,
- ) {
- }
-
- /**
- * On data container load callback.
- */
- public function onLoadCallback(DataContainer $dc): void
+ public function __construct(private readonly Connection $connection)
{
- switch (Input::get('act')) {
- case 'edit':
- case 'delete':
- case 'show':
- $nodeId = $this->db->fetchOne('SELECT pid FROM tl_content WHERE id=? AND ptable=?', [$dc->id, 'tl_node']);
- break;
-
- case 'paste':
- if ('create' === Input::get('mode')) {
- $nodeId = $dc->id;
- } else {
- $nodeId = $this->db->fetchOne('SELECT pid FROM tl_content WHERE id=? AND ptable=?', [$dc->id, 'tl_node']);
- }
- break;
-
- case 'create':
- case 'copy':
- case 'copyAll':
- case 'cut':
- case 'cutAll':
- if (1 === (int) Input::get('mode')) {
- $nodeId = $this->db->fetchOne('SELECT pid FROM tl_content WHERE id=? AND ptable=?', [Input::get('pid'), 'tl_node']);
- } else {
- $nodeId = Input::get('pid');
- }
- break;
-
- default:
- // Ajax requests such as toggle
- if (Input::get('field') && ($id = Input::get('cid') ?: Input::get('id'))) {
- $nodeId = $this->db->fetchOne('SELECT pid FROM tl_content WHERE id=? AND ptable=?', [$id, 'tl_node']);
- } else {
- $nodeId = $dc->id;
- }
- break;
- }
-
- $type = $this->db->fetchOne('SELECT type FROM tl_node WHERE id=?', [$nodeId]);
-
- // Throw an exception if the node is not present or is of a folder type
- if (!$type || NodeModel::TYPE_FOLDER === $type) {
- throw new AccessDeniedException('Node of folder type cannot have content elements');
- }
-
- $this->checkPermissions((int) $nodeId);
}
- /**
- * On nodes fields save callback.
- *
- * @throws \InvalidArgumentException
- */
+ #[AsCallback('tl_content', 'fields.nodes.save')]
public function onNodesSaveCallback(string|null $value, DataContainer $dc): string|null
{
$ids = (array) StringUtil::deserialize($value, true);
$ids = array_map('intval', $ids);
if (\count($ids) > 0) {
- $folders = $this->db->fetchAllAssociative('SELECT name FROM tl_node WHERE id IN ('.implode(', ', $ids).') AND type=?', [NodeModel::TYPE_FOLDER]);
+ $folders = $this->connection->fetchAllAssociative('SELECT name FROM tl_node WHERE id IN ('.implode(', ', $ids).') AND type=?', [NodeModel::TYPE_FOLDER]);
// Do not allow folder nodes
if (\count($folders) > 0) {
@@ -103,18 +40,4 @@ public function onNodesSaveCallback(string|null $value, DataContainer $dc): stri
return $value;
}
-
- /**
- * Check the permissions.
- */
- private function checkPermissions(int $nodeId): void
- {
- if (!$this->permissionChecker->hasUserPermission(PermissionChecker::PERMISSION_CONTENT)) {
- throw new AccessDeniedException('The user is not allowed to manage the node content');
- }
-
- if (!$this->permissionChecker->isUserAllowedNode($nodeId)) {
- throw new AccessDeniedException(\sprintf('The user is not allowed to manage the content of node ID %s', $nodeId));
- }
- }
}
diff --git a/src/EventListener/DataContainerListener.php b/src/EventListener/DataContainerListener.php
index 5f746a1..1809a43 100755
--- a/src/EventListener/DataContainerListener.php
+++ b/src/EventListener/DataContainerListener.php
@@ -8,11 +8,17 @@
use Codefog\TagsBundle\Manager\ManagerInterface;
use Codefog\TagsBundle\Tag;
use Contao\Backend;
+use Contao\BackendUser;
use Contao\Controller;
+use Contao\CoreBundle\DependencyInjection\Attribute\AsCallback;
+use Contao\CoreBundle\DependencyInjection\Attribute\AsHook;
use Contao\CoreBundle\Exception\AccessDeniedException;
use Contao\CoreBundle\Exception\ResponseException;
use Contao\CoreBundle\Intl\Locales;
use Contao\CoreBundle\Monolog\ContaoContext;
+use Contao\CoreBundle\Security\ContaoCorePermissions;
+use Contao\CoreBundle\Security\DataContainer\ReadAction;
+use Contao\CoreBundle\Twig\Finder\FinderFactory;
use Contao\DataContainer;
use Contao\Environment;
use Contao\Image;
@@ -21,15 +27,16 @@
use Contao\System;
use Contao\Validator;
use Doctrine\DBAL\Connection;
-use Haste\Model\Model;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
+use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBagInterface;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
+use Symfony\Contracts\Translation\TranslatorInterface;
use Terminal42\NodeBundle\Model\NodeModel;
-use Terminal42\NodeBundle\PermissionChecker;
+use Terminal42\NodeBundle\Security\NodePermissions;
use Terminal42\NodeBundle\Widget\NodePickerWidget;
class DataContainerListener
@@ -37,28 +44,26 @@ class DataContainerListener
public const BREADCRUMB_SESSION_KEY = 'tl_node_node';
public function __construct(
- private Connection $db,
- private Locales $locales,
- private LoggerInterface $logger,
- private PermissionChecker $permissionChecker,
- private RequestStack $requestStack,
- private ManagerInterface $tagsManager,
+ private readonly Connection $connection,
+ private readonly FinderFactory $finderFactory,
+ private readonly Locales $locales,
+ private readonly LoggerInterface $logger,
+ private readonly RequestStack $requestStack,
+ private readonly Security $security,
+ private readonly ManagerInterface $tagsManager,
+ private readonly TranslatorInterface $translator,
) {
}
- /**
- * On load callback.
- */
+ #[AsCallback('tl_node', 'config.onload')]
public function onLoadCallback(DataContainer $dc): void
{
$this->addBreadcrumb($dc);
- $this->checkPermissions($dc);
+ $this->checkPermissions();
$this->toggleSwitchToEditFlag($dc);
}
- /**
- * On paste button callback.
- */
+ #[AsCallback('tl_node', 'list.sorting.paste_button_callback')]
public function onPasteButtonCallback(DataContainer $dc, array $row, string $table, bool $cr, array|false|null $clipboard = null): string
{
$disablePA = false;
@@ -75,9 +80,8 @@ public function onPasteButtonCallback(DataContainer $dc, array $row, string $tab
$disablePI = true;
}
- // Disable "paste after" button if the parent node is a root node and the user is
- // not allowed
- if (!$disablePA && !$this->permissionChecker->hasUserPermission(PermissionChecker::PERMISSION_ROOT) && (!$row['pid'] || \in_array((int) $row['id'], $dc->rootIds, true))) {
+ // Disable "paste after" button if the parent node is a root node and the user is not allowed
+ if (!$disablePA && !$this->security->isGranted(NodePermissions::USER_CAN_CREATE_ROOT_NODES) && (!$row['pid'] || \in_array((int) $row['id'], $dc->rootIds, true))) {
$disablePA = true;
}
@@ -94,77 +98,13 @@ public function onPasteButtonCallback(DataContainer $dc, array $row, string $tab
return $return.($disablePI ? Image::getHtml('pasteinto_.svg').' ' : ''.$imagePasteInto.' ');
}
- /**
- * On "edit" button callback.
- */
- public function onEditButtonCallback(array $row, string $href, string $label, string $title, string $icon, string $attributes): string
- {
- return $this->generateButton($row, $href, $label, $title, $icon, $attributes, NodeModel::TYPE_FOLDER !== $row['type'] && $this->permissionChecker->hasUserPermission(PermissionChecker::PERMISSION_CONTENT));
- }
-
- /**
- * On "edit header" button callback.
- */
- public function onEditHeaderButtonCallback(array $row, string $href, string $label, string $title, string $icon, string $attributes): string
+ #[AsCallback('tl_node', 'list.operations.children.button')]
+ public function onChildrenButtonCallback(array $row, string $href, string $label, string $title, string $icon, string $attributes): string
{
- return $this->generateButton($row, $href, $label, $title, $icon, $attributes, $this->permissionChecker->hasUserPermission(PermissionChecker::PERMISSION_EDIT));
+ return $this->generateButton($row, $href, $label, $title, $icon, $attributes, NodeModel::TYPE_FOLDER !== $row['type']);
}
- /**
- * On "copy" button callback.
- */
- public function onCopyButtonCallback(array $row, string $href, string $label, string $title, string $icon, string $attributes, string $table): string
- {
- if ($GLOBALS['TL_DCA'][$table]['config']['closed'] ?? null) {
- return '';
- }
-
- return $this->generateButton($row, $href, $label, $title, $icon, $attributes, $this->permissionChecker->hasUserPermission(PermissionChecker::PERMISSION_CREATE));
- }
-
- /**
- * On "copy childs" button callback.
- */
- public function onCopyChildsButtonCallback(array $row, string $href, string $label, string $title, string $icon, string $attributes, string $table): string
- {
- if ($GLOBALS['TL_DCA'][$table]['config']['closed'] ?? null) {
- return '';
- }
-
- $active = (NodeModel::TYPE_FOLDER === $row['type']) && $this->permissionChecker->hasUserPermission(PermissionChecker::PERMISSION_CREATE);
-
- // Make the button active only if there are subnodes
- if ($active) {
- $active = $this->db->fetchOne("SELECT COUNT(*) FROM $table WHERE pid=?", [$row['id']]) > 0;
- }
-
- return $this->generateButton($row, $href, $label, $title, $icon, $attributes, $active);
- }
-
- /**
- * On "delete" button callback.
- */
- public function onDeleteButtonCallback(array $row, string $href, string $label, string $title, string $icon, string $attributes): string
- {
- $active = true;
-
- // Allow delete if the user has permission
- if (!$this->permissionChecker->isUserAdmin()) {
- $rootIds = (array) func_get_arg(7);
- $active = $this->permissionChecker->hasUserPermission(PermissionChecker::PERMISSION_DELETE);
-
- // If the node is a root one, check if the user has permission to manage it
- if ($active && \in_array((int) $row['id'], $rootIds, true)) {
- $active = $this->permissionChecker->hasUserPermission(PermissionChecker::PERMISSION_ROOT);
- }
- }
-
- return $this->generateButton($row, $href, $label, $title, $icon, $attributes, $active);
- }
-
- /**
- * On label callback.
- */
+ #[AsCallback('tl_node', 'list.label.label')]
public function onLabelCallback(array $row, string $label, DataContainer|null $dc = null, string $imageAttribute = '', bool $returnImage = false): string
{
$image = NodeModel::TYPE_CONTENT === $row['type'] ? 'articles.svg' : 'folderC.svg';
@@ -183,12 +123,7 @@ public function onLabelCallback(array $row, string $label, DataContainer|null $d
}
$tags = [];
-
- if (class_exists(DcaRelationsModel::class)) {
- $tagIds = DcaRelationsModel::getRelatedValues('tl_node', 'tags', $row['id']);
- } else {
- $tagIds = Model::getRelatedValues('tl_node', 'tags', $row['id']);
- }
+ $tagIds = DcaRelationsModel::getRelatedValues('tl_node', 'tags', $row['id']);
// Generate the tags
if (\count($tagIds) > 0) {
@@ -198,31 +133,67 @@ public function onLabelCallback(array $row, string $label, DataContainer|null $d
}
}
+ $extras = [];
+
+ if ([] !== $languages) {
+ $extras[] = implode(', ', $languages);
+ }
+
+ if ([] !== $tags) {
+ $extras[] = implode(', ', $tags);
+ }
+
+ if (NodeModel::TYPE_CONTENT === $row['type']) {
+ $extras[] = \sprintf('ID: %d', $row['id']);
+
+ if ($row['alias']) {
+ $extras[] = \sprintf('%s: %s', $GLOBALS['TL_LANG']['tl_node']['alias'][0], $row['alias']);
+ }
+ }
+
return \sprintf(
- '%s %s%s%s',
+ '%s %s%s',
Image::getHtml($image, '', $imageAttribute),
Backend::addToUrl('nn='.$row['id']),
StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['selectNode']),
$label,
- \count($languages) > 0 ? \sprintf(' [%s]', implode(', ', $languages)) : '',
- \count($tags) > 0 ? \sprintf(' [%s]', implode(', ', $tags)) : '',
+ $extras ? ' '.implode('', array_map(static fn (string $v) => \sprintf('[%s]', $v), $extras)) : '',
);
}
- /**
- * On languages options callback.
- */
+ #[AsCallback('tl_node', 'fields.languages.options')]
public function onLanguagesOptionsCallback(): array
{
return $this->locales->getLocales(null, true);
}
- /**
- * On execute the post actions.
- *
- * @param string $action
- */
- public function onExecutePostActions($action, DataContainer $dc): void
+ #[AsCallback('tl_node', 'fields.groups.options')]
+ public function onGroupsOptionsCallback(): array
+ {
+ $options = [-1 => $this->translator->trans('MSC.guests', [], 'contao_default')];
+ $groups = $this->connection->fetchAllAssociative('SELECT id, name FROM tl_member_group WHERE tstamp>0 ORDER BY name');
+
+ foreach ($groups as $group) {
+ $options[$group['id']] = $group['name'];
+ }
+
+ return $options;
+ }
+
+ #[AsCallback('tl_node', 'fields.nodeTpl.options')]
+ public function onNodeTplOptionsCallback(): array
+ {
+ return $this->finderFactory
+ ->create()
+ ->identifier('node')
+ ->extension('html.twig')
+ ->withVariants()
+ ->asTemplateOptions()
+ ;
+ }
+
+ #[AsHook('executePostActions')]
+ public function onExecutePostActions(string $action, DataContainer $dc): void
{
if ('reloadNodePickerWidget' === $action) {
$this->reloadNodePickerWidget($dc);
@@ -272,8 +243,8 @@ private function reloadNodePickerWidget(DataContainer $dc): void
$value = null;
// Load the value
- if ('overrideAll' !== Input::get('act') && $id > 0 && $this->db->createSchemaManager()->tablesExist([$dc->table])) {
- $row = $this->db->fetchAssociative("SELECT * FROM {$dc->table} WHERE id=?", [$id]);
+ if ('overrideAll' !== Input::get('act') && $id > 0 && $this->connection->createSchemaManager()->tablesExist([$dc->table])) {
+ $row = $this->connection->fetchAssociative("SELECT * FROM {$dc->table} WHERE id=?", [$id]);
// The record does not exist
if (!$row) {
@@ -324,110 +295,41 @@ private function reloadNodePickerWidget(DataContainer $dc): void
*/
private function toggleSwitchToEditFlag(DataContainer $dc): void
{
- if (!$dc->id || !$this->permissionChecker->hasUserPermission(PermissionChecker::PERMISSION_CONTENT)) {
+ if (!$dc->id) {
+ return;
+ }
+
+ if (!$this->security->isGranted(NodePermissions::USER_CAN_EDIT_NODE_CONTENT)) {
return;
}
- $type = $this->db->fetchOne('SELECT type FROM tl_node WHERE id=?', [$dc->id]);
+ $type = $this->connection->fetchOne('SELECT type FROM tl_node WHERE id=?', [$dc->id]);
if (NodeModel::TYPE_CONTENT === $type) {
$GLOBALS['TL_DCA'][$dc->table]['config']['switchToEdit'] = true;
}
}
- /**
- * Check the permissions.
- */
- private function checkPermissions(DataContainer $dc): void
+ private function checkPermissions(): void
{
- if ($this->permissionChecker->isUserAdmin()) {
- return;
- }
+ $user = $this->security->getUser();
- // Close the table if user is not allowed to create new records
- if (!$this->permissionChecker->hasUserPermission(PermissionChecker::PERMISSION_CREATE)) {
- $GLOBALS['TL_DCA'][$dc->table]['config']['closed'] = true;
- $GLOBALS['TL_DCA'][$dc->table]['config']['notCopyable'] = true;
- }
-
- // Set the flag if user is not allowed to edit records
- if (!$this->permissionChecker->hasUserPermission(PermissionChecker::PERMISSION_EDIT)) {
- $GLOBALS['TL_DCA'][$dc->table]['config']['notEditable'] = true;
+ if (!$user instanceof BackendUser) {
+ return;
}
- // Set the flag if user is not allowed to delete records
- if (!$this->permissionChecker->hasUserPermission(PermissionChecker::PERMISSION_DELETE)) {
- $GLOBALS['TL_DCA'][$dc->table]['config']['notDeletable'] = true;
+ if ($user->isAdmin) {
+ return;
}
- $session = $this->requestStack->getSession()->all();
-
- // Filter allowed page IDs
- if (\is_array($session['CURRENT']['IDS'] ?? null)) {
- $session['CURRENT']['IDS'] = $this->permissionChecker->filterAllowedIds(
- $session['CURRENT']['IDS'],
- 'deleteAll' === Input::get('act') ? PermissionChecker::PERMISSION_DELETE : PermissionChecker::PERMISSION_EDIT,
- );
-
- $this->requestStack->getSession()->replace($session);
+ // Set root IDs
+ if (empty($user->nodeMounts) || !\is_array($user->nodeMounts)) {
+ $root = [0];
+ } else {
+ $root = $user->nodeMounts;
}
- // Limit the allowed roots for the user
- if (null !== ($roots = $this->permissionChecker->getUserAllowedRoots())) {
- if (!empty($GLOBALS['TL_DCA'][$dc->table]['list']['sorting']['root']) && \is_array($GLOBALS['TL_DCA'][$dc->table]['list']['sorting']['root'])) {
- $GLOBALS['TL_DCA'][$dc->table]['list']['sorting']['root'] = array_intersect($GLOBALS['TL_DCA'][$dc->table]['list']['sorting']['root'], $roots);
- } else {
- $GLOBALS['TL_DCA'][$dc->table]['list']['sorting']['root'] = $roots;
- }
-
- // Allow root paste if the user has enough permission
- if ($this->permissionChecker->hasUserPermission(PermissionChecker::PERMISSION_ROOT)) {
- $GLOBALS['TL_DCA'][$dc->table]['list']['sorting']['rootPaste'] = true;
- }
-
- // Check current action
- if (($action = Input::get('act')) && 'paste' !== $action) {
- switch ($action) {
- case 'edit':
- $nodeId = (int) Input::get('id');
-
- // Dynamically add the record to the user profile
- if (!$this->permissionChecker->isUserAllowedNode($nodeId)) {
- /** @var AttributeBagInterface $sessionBag */
- $sessionBag = $this->requestStack->getSession()->getbag('contao_backend');
-
- $newRecords = $sessionBag->get('new_records');
- $newRecords = \is_array($newRecords[$dc->table]) ? array_map('intval', $newRecords[$dc->table]) : [];
-
- if (\in_array($nodeId, $newRecords, true)) {
- $this->permissionChecker->addNodeToAllowedRoots($nodeId);
- }
- }
- // no break;
-
- case 'copy':
- case 'delete':
- case 'show':
- if (!isset($nodeId)) {
- $nodeId = (int) Input::get('id');
- }
-
- if (!$this->permissionChecker->isUserAllowedNode($nodeId)) {
- throw new AccessDeniedException(\sprintf('Not enough permissions to %s node ID %s.', $action, $nodeId));
- }
- break;
-
- case 'editAll':
- case 'deleteAll':
- case 'overrideAll':
- if (\is_array($session['CURRENT']['IDS'])) {
- $session['CURRENT']['IDS'] = array_intersect($session['CURRENT']['IDS'], $roots);
- $this->requestStack->getSession()->replace($session);
- }
- break;
- }
- }
- }
+ $GLOBALS['TL_DCA']['tl_node']['list']['sorting']['root'] = $root;
}
/**
@@ -448,7 +350,7 @@ private function addBreadcrumb(DataContainer $dc): void
}
$session->set(self::BREADCRUMB_SESSION_KEY, Input::get('nn', true));
- Controller::redirect(preg_replace('/&nn=[^&]*/', '', Environment::get('request')));
+ Controller::redirect(preg_replace('/&nn=[^&]*/', '', (string) Environment::get('request')));
}
if (($nodeId = $session->get(self::BREADCRUMB_SESSION_KEY)) < 1) {
@@ -468,7 +370,7 @@ private function addBreadcrumb(DataContainer $dc): void
$id = $nodeId;
do {
- $node = $this->db->fetchAssociative("SELECT * FROM {$dc->table} WHERE id=?", [$id]);
+ $node = $this->connection->fetchAssociative("SELECT * FROM {$dc->table} WHERE id=?", [$id]);
if (!$node) {
// Currently selected node does not exist
@@ -491,7 +393,7 @@ private function addBreadcrumb(DataContainer $dc): void
}
// Do not show the mounted nodes
- if (!$this->permissionChecker->isUserAdmin() && $this->permissionChecker->isUserAllowedRootNode($node['id'])) {
+ if (!$this->security->isGranted(ContaoCorePermissions::DC_PREFIX.$dc->table, new ReadAction($dc->table, $node))) {
break;
}
@@ -500,10 +402,12 @@ private function addBreadcrumb(DataContainer $dc): void
}
// Check whether the node is mounted
- if (!$this->permissionChecker->isUserAllowedRootNode($ids)) {
- $session->set(self::BREADCRUMB_SESSION_KEY, 0);
+ foreach ($ids as $id) {
+ if (!$this->security->isGranted(ContaoCorePermissions::DC_PREFIX.$dc->table, new ReadAction($dc->table, ['id' => $id]))) {
+ $session->set(self::BREADCRUMB_SESSION_KEY, 0);
- throw new AccessDeniedException('Node ID '.$nodeId.' is not mounted.');
+ throw new AccessDeniedException('Node ID '.$nodeId.' is not mounted.');
+ }
}
// Limit tree
diff --git a/src/EventListener/InsertTagsListener.php b/src/EventListener/InsertTagsListener.php
deleted file mode 100755
index 6e1a875..0000000
--- a/src/EventListener/InsertTagsListener.php
+++ /dev/null
@@ -1,74 +0,0 @@
-manager->generateSingle((int) $ids[0])) {
- $this->logError($ids, $tag);
- }
-
- return $buffer;
- }
-
- $nodes = $this->manager->generateMultiple($ids);
- $invalid = array_keys(array_diff_key(array_flip($ids), $nodes));
-
- if (!empty($invalid)) {
- $this->logError($invalid, $tag);
- }
-
- return implode("\n", $nodes);
- }
-
- private function logError(array $ids, string $tag): void
- {
- if (null === $this->logger) {
- return;
- }
-
- $this->logger->error(
- 'Invalid nodes ('.implode(', ', $ids).') in insert tag ('.$tag.') on page '.Environment::get('uri'),
- ['contao' => new ContaoContext(self::class, ContaoContext::ERROR)],
- );
- }
-}
diff --git a/src/FrontendModule/NodesModule.php b/src/FrontendModule/NodesModule.php
deleted file mode 100644
index 030afef..0000000
--- a/src/FrontendModule/NodesModule.php
+++ /dev/null
@@ -1,68 +0,0 @@
-objModel->nodes, true))) {
- return '';
- }
-
- /** @var Request $request */
- $request = System::getContainer()->get('request_stack')->getCurrentRequest();
-
- // Display the backend wildcard
- if (null !== $request) {
- /** @var ScopeMatcher $scopeMatcher */
- $scopeMatcher = System::getContainer()->get('contao.routing.scope_matcher');
-
- if ($scopeMatcher->isBackendRequest($request)) {
- return NodesContentElement::generateBackendWildcard($this->arrData, $ids);
- }
- }
-
- $this->nodes = System::getContainer()->get('terminal42_node.manager')->generateMultiple($ids);
-
- if (0 === \count($this->nodes)) {
- return '';
- }
-
- return parent::generate();
- }
-
- /**
- * Generate the module.
- */
- protected function compile(): void
- {
- $this->Template->nodes = $this->nodes;
- }
-}
diff --git a/src/InsertTag/NodeInsertTag.php b/src/InsertTag/NodeInsertTag.php
new file mode 100644
index 0000000..9802f96
--- /dev/null
+++ b/src/InsertTag/NodeInsertTag.php
@@ -0,0 +1,69 @@
+getParameters()->get(0));
+ $total = \count($ids);
+
+ if (0 === $total) {
+ return new InsertTagResult('');
+ }
+
+ if (1 === $total) {
+ $buffer = $this->manager->generateSingle($ids[0]);
+
+ if (null === $buffer) {
+ $this->logError($insertTag, $ids);
+
+ return new InsertTagResult('');
+ }
+
+ return new InsertTagResult($buffer);
+ }
+
+ $nodes = $this->manager->generateMultiple($ids);
+ $invalid = array_keys(array_diff_key(array_flip($ids), $nodes));
+
+ if (!empty($invalid)) {
+ $this->logError($insertTag, $invalid);
+ }
+
+ return new InsertTagResult(implode("\n", $nodes));
+ }
+
+ private function logError(ResolvedInsertTag $insertTag, array $ids): void
+ {
+ if (null === $this->logger) {
+ return;
+ }
+
+ $this->logger->error(
+ 'Invalid nodes ('.implode(', ', $ids).') in insert tag ('.$insertTag->getName().') on page '.Environment::get('uri'),
+ ['contao' => new ContaoContext(self::class, ContaoContext::ERROR)],
+ );
+ }
+}
diff --git a/src/Migration/GuestsMigration.php b/src/Migration/GuestsMigration.php
new file mode 100644
index 0000000..084ac17
--- /dev/null
+++ b/src/Migration/GuestsMigration.php
@@ -0,0 +1,15 @@
+|null
*/
public function getContentElements(): Collection|null
diff --git a/src/NodeElement.php b/src/NodeElement.php
new file mode 100644
index 0000000..5a5822e
--- /dev/null
+++ b/src/NodeElement.php
@@ -0,0 +1,24 @@
+row;
+ }
+
+ public function getRenderedHtml(): string
+ {
+ return $this->renderedHtml;
+ }
+}
diff --git a/src/NodeManager.php b/src/NodeManager.php
index e44132c..6360c52 100644
--- a/src/NodeManager.php
+++ b/src/NodeManager.php
@@ -6,96 +6,112 @@
use Contao\ContentModel;
use Contao\Controller;
-use Contao\FrontendTemplate;
use Contao\StringUtil;
use Terminal42\NodeBundle\Model\NodeModel;
+use Twig\Environment;
class NodeManager
{
- /**
- * Generate single node.
- */
- public function generateSingle(int $id): string|null
+ public function __construct(private readonly Environment $twig)
{
- if (!$id) {
+ }
+
+ public function generateSingle(int|string $idOrAlias): string|null
+ {
+ if (!$idOrAlias) {
return null;
}
- if (null === ($nodeModel = NodeModel::findOneBy(['id=?', 'type=?'], [$id, NodeModel::TYPE_CONTENT]))) {
+ if (null === ($nodeModel = NodeModel::findOneBy(['(id=? OR alias=?)', 'type=?'], [$idOrAlias, $idOrAlias, NodeModel::TYPE_CONTENT]))) {
return null;
}
return $this->generateBuffer($nodeModel);
}
- /**
- * Generate multiple nodes.
- */
- public function generateMultiple(array $ids): array
+ public function generateMultiple(array $idsOrAliases): array
{
- $ids = array_filter($ids);
+ $idsOrAliases = array_values(array_filter($idsOrAliases));
+
+ if ([] === $idsOrAliases) {
+ return [];
+ }
+
+ $aliases = array_values(array_filter($idsOrAliases, static fn ($v) => \is_string($v) && !is_numeric($v)));
+ $ids = array_map(intval(...), array_values(array_diff($idsOrAliases, $aliases)));
- if (0 === \count($ids)) {
+ if ([] === $aliases && [] === $ids) {
return [];
}
- $ids = array_map('intval', $ids);
+ $columns = ['type=?'];
+ $values = [NodeModel::TYPE_CONTENT];
- $nodeModels = NodeModel::findBy(
- ['id IN ('.implode(',', $ids).')', 'type=?'],
- [NodeModel::TYPE_CONTENT, implode(',', $ids)],
- ['order' => 'FIND_IN_SET(`id`, ?)'],
- );
+ if ([] !== $aliases) {
+ $columns[] = "alias IN ('".implode("','", $aliases)."')";
+ }
+
+ if ([] !== $ids) {
+ $columns[] = 'id IN ('.implode(',', $ids).')';
+ }
- if (null === $nodeModels) {
+ if (null === ($nodeModels = NodeModel::findBy($columns, $values))) {
return [];
}
- $nodes = [];
+ $sortedNodeModels = [];
/** @var NodeModel $nodeModel */
foreach ($nodeModels as $nodeModel) {
+ // No strict check here
+ $sortedNodeModels[array_search($nodeModel->alias ?: $nodeModel->id, $idsOrAliases, false)] = $nodeModel;
+ }
+
+ $nodes = [];
+
+ /** @var NodeModel $nodeModel */
+ foreach ($sortedNodeModels as $nodeModel) {
$nodes[$nodeModel->id] = $this->generateBuffer($nodeModel);
}
- return array_filter($nodes, static fn ($buffer) => null !== $buffer);
+ return array_filter($nodes, static fn ($buffer) => '' !== $buffer);
}
- /**
- * Generate the node buffer (content elements).
- */
private function generateBuffer(NodeModel $nodeModel): string
{
if (!Controller::isVisibleElement($nodeModel)) {
return '';
}
- $buffer = '';
- $elementsData = [];
+ $nodeElements = [];
if (null !== ($elements = $nodeModel->getContentElements())) {
/** @var ContentModel $element */
foreach ($elements as $index => $element) {
- $elementsData[] = $element->row();
+ // Keep the index if somebody wants to refer that inside the generated content element
$element->nodeElementIndex = $index;
- $buffer .= Controller::getContentElement($element);
+
+ $nodeElements[] = new NodeElement($element->row(), Controller::getContentElement($element));
}
}
if (!$nodeModel->wrapper) {
- return $buffer;
+ return implode('', array_map(static fn (NodeElement $v) => $v->getRenderedHtml(), $nodeElements));
}
- $template = new FrontendTemplate($nodeModel->nodeTpl ?: 'node_default');
- $template->setData($nodeModel->row());
- $template->elementsData = $elementsData;
-
$cssID = StringUtil::deserialize($nodeModel->cssID, true);
- $template->class = !empty($cssID[1]) ? $cssID[1] : '';
- $template->cssID = !empty($cssID[0]) ? $cssID[0] : '';
- $template->buffer = $buffer;
+ if ($nodeModel->nodeTpl) {
+ $templateName = \sprintf('@Contao/%s.html.twig', $nodeModel->nodeTpl);
+ } else {
+ $templateName = '@Contao/node/default.html.twig';
+ }
- return $template->parse();
+ return $this->twig->render($templateName, [
+ ...$nodeModel->row(),
+ 'elements' => $nodeElements,
+ 'class' => !empty($cssID[1]) ? $cssID[1] : '',
+ 'cssID' => !empty($cssID[0]) ? $cssID[0] : '',
+ ]);
}
}
diff --git a/src/PermissionChecker.php b/src/PermissionChecker.php
deleted file mode 100644
index 7d60857..0000000
--- a/src/PermissionChecker.php
+++ /dev/null
@@ -1,178 +0,0 @@
-authorizationChecker->isGranted('ROLE_ADMIN');
- }
-
- /**
- * Return true if the user has permission.
- */
- public function hasUserPermission(string $permission): bool
- {
- $isGranted = $this->authorizationChecker->isGranted('contao_user.nodePermissions.'.$permission);
-
- // If the user is able to create records, he is automatically able to edit them
- if (!$isGranted && self::PERMISSION_EDIT === $permission) {
- return $this->hasUserPermission(self::PERMISSION_CREATE);
- }
-
- return $isGranted;
- }
-
- /**
- * Get the user allowed roots. Return null if the user has no limitation.
- */
- public function getUserAllowedRoots(): array|null
- {
- if ($this->isUserAdmin()) {
- return null;
- }
-
- $ids = (array) $this->getUser()->nodeMounts;
-
- if (empty($ids)) {
- return null;
- }
-
- return array_map('intval', $ids);
- }
-
- /**
- * Check whether at least one of the given IDs is in the allowed root nodes.
- *
- * @param int|array $nodeId
- */
- public function isUserAllowedRootNode(array|int $nodeId): bool
- {
- if (null === ($roots = $this->getUserAllowedRoots())) {
- return true;
- }
-
- if (!\is_array($nodeId)) {
- $nodeId = [$nodeId];
- }
-
- return (bool) array_intersect($nodeId, $roots);
- }
-
- /**
- * Return if the user is allowed to manage the node.
- */
- public function isUserAllowedNode(int $nodeId): bool
- {
- if (null === ($roots = $this->getUserAllowedRoots())) {
- return true;
- }
-
- // Return true if the node is a root one and user has permission to manage those
- if (\in_array($nodeId, $roots, true) && $this->hasUserPermission(self::PERMISSION_ROOT)) {
- return true;
- }
-
- $ids = Database::getInstance()->getChildRecords($roots, 'tl_node', false, $roots);
- $ids = array_map('intval', $ids);
-
- return \in_array($nodeId, $ids, true);
- }
-
- /**
- * Add the node to allowed roots.
- */
- public function addNodeToAllowedRoots(int $nodeId): void
- {
- if (null === ($roots = $this->getUserAllowedRoots())) {
- return;
- }
-
- $user = $this->getUser();
-
- // Add the permissions on group level
- if ('custom' !== $user->inherit) {
- $groups = $this->db->fetchAllAssociative('SELECT id, nodeMounts, nodePermissions FROM tl_user_group WHERE id IN('.implode(',', array_map('intval', $user->groups)).')');
-
- foreach ($groups as $group) {
- $permissions = StringUtil::deserialize($group['nodePermissions'], true);
-
- if (\in_array(self::PERMISSION_CREATE, $permissions, true)) {
- $nodeIds = (array) StringUtil::deserialize($group['nodeMounts'], true);
- $nodeIds[] = $nodeId;
-
- $this->db->update('tl_user_group', ['nodeMounts' => serialize($nodeIds)], ['id' => $group['id']]);
- }
- }
- }
-
- // Add the permissions on user level
- if ('group' !== $user->inherit) {
- $userData = $this->db->fetchAssociative('SELECT nodePermissions, nodeMounts FROM tl_user WHERE id=?', [$user->id]);
- $permissions = StringUtil::deserialize($userData['nodePermissions'], true);
-
- if (\in_array(self::PERMISSION_CREATE, $permissions, true)) {
- $nodeIds = (array) StringUtil::deserialize($userData['nodeMounts'], true);
- $nodeIds[] = $nodeId;
-
- $this->db->update('tl_user', ['nodeMounts' => serialize($nodeIds)], ['id' => $user->id]);
- }
- }
-
- // Add the new element to the user object
- $user->nodeMounts = array_merge($roots, [$nodeId]);
- }
-
- /**
- * Filter the allowed IDs.
- */
- public function filterAllowedIds(array $ids, string $permission): array
- {
- if (0 === \count($ids) || !$this->hasUserPermission($permission)) {
- return [];
- }
-
- return array_filter(array_map('intval', $ids), [$this, 'isUserAllowedNode']);
- }
-
- private function getUser(): BackendUser
- {
- $user = $this->tokenStorage->getToken()?->getUser();
-
- if (!$user instanceof BackendUser) {
- throw new \RuntimeException('The token does not contain a back end user object');
- }
-
- return $user;
- }
-}
diff --git a/src/Picker/NodePickerProvider.php b/src/Picker/NodePickerProvider.php
index 5cbef14..606379a 100644
--- a/src/Picker/NodePickerProvider.php
+++ b/src/Picker/NodePickerProvider.php
@@ -4,10 +4,12 @@
namespace Terminal42\NodeBundle\Picker;
+use Contao\CoreBundle\DependencyInjection\Attribute\AsPickerProvider;
use Contao\CoreBundle\Picker\AbstractPickerProvider;
use Contao\CoreBundle\Picker\DcaPickerProviderInterface;
use Contao\CoreBundle\Picker\PickerConfig;
+#[AsPickerProvider(priority: 132)]
class NodePickerProvider extends AbstractPickerProvider implements DcaPickerProviderInterface
{
public function getDcaTable(PickerConfig|null $config = null): string
@@ -34,10 +36,7 @@ public function getDcaAttributes(PickerConfig $config): array
return $attributes;
}
- /**
- * @param string|int $value
- */
- public function convertDcaValue(PickerConfig $config, $value): int|string
+ public function convertDcaValue(PickerConfig $config, mixed $value): int|string
{
return (int) $value;
}
@@ -47,12 +46,7 @@ public function getName(): string
return 'nodePicker';
}
- /**
- * Do not add "string" parameter type for compatibility with Contao 4.13.
- *
- * @param string $context
- */
- public function supportsContext($context): bool
+ public function supportsContext(string $context): bool
{
return 'node' === $context;
}
diff --git a/src/Security/NodePermissions.php b/src/Security/NodePermissions.php
new file mode 100644
index 0000000..554a252
--- /dev/null
+++ b/src/Security/NodePermissions.php
@@ -0,0 +1,22 @@
+nodeMountsCache = [];
+
+ if ($this->inner instanceof ResetInterface) {
+ $this->inner->reset();
+ }
+ }
+
+ public function supportsAttribute(string $attribute): bool
+ {
+ if ($this->inner instanceof CacheableVoterInterface) {
+ return $this->inner->supportsAttribute($attribute);
+ }
+
+ return false;
+ }
+
+ public function supportsType(string $subjectType): bool
+ {
+ if ($this->inner instanceof CacheableVoterInterface) {
+ return $this->inner->supportsType($subjectType);
+ }
+
+ return false;
+ }
+
+ public function vote(TokenInterface $token, mixed $subject, array $attributes, Vote|null $vote = null): int
+ {
+ foreach ($attributes as $attribute) {
+ if (NodePermissions::USER_CAN_ACCESS_NODE === $attribute) {
+ return $this->voteOnAttribute($token, $subject, $attribute) ? self::ACCESS_GRANTED : self::ACCESS_DENIED;
+ }
+ }
+
+ return $this->inner->vote($token, $subject, $attributes);
+ }
+
+ private function voteOnAttribute(TokenInterface $token, mixed $subject, string $attribute): bool
+ {
+ $user = $token->getUser();
+
+ if (!$user instanceof BackendUser) {
+ return false;
+ }
+
+ if ($user->isAdmin) {
+ return true;
+ }
+
+ $permission = explode('.', $attribute, 3);
+
+ if ('contao_user' !== $permission[0] || !isset($permission[1])) {
+ return false;
+ }
+
+ $field = $permission[1];
+
+ if (null === $subject) {
+ return \is_array($user->$field) && [] !== $user->$field;
+ }
+
+ if (!\is_scalar($subject) && !\is_array($subject)) {
+ return false;
+ }
+
+ if (!\is_array($subject)) {
+ $subject = [$subject];
+ }
+
+ if (\is_array($user->$field) && array_intersect($subject, $user->$field)) {
+ return true;
+ }
+
+ if (!isset($this->nodeMountsCache[$user->id]) || (!empty($this->nodeMountsCache[$user->id]) && !array_intersect($subject, $this->nodeMountsCache[$user->id]))) {
+ $database = $this->contaoFramework->createInstance(Database::class);
+ $this->nodeMountsCache[$user->id] = $database->getChildRecords($user->$field, 'tl_node');
+ }
+
+ return !empty($this->nodeMountsCache[$user->id]) && array_intersect($subject, $this->nodeMountsCache[$user->id]);
+ }
+}
diff --git a/src/Security/Voter/NodeContentVoter.php b/src/Security/Voter/NodeContentVoter.php
new file mode 100644
index 0000000..3557c71
--- /dev/null
+++ b/src/Security/Voter/NodeContentVoter.php
@@ -0,0 +1,54 @@
+accessDecisionManager->decide($token, [NodePermissions::USER_CAN_ACCESS_MODULE])) {
+ return false;
+ }
+
+ if (!$this->accessDecisionManager->decide($token, [NodePermissions::USER_CAN_EDIT_NODE_CONTENT])) {
+ return false;
+ }
+
+ if (!$this->accessDecisionManager->decide($token, [NodePermissions::USER_CAN_ACCESS_NODE], $id)) {
+ return false;
+ }
+
+ $type = $this->connection->fetchOne('SELECT type FROM tl_node WHERE id = ?', [$id]);
+
+ if (NodeModel::TYPE_FOLDER === $type) {
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/src/Security/Voter/NodePermissionVoter.php b/src/Security/Voter/NodePermissionVoter.php
new file mode 100644
index 0000000..d3abf3e
--- /dev/null
+++ b/src/Security/Voter/NodePermissionVoter.php
@@ -0,0 +1,159 @@
+accessDecisionManager->decide($token, ['ROLE_ADMIN'])) {
+ return true;
+ }
+
+ if (!$this->accessDecisionManager->decide($token, [NodePermissions::USER_CAN_ACCESS_MODULE])) {
+ return false;
+ }
+
+ return match (true) {
+ $action instanceof CreateAction => $this->canCreate($action, $token),
+ $action instanceof ReadAction => $this->canRead($action, $token),
+ $action instanceof UpdateAction => $this->canUpdate($action, $token),
+ default => $this->canDelete($action, $token), // DeleteAction
+ };
+ }
+
+ private function canCreate(CreateAction $action, TokenInterface $token): bool
+ {
+ if (!$this->accessDecisionManager->decide($token, [NodePermissions::USER_CAN_CREATE_NODE])) {
+ return false;
+ }
+
+ $newAction = $action->getNew();
+
+ // The copy operation is allowed if edit is allowed.
+ if (null !== $newAction && null === ($newAction['sorting'] ?? null)) {
+ $nodeId = $action->getNewId();
+
+ return $this->canEdit($token, $nodeId) && $this->canCreate(new CreateAction($action->getDataSource()), $token);
+ }
+
+ // Check access to any node for the "new" operation.
+ if (null === $action->getNewPid()) {
+ $nodeIds = $this->getNodeMounts($token);
+ } else {
+ $nodeIds = [(int) $action->getNewPid()];
+ }
+
+ // To create a record, the edit permissions must be available.
+ foreach ($nodeIds as $nodeId) {
+ if ($this->canEdit($token, $nodeId)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private function canRead(ReadAction $action, TokenInterface $token): bool
+ {
+ return $this->canAccessNode($token, $action->getCurrentId());
+ }
+
+ private function canUpdate(UpdateAction $action, TokenInterface $token): bool
+ {
+ $nodeId = $action->getCurrentId();
+
+ if (!$this->canAccessNode($token, $nodeId)) {
+ return false;
+ }
+
+ $newRecord = $action->getNew();
+
+ // Edit operation
+ if (null === $newRecord) {
+ return $this->canEdit($token, $nodeId);
+ }
+
+ // Move existing record
+ $changeSorting = \array_key_exists('sorting', $newRecord);
+ $changePid = \array_key_exists('pid', $newRecord) && $action->getCurrentPid() !== $action->getNewPid();
+
+ if (($changeSorting || $changePid) && !$this->canEdit($token, $nodeId)) {
+ return false;
+ }
+
+ if ($changePid && !$this->canEdit($token, $action->getNewPid())) {
+ return false;
+ }
+
+ unset($newRecord['pid'], $newRecord['sorting'], $newRecord['tstamp']);
+
+ // Record was possibly only moved (pid, sorting), no need to check edit permissions
+ if ([] === array_diff($newRecord, $action->getCurrent())) {
+ return true;
+ }
+
+ return $this->accessDecisionManager->decide($token, [NodePermissions::USER_CAN_EDIT_NODE]);
+ }
+
+ private function canEdit(TokenInterface $token, string|null $nodeId): bool
+ {
+ return $this->accessDecisionManager->decide($token, [NodePermissions::USER_CAN_ACCESS_NODE], $nodeId)
+ && $this->accessDecisionManager->decide($token, [NodePermissions::USER_CAN_EDIT_NODE]);
+ }
+
+ private function canDelete(DeleteAction $action, TokenInterface $token): bool
+ {
+ return $this->accessDecisionManager->decide($token, [NodePermissions::USER_CAN_ACCESS_NODE], $action->getCurrentId())
+ && $this->accessDecisionManager->decide($token, [NodePermissions::USER_CAN_DELETE_NODE]);
+ }
+
+ private function canAccessNode(TokenInterface $token, string|null $nodeId): bool
+ {
+ return $this->accessDecisionManager->decide($token, [NodePermissions::USER_CAN_ACCESS_NODE], $nodeId);
+ }
+
+ private function getNodeMounts(TokenInterface $token): array
+ {
+ $user = $token->getUser();
+
+ if (!$user instanceof BackendUser) {
+ return [];
+ }
+
+ if (isset($this->nodeMountsCache[$user->id])) {
+ return $this->nodeMountsCache[$user->id];
+ }
+
+ $database = $this->contaoFramework->createInstance(Database::class);
+
+ return $this->nodeMountsCache[$user->id] = $database->getChildRecords($user->nodeMounts, 'tl_node', false, $user->nodeMounts);
+ }
+}
diff --git a/src/Widget/NodePickerWidget.php b/src/Widget/NodePickerWidget.php
index b559ec5..51985ba 100644
--- a/src/Widget/NodePickerWidget.php
+++ b/src/Widget/NodePickerWidget.php
@@ -35,12 +35,7 @@ class NodePickerWidget extends Widget
*/
protected $strTemplate = 'be_widget';
- /**
- * Generate the widget and return it as string.
- *
- * @return string
- */
- public function generate()
+ public function generate(): string
{
$container = System::getContainer();
$values = [];
@@ -48,7 +43,7 @@ public function generate()
// Can be an array
if (!empty($this->varValue) && null !== ($nodes = NodeModel::findMultipleByIds((array) $this->varValue))) {
/** @var DataContainerListener $eventListener */
- $eventListener = $container->get('terminal42_node.listener.data_container');
+ $eventListener = $container->get(DataContainerListener::class);
/** @var NodeModel $node */
foreach ($nodes as $node) {
@@ -106,18 +101,15 @@ public function generate()
return ''.$return.'
';
}
- /**
- * Return an array if the "multiple" attribute is set.
- */
- protected function validator($input)
+ protected function validator(mixed $varInput): array|int|string
{
- $this->checkValue($input);
+ $this->checkValue($varInput);
if ($this->hasErrors()) {
return '';
}
- if (!$input) {
+ if (!$varInput) {
if ($this->mandatory) {
$this->addError(\sprintf($GLOBALS['TL_LANG']['ERR']['mandatory'], $this->strLabel));
}
@@ -125,21 +117,16 @@ protected function validator($input)
return '';
}
- if (!str_contains($input, ',')) {
- return $this->multiple ? [(int) $input] : (int) $input;
+ if (!str_contains((string) $varInput, ',')) {
+ return $this->multiple ? [(int) $varInput] : (int) $varInput;
}
- $value = array_map('intval', array_filter(explode(',', $input)));
+ $value = array_map('intval', array_filter(explode(',', (string) $varInput)));
return $this->multiple ? $value : $value[0];
}
- /**
- * Check the selected value.
- *
- * @param string $input
- */
- protected function checkValue($input): void
+ protected function checkValue(string $input): void
{
if ('' === $input || !\is_array($this->rootNodes)) {
return;