Fix frontend CSRF, clean reader URLs, and review findings
CSRF / actions: - List action forms now send both REQUEST_TOKEN (Contao gate) and _token (Symfony, validated via isCsrfTokenValid) — mirrors the editor; fixes the 400/500 "Ungültiger CSRF-Token" on publish/toggle/duplicate/delete Public reader / URLs: - Reader & results read auto_item via Input::get (marks the route param used) so clean path URLs /<page>/<alias> no longer 404 - List controller generates path URLs via ['parameters' => '/'.$alias] - Allow re-participation in the same session after a 60s cooldown Review fixes: - Results: merge answer snapshots by questionId+type (no more split cards on label edits); seed "Vielleicht" when allowMaybe so screen matches Excel - Reader: replace generic answer hint with per-option "weiter zu Frage X" only when a jump is configured; strip HTML tags from survey description - PDF: render free-text frequency summary; drop duplicate "Minimum" label - Drag-sort: bundle SortableJS locally, drop the duplicated inline sort CSS/JS from _survey_assets, throttle the backend MutationObserver - Backend: move hardcoded tl_survey operation labels to TL_LANG (de+en) - Remove dead code: SurveyEditorVoter, SurveyPermissionService, categoryFilter/categorySummary + helpers, orphaned publishSurvey lang key Reader layout: - max-width 1000px (other views keep 1400px); back/next buttons fill the width as equal, card-sized halves; sort handle icon offset tweak Docs: update AI_HANDOVER (dual-token CSRF, clean URLs) and README Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -85,6 +85,12 @@
|
||||
margin-bottom: 4em;
|
||||
}
|
||||
|
||||
.survey-shell--reader {
|
||||
max-width: 1000px;
|
||||
margin-left: max(var(--survey-page-gutter), calc((100% - 1000px) / 2));
|
||||
margin-right: max(var(--survey-page-gutter), calc((100% - 1000px) / 2));
|
||||
}
|
||||
|
||||
.survey-shell--template {
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(236, 124, 50, 0.18), transparent 32%),
|
||||
@@ -489,6 +495,28 @@
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Reader: Zurück/Weiter füllen die volle Breite und sind so groß wie die Ja/Nein-Karten */
|
||||
.survey-shell--reader .survey-button-row--navigation {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.survey-shell--reader .survey-button-row--navigation.has-back {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.survey-shell--reader .survey-navigation-form--back {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.survey-shell--reader .survey-button-row--navigation .survey-button {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 1.25rem;
|
||||
border-radius: 1.5rem;
|
||||
}
|
||||
|
||||
.survey-publish-toggle-form {
|
||||
margin: 0;
|
||||
}
|
||||
@@ -725,82 +753,6 @@
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.survey-accordion-sort-handle {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
min-width: 3.5rem;
|
||||
padding: 0 1rem;
|
||||
border: 0;
|
||||
border-left: 1px solid rgba(162, 168, 180, 0.18);
|
||||
background: transparent;
|
||||
cursor: grab;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
align-self: stretch;
|
||||
transition: background 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
.survey-accordion-sort-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.survey-accordion-item .handorgel__header__button:hover + .survey-accordion-sort-handle,
|
||||
.survey-accordion-item .handorgel__header__button:focus + .survey-accordion-sort-handle,
|
||||
.survey-accordion-item .handorgel__header--opened .survey-accordion-sort-handle,
|
||||
.survey-accordion-sort-handle:hover,
|
||||
.survey-accordion-sort-handle:focus {
|
||||
background: var(--survey-blue);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.survey-sort-icon {
|
||||
display: inline-flex;
|
||||
width: 0.95rem;
|
||||
height: 1.28rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #58a6da;
|
||||
line-height: 0;
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.survey-sort-icon svg {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: translateY(4px);
|
||||
}
|
||||
|
||||
.survey-question-order-save-shell {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
right: 1rem;
|
||||
z-index: 40;
|
||||
transform: translate(120%, -50%);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: transform 0.26s ease, opacity 0.26s ease;
|
||||
}
|
||||
|
||||
.survey-question-order-save-shell.is-visible {
|
||||
transform: translate(0, -50%);
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.survey-question-order-save {
|
||||
box-shadow: 0 18px 40px rgba(0, 59, 102, 0.24);
|
||||
}
|
||||
|
||||
.survey-accordion-item.is-reordering {
|
||||
box-shadow: 0 20px 44px rgba(0, 59, 102, 0.18);
|
||||
}
|
||||
|
||||
.survey-accordion-item.sortable-ghost {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.survey-accordion-item .handorgel__header__button,
|
||||
.survey-accordion-item .handorgel__header__button:hover,
|
||||
.survey-accordion-item .handorgel__header__button:focus {
|
||||
@@ -1075,22 +1027,6 @@
|
||||
padding: 0.95rem 1rem;
|
||||
}
|
||||
|
||||
.survey-accordion-sort-handle {
|
||||
min-width: 3.1rem;
|
||||
padding: 0 0.8rem;
|
||||
}
|
||||
|
||||
.survey-question-order-save-shell {
|
||||
top: auto;
|
||||
right: 0.85rem;
|
||||
bottom: 0.85rem;
|
||||
transform: translate(0, 130%);
|
||||
}
|
||||
|
||||
.survey-question-order-save-shell.is-visible {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
@@ -1249,133 +1185,6 @@
|
||||
});
|
||||
});
|
||||
|
||||
var initializeQuestionSorting = function () {
|
||||
var styleId = 'survey-question-sort-icon-runtime';
|
||||
|
||||
if (!document.getElementById(styleId)) {
|
||||
var runtimeStyle = document.createElement('style');
|
||||
runtimeStyle.id = styleId;
|
||||
runtimeStyle.textContent = '' +
|
||||
'.survey-sort-icon{' +
|
||||
'width:1.14rem !important;' +
|
||||
'height:1.52rem !important;' +
|
||||
'color:#58a6da !important;' +
|
||||
'background:none !important;' +
|
||||
'transform:translateY(1px) !important;' +
|
||||
'}' +
|
||||
'.survey-sort-icon::before,.survey-sort-icon::after{' +
|
||||
'content:none !important;' +
|
||||
'}' +
|
||||
'.survey-sort-icon svg{' +
|
||||
'display:block !important;' +
|
||||
'width:100% !important;' +
|
||||
'height:100% !important;' +
|
||||
'transform:translateY(4px);' +
|
||||
'}' +
|
||||
'.survey-sort-icon svg path:first-child{' +
|
||||
'transform:translateY(1.4px);' +
|
||||
'transform-box:fill-box;' +
|
||||
'transform-origin:center;' +
|
||||
'}' +
|
||||
'.survey-sort-icon svg path:nth-child(2){' +
|
||||
'transform:translateY(-1.4px);' +
|
||||
'transform-box:fill-box;' +
|
||||
'transform-origin:center;' +
|
||||
'}' +
|
||||
'.survey-sort-icon svg path[stroke]{' +
|
||||
'stroke-width:2px;' +
|
||||
'}';
|
||||
|
||||
document.head.appendChild(runtimeStyle);
|
||||
}
|
||||
|
||||
if (typeof Sortable !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
var sortableList = document.querySelector('[data-survey-question-sortable]');
|
||||
var orderForm = document.querySelector('[data-survey-question-order-shell]');
|
||||
var orderInput = document.querySelector('[data-survey-question-order-input]');
|
||||
|
||||
if (!sortableList || !orderForm || !orderInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sortableList._surveyQuestionSortable) {
|
||||
return;
|
||||
}
|
||||
|
||||
var getOrderedIds = function () {
|
||||
return Array.from(sortableList.querySelectorAll('[data-survey-question-item]')).map(function (item) {
|
||||
return item.getAttribute('data-question-id') || '';
|
||||
}).filter(function (value) {
|
||||
return value !== '';
|
||||
});
|
||||
};
|
||||
|
||||
var initialOrder = getOrderedIds().join(',');
|
||||
|
||||
var updateOrderField = function () {
|
||||
orderInput.value = getOrderedIds().join(',');
|
||||
};
|
||||
|
||||
var updateQuestionIndices = function () {
|
||||
sortableList.querySelectorAll('[data-survey-question-item]').forEach(function (item, index) {
|
||||
var indexNode = item.querySelector('.survey-accordion-index');
|
||||
|
||||
if (indexNode) {
|
||||
indexNode.textContent = (index + 1) + '.';
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var updateSaveVisibility = function () {
|
||||
var currentOrder = getOrderedIds().join(',');
|
||||
var isDirty = currentOrder !== initialOrder;
|
||||
|
||||
orderForm.hidden = !isDirty;
|
||||
orderForm.classList.toggle('is-visible', isDirty);
|
||||
updateOrderField();
|
||||
};
|
||||
|
||||
sortableList.querySelectorAll('[data-survey-question-sort-handle]').forEach(function (handle) {
|
||||
handle.addEventListener('click', function (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
});
|
||||
});
|
||||
|
||||
updateQuestionIndices();
|
||||
updateOrderField();
|
||||
updateSaveVisibility();
|
||||
|
||||
var sortableInstance = Sortable.create(sortableList, {
|
||||
animation: 180,
|
||||
draggable: '[data-survey-question-item]',
|
||||
handle: '[data-survey-question-sort-handle]',
|
||||
ghostClass: 'sortable-ghost',
|
||||
forceFallback: true,
|
||||
fallbackTolerance: 3,
|
||||
onStart: function (event) {
|
||||
if (event.item) {
|
||||
event.item.classList.add('is-reordering');
|
||||
}
|
||||
},
|
||||
onEnd: function (event) {
|
||||
if (event.item) {
|
||||
event.item.classList.remove('is-reordering');
|
||||
}
|
||||
|
||||
updateQuestionIndices();
|
||||
updateSaveVisibility();
|
||||
}
|
||||
});
|
||||
|
||||
sortableList._surveyQuestionSortable = sortableInstance;
|
||||
};
|
||||
|
||||
window.initializeSurveyQuestionSorting = initializeQuestionSorting;
|
||||
|
||||
if (typeof handorgel === 'function') {
|
||||
document.querySelectorAll('[data-survey-handorgel]').forEach(function (element) {
|
||||
var accordion = new handorgel(element, {
|
||||
@@ -1395,7 +1204,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
initializeQuestionSorting();
|
||||
|
||||
resetEditorViewport();
|
||||
});
|
||||
|
||||
@@ -70,17 +70,17 @@
|
||||
|
||||
.survey-question-order-save-shell {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
right: 1rem;
|
||||
bottom: 1.5rem;
|
||||
right: 1.5rem;
|
||||
z-index: 40;
|
||||
transform: translate(120%, -50%);
|
||||
transform: translate(120%, 0);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: transform 0.26s ease, opacity 0.26s ease;
|
||||
}
|
||||
|
||||
.survey-question-order-save-shell.is-visible {
|
||||
transform: translate(0, -50%);
|
||||
transform: translate(0, 0);
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
@@ -115,5 +115,5 @@
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.6/Sortable.min.js"></script>
|
||||
<script src="{{ asset('bundles/survey/js/sortable.min.js') }}"></script>
|
||||
<script src="{{ asset('bundles/survey/js/survey-frontend-editor-sort.js') }}"></script>
|
||||
@@ -51,6 +51,7 @@
|
||||
<div class="survey-list-card__head-actions survey-list-card__head-actions--template">
|
||||
<form method="post" class="survey-question-action-form">
|
||||
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('duplicate-survey-' ~ survey.id) }}">
|
||||
<input type="hidden" name="_survey_action" value="duplicate-survey">
|
||||
<input type="hidden" name="item_id" value="{{ survey.id }}">
|
||||
<button class="survey-button secondary" onclick="return confirm('{{ 'survey.list.duplicate_confirm'|trans({'%title%': survey.title|striptags|trim})|e('js') }}');">{{ 'survey.list.duplicate'|trans }}</button>
|
||||
@@ -79,12 +80,14 @@
|
||||
{% endif %}
|
||||
<form method="post" class="survey-question-action-form">
|
||||
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('duplicate-survey-' ~ survey.id) }}">
|
||||
<input type="hidden" name="_survey_action" value="duplicate-survey">
|
||||
<input type="hidden" name="item_id" value="{{ survey.id }}">
|
||||
<button class="survey-button secondary" onclick="return confirm('{{ 'survey.list.duplicate_confirm'|trans({'%title%': survey.title|striptags|trim})|e('js') }}');">{{ 'survey.list.duplicate'|trans }}</button>
|
||||
</form>
|
||||
<form method="post" class="survey-question-action-form">
|
||||
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('delete-survey-' ~ survey.id) }}">
|
||||
<input type="hidden" name="_survey_action" value="delete-survey">
|
||||
<input type="hidden" name="item_id" value="{{ survey.id }}">
|
||||
<button class="survey-button danger" onclick="return confirm('{{ 'survey.list.delete_confirm'|trans|e('js') }}') && confirm('{{ 'survey.list.delete_confirm_second'|trans|e('js') }}');">{{ 'survey.list.delete'|trans }}</button>
|
||||
@@ -94,6 +97,7 @@
|
||||
<div class="survey-list-card__head-actions">
|
||||
<form method="post" class="survey-list-publish-form">
|
||||
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('publish-survey-' ~ survey.id) }}">
|
||||
<input type="hidden" name="_survey_action" value="publish-survey">
|
||||
<input type="hidden" name="item_id" value="{{ survey.id }}">
|
||||
<button
|
||||
@@ -111,6 +115,7 @@
|
||||
|
||||
<form method="post" class="survey-list-eye-form">
|
||||
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('toggle-active-' ~ survey.id) }}">
|
||||
<input type="hidden" name="_survey_action" value="toggle-active">
|
||||
<input type="hidden" name="item_id" value="{{ survey.id }}">
|
||||
<button type="submit" class="survey-visibility-dot{{ survey.isActive ? ' is-on' : '' }}" title="{{ survey.isActive ? 'survey.edit.deactivate_survey'|trans : 'survey.edit.activate_survey'|trans }}" aria-label="{{ survey.isActive ? 'survey.edit.deactivate_survey'|trans : 'survey.edit.activate_survey'|trans }}"></button>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
{% set moduleHeadline = (headline is iterable ? headline.text|default('') : headline|default(''))|striptags|trim %}
|
||||
|
||||
<section class="survey-shell survey-grid">
|
||||
<section class="survey-shell survey-shell--reader survey-grid">
|
||||
{% if moduleHeadline %}
|
||||
<header><h2>{{ moduleHeadline }}</h2></header>
|
||||
{% endif %}
|
||||
@@ -63,7 +63,10 @@
|
||||
{% for child in surveyForm.answer %}
|
||||
<label class="survey-card" style="cursor: pointer;">
|
||||
<div class="survey-meta"><strong>{{ child.vars.label|striptags|trim }}</strong>{{ form_widget(child) }}</div>
|
||||
<div>{{ 'survey.survey.answer_flow_hint'|trans }}</div>
|
||||
{% set jumpHint = jumpHints|default({})[child.vars.value]|default(null) %}
|
||||
{% if jumpHint %}
|
||||
<div class="survey-jump-hint">{{ 'survey.survey.jump_to'|trans({'%position%': jumpHint.position, '%label%': jumpHint.label|striptags|trim}) }}</div>
|
||||
{% endif %}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user