Security: verwijder hardcoded wachtwoord, voeg random-wachtwoord-generator toe bij eerste installatie
This commit is contained in:
@@ -6,25 +6,33 @@
|
||||
- files: array of nodes [{name, path, is_dir, extension, size, modified, children}]
|
||||
- relFile: currently selected relative file path (for active highlight + auto-expand)
|
||||
- editableExts: array of editable extensions (shown as .EXT badge)
|
||||
- csrf_token: CSRF token for delete forms
|
||||
- treeIdPrefix: prefix for collapse element ids (e.g. 'content-tree', 'plugin-tree', 'theme-tree')
|
||||
- treeRouteBase: admin route base used to build file links, e.g. '/admin/content'
|
||||
- treeRouteParams: extra query string for file links, e.g. 'plugin=MyPlugin&' or 'theme=default&' or '' (content)
|
||||
- treeDeleteRoute: admin route for file delete, e.g. '/admin/content-file-delete'
|
||||
- treeDeleteName: optional hidden field name for delete form (e.g. 'plugin' or 'theme'); emits no field when empty
|
||||
- treeDeleteValue: optional hidden field value for treeDeleteName (e.g. the plugin/theme name)
|
||||
- treeMoveRoute: admin route for file move, e.g. '/admin/plugins-file-move'
|
||||
- treeMoveParams: extra query string for move link, e.g. 'plugin=X&' or ''
|
||||
- treeDirActions: boolean; show per-folder new-file/new-dir/rename/delete buttons (content editor only)
|
||||
- treeDirActions: boolean; show per-folder new-file/new-dir/rename buttons (default true)
|
||||
- treeFileMove: boolean; show per-file move/rename pencil button (default true)
|
||||
- treeHideActionsFor: array of relative paths where move/delete actions are hidden (e.g. ['theme.json'])
|
||||
- treeDirSelectRoute: if set, folder names are links to ?dir=<path> (map-detail pane)
|
||||
- treeDirRenameRoute: if set, show a rename pencil per folder
|
||||
- treeDeleteBase: POST URL for file deletion (scope-specific)
|
||||
- treeScope: 'content' | 'theme' | 'plugin' (for tree-interactions.js)
|
||||
- treeMoveUrl: AJAX endpoint for drag-drop move (e.g. '/admin/tree-move')
|
||||
- treeScopeParam: extra query param string (e.g. 'plugin=MyPlugin&' or 'theme=default&' or '')
|
||||
|
||||
Icon maps are chosen automatically from the node extension. #}
|
||||
{% macro tree(nodes, opts) %}
|
||||
{% import _self as macros %}
|
||||
<ul class="editor-tree">
|
||||
{% for n in nodes %}
|
||||
{% set isActive = n.path == opts.relFile %}
|
||||
{% set containsActive = n.is_dir and opts.relFile starts with (n.path ~ '/') %}
|
||||
{# Een bestand is alleen actief wanneer er geen map geselecteerd is
|
||||
(een map-selectie opent het map-detail paneel, geen bestand). #}
|
||||
{% set isActive = n.path == opts.relFile and opts.selectedDir|default('') == '' %}
|
||||
{# Markeer de oudermap "bevat actieve selectie" voor auto-uitklappen:
|
||||
bij een geselecteerd bestand OF een geselecteerde (sub)map. #}
|
||||
{% set activePath = opts.selectedDir|default('') != '' ? opts.selectedDir : opts.relFile %}
|
||||
{% set containsActive = n.is_dir and activePath != '' and activePath starts with (n.path ~ '/') %}
|
||||
{% if n.is_dir %}
|
||||
{% set icon = 'bi-folder-fill text-warning' %}
|
||||
{% elseif n.extension == 'md' %}
|
||||
@@ -48,37 +56,42 @@
|
||||
{% else %}
|
||||
{% set icon = 'bi-file text-muted' %}
|
||||
{% endif %}
|
||||
<li>
|
||||
<li data-path="{{ n.path }}" data-isdir="{{ n.is_dir ? '1' : '0' }}">
|
||||
{% if n.is_dir %}
|
||||
{% set collapseId = opts.treeIdPrefix ~ '-' ~ n.path|replace({'/':'-','.':'-'}) %}
|
||||
<div class="tree-dir-row">
|
||||
<button type="button" class="tree-link tree-toggle btn-toggle {{ containsActive ? '' : 'collapsed' }}"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#{{ collapseId }}"
|
||||
aria-expanded="{{ containsActive ? 'true' : 'false' }}"
|
||||
title="{{ n.path }}">
|
||||
<div class="tree-dir-row" data-dir="{{ n.path }}">
|
||||
{# Chevron: expand/collapse only (zonder navigatie) #}
|
||||
<button type="button" class="tree-chevron-btn" data-bs-toggle="collapse" data-bs-target="#{{ collapseId }}" aria-expanded="{{ containsActive ? 'true' : 'false' }}" title="{{ ta.expand_collapse|default('Uitklappen/inklappen') }}" aria-label="{{ ta.expand_collapse|default('Uitklappen/inklappen') }}">
|
||||
<i class="bi {{ containsActive ? 'bi-chevron-down' : 'bi-chevron-right' }} tree-chevron"></i>
|
||||
</button>
|
||||
{# Mapnaam: selecteer de map. Voor content gaat dit naar ?dir= (map-detail paneel rechts);
|
||||
voor plugins/themes is het een niet-navigerende toggle (legacy gedrag). #}
|
||||
{% if opts.treeDirSelectRoute %}
|
||||
<a href="{{ opts.treeDirSelectRoute }}{% if '?' in opts.treeDirSelectRoute %}&{% else %}?{% endif %}dir={{ n.path|url_encode }}" class="tree-link tree-name {{ containsActive ? 'is-active' : '' }} {{ opts.selectedDir == n.path ? 'is-selected' : '' }}" title="{{ n.path }}">
|
||||
<i class="bi {{ icon }}"></i>
|
||||
<span class="text-truncate">{{ n.name }}</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<button type="button" class="tree-link tree-toggle btn-toggle {{ containsActive ? '' : 'collapsed' }}" data-bs-toggle="collapse" data-bs-target="#{{ collapseId }}" aria-expanded="{{ containsActive ? 'true' : 'false' }}" title="{{ n.path }}">
|
||||
<i class="bi {{ icon }}"></i>
|
||||
<span class="text-truncate">{{ n.name }}</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if opts.treeDirActions %}
|
||||
<span class="tree-dir-actions">
|
||||
<button type="button" class="btn btn-link btn-sm p-0 tree-newfile-btn" title="{{ ta.new_file|default('Nieuw bestand hier') }}" aria-label="{{ ta.new_file|default('Nieuw bestand') }}" data-bs-toggle="modal" data-bs-target="#newFileModal" data-in-dir="{{ n.path }}">
|
||||
<button type="button" class="btn btn-link btn-sm p-0 tree-newfile-btn" title="{{ ta.new_file|default('Nieuw bestand hier') }}" aria-label="{{ ta.new_file|default('Nieuw bestand') }}" data-bs-toggle="modal" data-bs-target="{{ opts.newFileModalId|default('#newFileModal') }}" data-in-dir="{{ n.path }}">
|
||||
<i class="bi bi-file-earmark-plus"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-link btn-sm p-0 tree-newdir-btn" title="{{ ta.new_folder|default('Nieuwe map') }}" aria-label="{{ ta.new_folder|default('Nieuwe map') }}" data-bs-toggle="modal" data-bs-target="#newDirModal" data-in-dir="{{ n.path }}">
|
||||
{% if opts.newDirModalId is defined and opts.newDirModalId %}
|
||||
<button type="button" class="btn btn-link btn-sm p-0 tree-newdir-btn" title="{{ ta.new_folder|default('Nieuwe map') }}" aria-label="{{ ta.new_folder|default('Nieuwe map') }}" data-bs-toggle="modal" data-bs-target="{{ opts.newDirModalId }}" data-in-dir="{{ n.path }}">
|
||||
<i class="bi bi-folder-plus"></i>
|
||||
</button>
|
||||
<a href="/admin/content-dir-rename-in?dir={{ n.path|url_encode }}" class="tree-rename-btn" title="{{ ta.rename|default('Hernoemen') }}" aria-label="{{ ta.rename|default('Hernoemen') }}">
|
||||
{% endif %}
|
||||
{% if opts.treeDirRenameRoute %}
|
||||
<a href="{{ opts.treeDirRenameRoute }}{% if '?' in opts.treeDirRenameRoute %}&{% else %}?{% endif %}dir={{ n.path|url_encode }}" class="tree-rename-btn" title="{{ ta.rename|default('Hernoemen') }}" aria-label="{{ ta.rename|default('Hernoemen') }}">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<form method="POST" action="/admin/content-dir-delete-in" class="d-inline" onsubmit="return confirm('{{ ta.confirm_delete_folder|default('Weet je zeker dat je deze map wilt verwijderen? De map moet leeg zijn.') }}')">
|
||||
<input type="hidden" name="csrf_token" value="{{ opts.csrfToken }}">
|
||||
<input type="hidden" name="dir" value="{{ n.path }}">
|
||||
<button type="submit" class="btn btn-link btn-sm p-0 text-danger tree-delete-btn" title="{{ ta.delete|default('Verwijderen') }}" aria-label="{{ ta.delete|default('Verwijderen') }}">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -99,22 +112,20 @@
|
||||
<span class="badge bg-secondary badge-ext">{{ n.extension|upper }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% if n.path not in (opts.treeHideActionsFor ?? []) %}
|
||||
<a href="{{ opts.treeMoveRoute }}?{{ opts.treeMoveParams }}file={{ n.path|url_encode }}"
|
||||
class="tree-move-btn" title="{{ ta.rename|default('Hernoemen') }}" aria-label="{{ ta.rename|default('Hernoemen') }}">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<form method="POST" action="{{ opts.treeDeleteRoute }}" class="tree-delete-form" onsubmit="return confirm('{{ ta.confirm_delete_file|default('Weet je zeker dat je dit bestand wilt verwijderen?') }}')">
|
||||
<input type="hidden" name="csrf_token" value="{{ opts.csrfToken }}">
|
||||
{% if opts.treeDeleteName %}
|
||||
<input type="hidden" name="{{ opts.treeDeleteName }}" value="{{ opts.treeDeleteValue }}">
|
||||
<span class="tree-file-actions">
|
||||
{% if n.path not in (opts.treeHideActionsFor ?? []) %}
|
||||
{% set showFileMove = opts.treeFileMove is defined ? opts.treeFileMove : true %}
|
||||
{% if showFileMove %}
|
||||
<a href="{{ opts.treeMoveRoute }}?{{ opts.treeMoveParams }}file={{ n.path|url_encode }}"
|
||||
class="tree-move-btn" title="{{ ta.rename|default('Hernoemen/Verplaatsen') }}" aria-label="{{ ta.rename|default('Hernoemen/Verplaatsen') }}">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<input type="hidden" name="file" value="{{ n.path }}">
|
||||
<button type="submit" class="btn btn-link btn-sm p-0 text-danger tree-delete-btn" title="{{ ta.delete|default('Verwijderen') }}" aria-label="{{ ta.delete|default('Verwijderen') }}">
|
||||
<button type="button" class="btn btn-link btn-sm p-0 tree-delete-btn" title="{{ ta.delete|default('Verwijderen') }}" aria-label="{{ ta.delete|default('Verwijderen') }}" data-path="{{ n.path }}">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
@@ -129,29 +140,48 @@
|
||||
<div class="card-header bg-white py-2 d-flex justify-content-between align-items-center">
|
||||
<span class="small text-muted fw-semibold"><i class="bi bi-folder2-open"></i> {{ treeHeader|default('Bestanden') }}</span>
|
||||
</div>
|
||||
<div class="card-body p-2" id="editorFileTree">
|
||||
<div class="card-body p-2" id="editorFileTree"
|
||||
data-csrf="{{ csrf_token|default('') }}"
|
||||
data-scope="{{ treeScope|default('content') }}"
|
||||
data-move-url="{{ treeMoveUrl|default('') }}"
|
||||
data-delete-base="{{ treeDeleteBase|default('') }}"
|
||||
data-new-file-modal="{{ newFileModalId|default('#newFileModal') }}"
|
||||
data-new-dir-modal="{{ newDirModalId|default('#newDirModal') }}"
|
||||
data-scope-param="{{ treeScopeParam|default('') }}"
|
||||
data-edit-base="{{ treeRouteBase|default('') }}">
|
||||
{% if treeDirSelectRoute is defined and treeDirSelectRoute is not empty %}
|
||||
{# Klikbare content-root: selecteert de root map (?dir= leeg).
|
||||
data-dir="" maakt de root ook een drag-drop doel (lege string = base dir). #}
|
||||
<a href="{{ treeDirSelectRoute }}" class="tree-link tree-root-link {{ selectedDir|default('') == '' ? 'is-selected' : '' }}" data-dir="" title="content">
|
||||
<i class="bi bi-folder2-open"></i>
|
||||
<span class="text-truncate">{{ treeRootLabel|default('content') }}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if files is empty %}
|
||||
<div class="small text-muted p-2">{{ treeEmptyText|default('Geen bestanden gevonden.') }}</div>
|
||||
{% else %}
|
||||
{{ tree_macros.tree(files, {
|
||||
'relFile': relFile,
|
||||
'selectedDir': selectedDir|default(''),
|
||||
'editableExts': editableExts,
|
||||
'csrfToken': csrf_token,
|
||||
'treeIdPrefix': treeIdPrefix,
|
||||
'treeRouteBase': treeRouteBase,
|
||||
'treeRouteParams': treeRouteParams,
|
||||
'treeDeleteRoute': treeDeleteRoute,
|
||||
'treeDeleteName': treeDeleteName|default(''),
|
||||
'treeDeleteValue': treeDeleteValue|default(''),
|
||||
'treeMoveRoute': treeMoveRoute,
|
||||
'treeMoveParams': treeMoveParams,
|
||||
'treeDirActions': treeDirActions|default(false),
|
||||
'treeDirActions': treeDirActions is defined ? treeDirActions : true,
|
||||
'treeDirSelectRoute': treeDirSelectRoute|default(''),
|
||||
'treeDirRenameRoute': treeDirRenameRoute|default(''),
|
||||
'treeFileMove': treeFileMove is defined ? treeFileMove : true,
|
||||
'treeHideActionsFor': treeHideActionsFor|default([]),
|
||||
'newFileModalId': newFileModalId|default('#newFileModal'),
|
||||
'newDirModalId': newDirModalId|default(''),
|
||||
}) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/admin/assets/js/tree-interactions.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
var tree = document.getElementById('editorFileTree');
|
||||
@@ -187,5 +217,10 @@
|
||||
var btn = findToggle(c);
|
||||
if (btn) expand(btn);
|
||||
});
|
||||
|
||||
// Make all tree <li> nodes draggable (for drag-and-drop move)
|
||||
Array.prototype.forEach.call(tree.querySelectorAll('li[data-path]'), function (li) {
|
||||
li.setAttribute('draggable', 'true');
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
Reference in New Issue
Block a user