fix xss (#2060)
This commit is contained in:
@@ -130,29 +130,29 @@ export default class KimaiFormSelect extends KimaiPlugin {
|
|||||||
select.find('option').remove().end().find('optgroup').remove().end();
|
select.find('option').remove().end().find('optgroup').remove().end();
|
||||||
|
|
||||||
if (emptyOption.length !== 0) {
|
if (emptyOption.length !== 0) {
|
||||||
select.append('<option value="">' + emptyOption.text() + '</option>');
|
select.append(this._createOption(emptyOption.text(), ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
let htmlOptions = '';
|
let emptyOpts = [];
|
||||||
let emptyOptions = '';
|
let options = [];
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(data)) {
|
for (const [key, value] of Object.entries(data)) {
|
||||||
if (key === '__empty__') {
|
if (key === '__empty__') {
|
||||||
for (const entity of value) {
|
for (const entity of value) {
|
||||||
emptyOptions += '<option value="' + entity.id + '">' + entity.name + '</option>';
|
emptyOpts.push(this._createOption(entity.name, entity.id));
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
htmlOptions += '<optgroup label="' + key + '">';
|
let optGroup = this._createOptgroup(key);
|
||||||
for (const entity of value) {
|
for (const entity of value) {
|
||||||
htmlOptions += '<option value="' + entity.id + '">' + entity.name + '</option>';
|
optGroup.appendChild(this._createOption(entity.name, entity.id));
|
||||||
}
|
}
|
||||||
htmlOptions += '</optgroup>';
|
options.push(optGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
select.append(htmlOptions);
|
select.append(options);
|
||||||
select.append(emptyOptions);
|
select.append(emptyOpts);
|
||||||
|
|
||||||
// if available, re-select the previous selected option (mostly usable for global activities)
|
// if available, re-select the previous selected option (mostly usable for global activities)
|
||||||
select.val(selectedValue);
|
select.val(selectedValue);
|
||||||
@@ -165,4 +165,28 @@ export default class KimaiFormSelect extends KimaiPlugin {
|
|||||||
select.trigger('change.select2');
|
select.trigger('change.select2');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} label
|
||||||
|
* @param {string} value
|
||||||
|
* @returns {HTMLElement}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_createOption(label, value) {
|
||||||
|
let option = document.createElement('option');
|
||||||
|
option.innerText = label;
|
||||||
|
option.value = value;
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} label
|
||||||
|
* @returns {HTMLElement}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_createOptgroup(label) {
|
||||||
|
let optGroup = document.createElement('optgroup');
|
||||||
|
optGroup.label = label;
|
||||||
|
return optGroup;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
public/build/app.dc6c3f34.js
Normal file
1
public/build/app.dc6c3f34.js
Normal file
File diff suppressed because one or more lines are too long
1
public/build/calendar.541a15eb.js
Normal file
1
public/build/calendar.541a15eb.js
Normal file
File diff suppressed because one or more lines are too long
@@ -5,7 +5,7 @@
|
|||||||
"build/runtime.098eaae1.js",
|
"build/runtime.098eaae1.js",
|
||||||
"build/0.79dbdbb9.js",
|
"build/0.79dbdbb9.js",
|
||||||
"build/1.32489d92.js",
|
"build/1.32489d92.js",
|
||||||
"build/app.e7537426.js"
|
"build/app.dc6c3f34.js"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"build/app.0325b7e0.css"
|
"build/app.0325b7e0.css"
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
"build/runtime.098eaae1.js": "sha384-xNNrNinl64G3nCUrIskgSjU0mUXXCB9lj6XCSInBTwxSKXk8uTMafnLHtdWdIGtd",
|
"build/runtime.098eaae1.js": "sha384-xNNrNinl64G3nCUrIskgSjU0mUXXCB9lj6XCSInBTwxSKXk8uTMafnLHtdWdIGtd",
|
||||||
"build/0.79dbdbb9.js": "sha384-U2Ao0ORAZ8PCeDmyRsqQFET3hc7pfUBimq0PrqFdG4/s0Bdi+qBj4TJK3o70bCd5",
|
"build/0.79dbdbb9.js": "sha384-U2Ao0ORAZ8PCeDmyRsqQFET3hc7pfUBimq0PrqFdG4/s0Bdi+qBj4TJK3o70bCd5",
|
||||||
"build/1.32489d92.js": "sha384-wVkjh5FzjFhMV4S4uNP23E/OLBOf+Zi7t3lpm9eWzoMr/tm2pydT+q0Op1XHuoUP",
|
"build/1.32489d92.js": "sha384-wVkjh5FzjFhMV4S4uNP23E/OLBOf+Zi7t3lpm9eWzoMr/tm2pydT+q0Op1XHuoUP",
|
||||||
"build/app.e7537426.js": "sha384-VAGMhwYJVTYXW8aMLhwzJqKmiG/fTXhdNBQj+823jsWFKecz7J6RXmWDbS9uwcHy",
|
"build/app.dc6c3f34.js": "sha384-x36nGKgEWmvmvxXUeFIQFZohiVkBb6hh47nVsbu6sacnkMyhSaJrkjcCMB/GH2hi",
|
||||||
"build/app.0325b7e0.css": "sha384-+e2X/+hgWNDL4q30jFlPjC+jD61G/j9nsyn+KO57Pakrq5XH5sjMuamVPtwgnDlG",
|
"build/app.0325b7e0.css": "sha384-+e2X/+hgWNDL4q30jFlPjC+jD61G/j9nsyn+KO57Pakrq5XH5sjMuamVPtwgnDlG",
|
||||||
"build/invoice.74279541.js": "sha384-2BXic5Sgorf2tXai6zSAN4wLY2dbg06L03/xMKW6itMcszvtnRArKzfBh6DNcF3f",
|
"build/invoice.74279541.js": "sha384-2BXic5Sgorf2tXai6zSAN4wLY2dbg06L03/xMKW6itMcszvtnRArKzfBh6DNcF3f",
|
||||||
"build/invoice.13d8ef4e.css": "sha384-B6RN/wZJToSBCZk2JeLokIqWEhbh+Eb9arYbt9dM+YoC2Z6PnCeTwTqSGyexWWJh",
|
"build/invoice.13d8ef4e.css": "sha384-B6RN/wZJToSBCZk2JeLokIqWEhbh+Eb9arYbt9dM+YoC2Z6PnCeTwTqSGyexWWJh",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"build/1.32489d92.js": "build/1.32489d92.js",
|
"build/1.32489d92.js": "build/1.32489d92.js",
|
||||||
"build/2.7ab75d0a.js": "build/2.7ab75d0a.js",
|
"build/2.7ab75d0a.js": "build/2.7ab75d0a.js",
|
||||||
"build/app.css": "build/app.0325b7e0.css",
|
"build/app.css": "build/app.0325b7e0.css",
|
||||||
"build/app.js": "build/app.e7537426.js",
|
"build/app.js": "build/app.dc6c3f34.js",
|
||||||
"build/calendar.css": "build/calendar.1408f57e.css",
|
"build/calendar.css": "build/calendar.1408f57e.css",
|
||||||
"build/calendar.js": "build/calendar.541a15eb.js",
|
"build/calendar.js": "build/calendar.541a15eb.js",
|
||||||
"build/chart.js": "build/chart.34d60a88.js",
|
"build/chart.js": "build/chart.34d60a88.js",
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class Extensions extends AbstractExtension
|
|||||||
new TwigFilter('multiline_indent', [$this, 'multilineIndent']),
|
new TwigFilter('multiline_indent', [$this, 'multilineIndent']),
|
||||||
new TwigFilter('color', [$this, 'color']),
|
new TwigFilter('color', [$this, 'color']),
|
||||||
new TwigFilter('font_contrast', [$this, 'calculateFontContrastColor']),
|
new TwigFilter('font_contrast', [$this, 'calculateFontContrastColor']),
|
||||||
|
new TwigFilter('nl2str', [$this, 'replaceNewline'], ['pre_escape' => 'html', 'is_safe' => ['html']]),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,4 +114,13 @@ class Extensions extends AbstractExtension
|
|||||||
{
|
{
|
||||||
return Constants::HOMEPAGE . '/documentation/' . $url;
|
return Constants::HOMEPAGE . '/documentation/' . $url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function replaceNewline($input, string $newline)
|
||||||
|
{
|
||||||
|
if (!\is_string($input)) {
|
||||||
|
return $input;
|
||||||
|
}
|
||||||
|
|
||||||
|
return str_replace(["\r\n", "\n", "\r"], $newline, $input);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,9 +43,9 @@ final class MarkdownExtension extends AbstractExtension
|
|||||||
public function getFilters()
|
public function getFilters()
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
new TwigFilter('md2html', [$this, 'markdownToHtml'], ['is_safe' => ['html']]),
|
new TwigFilter('md2html', [$this, 'markdownToHtml'], ['pre_escape' => 'html', 'is_safe' => ['html']]),
|
||||||
new TwigFilter('desc2html', [$this, 'timesheetContent'], ['is_safe' => ['html']]),
|
new TwigFilter('desc2html', [$this, 'timesheetContent'], ['pre_escape' => 'html', 'is_safe' => ['html']]),
|
||||||
new TwigFilter('comment2html', [$this, 'commentContent'], ['is_safe' => ['html']]),
|
new TwigFilter('comment2html', [$this, 'commentContent'], ['pre_escape' => 'html', 'is_safe' => ['html']]),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,24 +9,22 @@
|
|||||||
{% set inUse = (stats.recordAmount > 0) %}
|
{% set inUse = (stats.recordAmount > 0) %}
|
||||||
|
|
||||||
{% set params = {
|
{% set params = {
|
||||||
'%activity%': '<strong>' ~ activity.name ~ '</strong>',
|
'%activity%': activity.name,
|
||||||
'%project%': '<strong>-</strong>',
|
'%project%': '-',
|
||||||
'%customer%': '<strong>-</strong>',
|
'%customer%': '-',
|
||||||
'%records%': '<strong>' ~ stats.recordAmount ~ '</strong>',
|
'%records%': stats.recordAmount,
|
||||||
'%duration%': '<strong>' ~ stats.recordDuration|duration ~ '</strong>'
|
'%duration%': stats.recordDuration|duration
|
||||||
} %}
|
} %}
|
||||||
|
|
||||||
{% if activity.project is not null %}
|
{% if activity.project is not null %}
|
||||||
{% set params = params|merge({
|
{% set params = params|merge({
|
||||||
'%project%': '<strong>' ~ activity.project.name ~ '</strong>',
|
'%project%': activity.project.name,
|
||||||
'%customer%': '<strong>' ~ activity.project.customer.name ~ '</strong>',
|
'%customer%': activity.project.customer.name,
|
||||||
}) %}
|
}) %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% set message = '<p>' ~ ("admin_activity.short_stats"|trans(params)|raw) ~ '</p><p>' ~ ("admin_entity.delete_confirm"|trans|raw) ~ '</p>' %}
|
|
||||||
|
|
||||||
{{ include(app.request.xmlHttpRequest ? 'default/_form_delete_modal.html.twig' : 'default/_form_delete.html.twig', {
|
{{ include(app.request.xmlHttpRequest ? 'default/_form_delete_modal.html.twig' : 'default/_form_delete.html.twig', {
|
||||||
'message': message|raw,
|
'message': ("admin_activity.short_stats"|trans(params) ~ "admin_entity.delete_confirm"|trans),
|
||||||
'form': form,
|
'form': form,
|
||||||
'used': inUse,
|
'used': inUse,
|
||||||
'back': path('admin_activity')
|
'back': path('admin_activity')
|
||||||
|
|||||||
@@ -4,11 +4,11 @@
|
|||||||
{% block box_attributes %}id="budget_box"{% endblock %}
|
{% block box_attributes %}id="budget_box"{% endblock %}
|
||||||
{% block box_body %}
|
{% block box_body %}
|
||||||
{% set params = {
|
{% set params = {
|
||||||
'%activity%': '<strong>' ~ activity.name ~ '</strong>',
|
'%activity%': activity.name,
|
||||||
'%project%': '<strong>-</strong>',
|
'%project%': '-',
|
||||||
'%customer%': '<strong>-</strong>',
|
'%customer%': '-',
|
||||||
'%records%': '<strong>' ~ stats.recordAmount ~ '</strong>',
|
'%records%': stats.recordAmount,
|
||||||
'%duration%': '<strong>' ~ stats.recordDuration|duration ~ '</strong>'
|
'%duration%': stats.recordDuration|duration
|
||||||
} %}
|
} %}
|
||||||
|
|
||||||
{% set currency = null %}
|
{% set currency = null %}
|
||||||
@@ -18,17 +18,16 @@
|
|||||||
|
|
||||||
{% if activity.project is not null %}
|
{% if activity.project is not null %}
|
||||||
{% set params = params|merge({
|
{% set params = params|merge({
|
||||||
'%project%': '<strong>' ~ activity.project.name ~ '</strong>',
|
'%project%': activity.project.name,
|
||||||
'%customer%': '<strong>' ~ activity.project.customer.name ~ '</strong>',
|
'%customer%': activity.project.customer.name,
|
||||||
}) %}
|
}) %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
{{ 'admin_activity.short_stats'|trans(params)|raw }}
|
{{ 'admin_activity.short_stats'|trans(params) }}
|
||||||
{{ 'label.rate_internal'|trans }}: {{ stats.recordInternalRate|money(currency) }}.
|
{{ 'label.rate_internal'|trans }}: {{ stats.recordInternalRate|money(currency) }}.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
{{ progress.progressbar(activity.budget, stats.recordRate, 'label.budget'|trans, stats.recordRate|money(currency) ~ ' / ' ~ activity.budget|money(currency) ) }}
|
{{ progress.progressbar(activity.budget, stats.recordRate, 'label.budget'|trans, stats.recordRate|money(currency) ~ ' / ' ~ activity.budget|money(currency) ) }}
|
||||||
{{ progress.progressbar(activity.timeBudget, stats.recordDuration, 'label.timeBudget'|trans, stats.recordDuration|duration ~ ' / ' ~ activity.timeBudget|duration ) }}
|
{{ progress.progressbar(activity.timeBudget, stats.recordDuration, 'label.timeBudget'|trans, stats.recordDuration|duration ~ ' / ' ~ activity.timeBudget|duration ) }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -9,17 +9,15 @@
|
|||||||
{% set inUse = (stats.recordAmount > 0) %}
|
{% set inUse = (stats.recordAmount > 0) %}
|
||||||
|
|
||||||
{% set params = {
|
{% set params = {
|
||||||
'%activity%': '<strong>' ~ stats.activityAmount ~ '</strong>',
|
'%activity%': stats.activityAmount,
|
||||||
'%project%': '<strong>' ~ stats.projectAmount ~ '</strong>',
|
'%project%': stats.projectAmount,
|
||||||
'%customer%': '<strong>' ~ customer.name ~ '</strong>',
|
'%customer%': customer.name,
|
||||||
'%records%': '<strong>' ~ stats.recordAmount ~ '</strong>',
|
'%records%': stats.recordAmount,
|
||||||
'%duration%': '<strong>' ~ stats.recordDuration|duration ~ '</strong>'
|
'%duration%': stats.recordDuration|duration
|
||||||
} %}
|
} %}
|
||||||
|
|
||||||
{% set message = '<p>' ~ ("admin_customer.short_stats"|trans(params)|raw) ~ '</p><p>' ~ ("admin_entity.delete_confirm"|trans|raw) ~ '</p>' %}
|
|
||||||
|
|
||||||
{{ include(app.request.xmlHttpRequest ? 'default/_form_delete_modal.html.twig' : 'default/_form_delete.html.twig', {
|
{{ include(app.request.xmlHttpRequest ? 'default/_form_delete_modal.html.twig' : 'default/_form_delete.html.twig', {
|
||||||
'message': message|raw,
|
'message': ("admin_customer.short_stats"|trans(params) ~ "admin_entity.delete_confirm"|trans),
|
||||||
'form': form,
|
'form': form,
|
||||||
'used': inUse,
|
'used': inUse,
|
||||||
'back': path('admin_customer')
|
'back': path('admin_customer')
|
||||||
|
|||||||
@@ -6,17 +6,17 @@
|
|||||||
{% set currency = customer.currency %}
|
{% set currency = customer.currency %}
|
||||||
|
|
||||||
{% set params = {
|
{% set params = {
|
||||||
'%activity%': '<strong>' ~ stats.activityAmount ~ '</strong>',
|
'%activity%': stats.activityAmount,
|
||||||
'%project%': '<strong>' ~ stats.projectAmount ~ '</strong>',
|
'%project%': stats.projectAmount,
|
||||||
'%customer%': '<strong>' ~ customer.name ~ '</strong>',
|
'%customer%': customer.name,
|
||||||
'%records%': '<strong>' ~ stats.recordAmount ~ '</strong>',
|
'%records%': stats.recordAmount,
|
||||||
'%duration%': '<strong>' ~ stats.recordDuration|duration ~ '</strong>',
|
'%duration%': stats.recordDuration|duration,
|
||||||
'%rate%': '<strong>' ~ stats.recordRate|money(currency) ~ '</strong>',
|
'%rate%': stats.recordRate|money(currency),
|
||||||
'%internal_rate%': '<strong>' ~ stats.recordInternalRate|money(currency) ~ '</strong>'
|
'%internal_rate%': stats.recordInternalRate|money(currency)
|
||||||
} %}
|
} %}
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
{{ 'admin_customer.short_stats'|trans(params)|raw }}
|
{{ 'admin_customer.short_stats'|trans(params) }}
|
||||||
{{ 'label.rate_internal'|trans }}: {{ stats.recordInternalRate|money(currency) }}.
|
{{ 'label.rate_internal'|trans }}: {{ stats.recordInternalRate|money(currency) }}.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
{{ form_widget(form) }}
|
{{ form_widget(form) }}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>{{ message|raw }}</p>
|
<p>{{ message }}</p>
|
||||||
{{ form_widget(form) }}
|
{{ form_widget(form) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
{{ form_widget(form) }}
|
{{ form_widget(form) }}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>{{ message|raw }}</p>
|
<p>{{ message }}</p>
|
||||||
{{ form_widget(form) }}
|
{{ form_widget(form) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -116,7 +116,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'activity') }}">{{ widgets.label_activity(entry.activity) }}</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'activity') }}">{{ widgets.label_activity(entry.activity) }}</td>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }}">
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }}">
|
||||||
{{ entry.description|escape|desc2html }}
|
{{ entry.description|desc2html }}
|
||||||
</td>
|
</td>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'unit_price') }}">
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'unit_price') }}">
|
||||||
{{ rate|money(currency) }}
|
{{ rate|money(currency) }}
|
||||||
|
|||||||
@@ -263,7 +263,7 @@ mpdf-->
|
|||||||
{{ entry.project.customer.name }} - {{ entry.project.name }}{% if entry.activity is not null %} - {{ entry.activity.name }}{% endif %}
|
{{ entry.project.customer.name }} - {{ entry.project.name }}{% if entry.activity is not null %} - {{ entry.activity.name }}{% endif %}
|
||||||
{% if entry.description is not empty %}
|
{% if entry.description is not empty %}
|
||||||
<br>
|
<br>
|
||||||
<i>{{ entry.description|escape|desc2html }}</i>
|
<i>{{ entry.description|desc2html }}</i>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="duration">{{ entry.duration|duration(decimal) }}</td>
|
<td class="duration">{{ entry.duration|duration(decimal) }}</td>
|
||||||
|
|||||||
@@ -440,7 +440,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="column-description" {% if not columns.description %}style="display: none"{% endif %}>
|
<td class="column-description" {% if not columns.description %}style="display: none"{% endif %}>
|
||||||
{% if entry.description is not empty %}
|
{% if entry.description is not empty %}
|
||||||
{{ entry.description|escape|desc2html }}
|
{{ entry.description|desc2html }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="column-exported" {% if not columns.exported %}style="display: none"{% endif %}>
|
<td class="column-exported" {% if not columns.exported %}style="display: none"{% endif %}>
|
||||||
|
|||||||
@@ -100,7 +100,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }} timesheet-description">
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }} timesheet-description">
|
||||||
{% if entry.description is not empty %}
|
{% if entry.description is not empty %}
|
||||||
{{ entry.description|escape|desc2html }}
|
{{ entry.description|desc2html }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'unit_price') }} text-center">{{ rate|money(currency) }}</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'unit_price') }} text-center">{{ rate|money(currency) }}</td>
|
||||||
|
|||||||
@@ -33,10 +33,10 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<strong>{{ 'label.contact'|trans }}</strong>:
|
<strong>{{ 'label.contact'|trans }}</strong>:
|
||||||
{{ model.template.contact|replace({"\n": ' – ', "\r\n": ' – ', "\r": ' – '})|raw }}
|
{{ model.template.contact|nl2str(' – ') }}
|
||||||
<br>
|
<br>
|
||||||
<strong>{{ 'label.invoice_bank_account'|trans }}</strong>:
|
<strong>{{ 'label.invoice_bank_account'|trans }}</strong>:
|
||||||
{{ model.template.paymentDetails|replace({"\n": ' – ', "\r\n": ' – ', "\r": ' – '})|raw }}
|
{{ model.template.paymentDetails|nl2str(' – ') }}
|
||||||
</td>
|
</td>
|
||||||
<td align="right">
|
<td align="right">
|
||||||
{{ 'export.page_of'|trans({'%page%': '{PAGENO}', '%pages%': '{nb}'}) }}
|
{{ 'export.page_of'|trans({'%page%': '{PAGENO}', '%pages%': '{nb}'}) }}
|
||||||
@@ -151,9 +151,9 @@ mpdf-->
|
|||||||
</table>
|
</table>
|
||||||
|
|
||||||
{% if model.template.paymentTerms is not empty %}
|
{% if model.template.paymentTerms is not empty %}
|
||||||
<p>
|
<div class="paymentTerms">
|
||||||
{{ model.template.paymentTerms|md2html }}
|
{{ model.template.paymentTerms|md2html }}
|
||||||
</p>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -127,7 +127,7 @@
|
|||||||
<div class="col-xs-12">
|
<div class="col-xs-12">
|
||||||
{% if model.template.paymentTerms is not empty %}
|
{% if model.template.paymentTerms is not empty %}
|
||||||
<div contenteditable="true" class="paymentTerms">
|
<div contenteditable="true" class="paymentTerms">
|
||||||
{{ model.template.paymentTerms|nl2br|md2html }}
|
{{ model.template.paymentTerms|md2html }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@@ -135,11 +135,11 @@
|
|||||||
|
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<p>
|
<p>
|
||||||
<strong>{{ 'label.address'|trans }}</strong>: {{ model.template.company }} – {{ model.template.address|replace({"\n": ' – ', "\r\n": ' – ', "\r": ' – '})|raw }}
|
<strong>{{ 'label.address'|trans }}</strong>: {{ model.template.company }} – {{ model.template.address|nl2str(' – ') }}
|
||||||
<br>
|
<br>
|
||||||
<strong>{{ 'label.invoice_bank_account'|trans }}</strong>: {{ model.template.paymentDetails|replace({"\n": ' – ', "\r\n": ' – ', "\r": ' – '})|raw }}
|
<strong>{{ 'label.invoice_bank_account'|trans }}</strong>: {{ model.template.paymentDetails|nl2str(' – ') }}
|
||||||
<br>
|
<br>
|
||||||
<strong>{{ 'label.contact'|trans }}</strong>: {{ model.template.contact|replace({"\n": ' – ', "\r\n": ' – ', "\r": ' – '})|raw }}
|
<strong>{{ 'label.contact'|trans }}</strong>: {{ model.template.contact|nl2str(' – ') }}
|
||||||
</p>
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
<div class="col-xs-12">
|
<div class="col-xs-12">
|
||||||
<header>
|
<header>
|
||||||
<address>
|
<address>
|
||||||
<p contenteditable="true">{{ model.template.company }} – {{ model.template.address|replace({"\n": ' – ', "\r\n": ' – ', "\r": ' – '})|raw }}</p>
|
<p contenteditable="true">{{ model.template.company }} – {{ model.template.address|nl2str(' – ') }}</p>
|
||||||
</address>
|
</address>
|
||||||
</header>
|
</header>
|
||||||
<article class="address">
|
<article class="address">
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
{% if model.template.paymentTerms is not empty %}
|
{% if model.template.paymentTerms is not empty %}
|
||||||
<article class="paymentTerms">
|
<article class="paymentTerms">
|
||||||
<div contenteditable="true">
|
<div contenteditable="true">
|
||||||
{{ model.template.paymentTerms|nl2br|md2html }}
|
{{ model.template.paymentTerms|md2html }}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -9,17 +9,15 @@
|
|||||||
{% set inUse = (stats.recordAmount > 0) %}
|
{% set inUse = (stats.recordAmount > 0) %}
|
||||||
|
|
||||||
{% set params = {
|
{% set params = {
|
||||||
'%project%': '<strong>' ~ project.name ~ '</strong>',
|
'%project%': project.name,
|
||||||
'%customer%': '<strong>' ~ project.customer.name ~ '</strong>',
|
'%customer%': project.customer.name,
|
||||||
'%records%': '<strong>' ~ stats.recordAmount ~ '</strong>',
|
'%records%': stats.recordAmount,
|
||||||
'%activities%': '<strong>' ~ stats.activityAmount ~ '</strong>',
|
'%activities%': stats.activityAmount,
|
||||||
'%duration%': '<strong>' ~ stats.recordDuration|duration ~ '</strong>'
|
'%duration%': stats.recordDuration|duration
|
||||||
} %}
|
} %}
|
||||||
|
|
||||||
{% set message = '<p>' ~ ("admin_project.short_stats"|trans(params)|raw) ~ '</p><p>' ~ ("admin_entity.delete_confirm"|trans|raw) ~ '</p>' %}
|
|
||||||
|
|
||||||
{{ include(app.request.xmlHttpRequest ? 'default/_form_delete_modal.html.twig' : 'default/_form_delete.html.twig', {
|
{{ include(app.request.xmlHttpRequest ? 'default/_form_delete_modal.html.twig' : 'default/_form_delete.html.twig', {
|
||||||
'message': message|raw,
|
'message': ("admin_project.short_stats"|trans(params) ~ "admin_entity.delete_confirm"|trans),
|
||||||
'form': form,
|
'form': form,
|
||||||
'used': inUse,
|
'used': inUse,
|
||||||
'back': path('admin_project')
|
'back': path('admin_project')
|
||||||
|
|||||||
@@ -4,17 +4,17 @@
|
|||||||
{% block box_attributes %}id="budget_box"{% endblock %}
|
{% block box_attributes %}id="budget_box"{% endblock %}
|
||||||
{% block box_body %}
|
{% block box_body %}
|
||||||
{% set params = {
|
{% set params = {
|
||||||
'%project%': '<strong>' ~ project.name ~ '</strong>',
|
'%project%': project.name,
|
||||||
'%customer%': '<strong>' ~ project.customer.name ~ '</strong>',
|
'%customer%': project.customer.name,
|
||||||
'%records%': '<strong>' ~ stats.recordAmount ~ '</strong>',
|
'%records%': stats.recordAmount,
|
||||||
'%activities%': '<strong>' ~ stats.activityAmount ~ '</strong>',
|
'%activities%': stats.activityAmount,
|
||||||
'%duration%': '<strong>' ~ stats.recordDuration|duration ~ '</strong>'
|
'%duration%': stats.recordDuration|duration
|
||||||
} %}
|
} %}
|
||||||
|
|
||||||
{% set currency = project.customer.currency %}
|
{% set currency = project.customer.currency %}
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
{{ 'admin_project.short_stats'|trans(params)|raw }}
|
{{ 'admin_project.short_stats'|trans(params) }}
|
||||||
{{ 'label.rate_internal'|trans }}: {{ stats.recordInternalRate|money(currency) }}.
|
{{ 'label.rate_internal'|trans }}: {{ stats.recordInternalRate|money(currency) }}.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
<tr{% if is_granted('view', entry) %} class="alternative-link open-edit" data-href="{{ path('project_details', {'id': entry.id}) }}"{% endif %}>
|
<tr{% if is_granted('view', entry) %} class="alternative-link open-edit" data-href="{{ path('project_details', {'id': entry.id}) }}"{% endif %}>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'name') }}">{{ widgets.label_color_dot('project', true, entry.name, null, entry.color) }}</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'name') }}">{{ widgets.label_color_dot('project', true, entry.name, null, entry.color) }}</td>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'customer') }}">{{ widgets.label_customer(entry.customer) }}</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'customer') }}">{{ widgets.label_customer(entry.customer) }}</td>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'comment') }}">{{ entry.comment|comment2html() }}</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'comment') }}">{{ entry.comment|comment2html }}</td>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'orderNumber') }}">{{ entry.orderNumber }}</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'orderNumber') }}">{{ entry.orderNumber }}</td>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'orderDate') }}">{% if entry.orderDate is not null %}{{ entry.orderDate|date_full }}{% endif %}</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'orderDate') }}">{% if entry.orderDate is not null %}{{ entry.orderDate|date_full }}{% endif %}</td>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'start') }}">{% if entry.start is not null %}{{ entry.start|date_full }}{% endif %}</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'start') }}">{% if entry.start is not null %}{{ entry.start|date_full }}{% endif %}</td>
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
<td>
|
<td>
|
||||||
{% if entry.description is not empty %}
|
{% if entry.description is not empty %}
|
||||||
<div>
|
<div>
|
||||||
{{ entry.description|escape|desc2html }}
|
{{ entry.description|desc2html }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span class="small">
|
<span class="small">
|
||||||
|
|||||||
@@ -144,7 +144,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }} timesheet-description">
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }} timesheet-description">
|
||||||
{% if allowMarkdown %}
|
{% if allowMarkdown %}
|
||||||
{{ entry.description|escape|desc2html }}
|
{{ entry.description|desc2html }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ entry.description|nl2br }}
|
{{ entry.description|nl2br }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
{% extends app.request.xmlHttpRequest ? 'form.html.twig' : 'base.html.twig' %}
|
{% extends app.request.xmlHttpRequest ? 'form.html.twig' : 'base.html.twig' %}
|
||||||
{% import "macros/widgets.html.twig" as widgets %}
|
{% import "macros/widgets.html.twig" as widgets %}
|
||||||
{% import "macros/datatables.html.twig" as tables %}
|
|
||||||
|
|
||||||
{% block page_title %}{{ 'admin_user.title'|trans }}{% endblock %}
|
{% block page_title %}{{ 'admin_user.title'|trans }}{% endblock %}
|
||||||
|
|
||||||
@@ -9,15 +8,13 @@
|
|||||||
{% set inUse = (stats.recordsTotal > 0) %}
|
{% set inUse = (stats.recordsTotal > 0) %}
|
||||||
|
|
||||||
{% set params = {
|
{% set params = {
|
||||||
'%user%': '<strong>' ~ widgets.username(user) ~ '</strong>',
|
'%user%': widgets.username(user),
|
||||||
'%records%': '<strong>' ~ stats.recordsTotal ~ '</strong>',
|
'%records%': stats.recordsTotal,
|
||||||
'%duration%': '<strong>' ~ stats.durationTotal|duration ~ '</strong>'
|
'%duration%': stats.durationTotal|duration
|
||||||
} %}
|
} %}
|
||||||
|
|
||||||
{% set message = '<p>' ~ ("admin_user.short_stats"|trans(params)|raw) ~ '</p>' %}
|
|
||||||
|
|
||||||
{{ include(app.request.xmlHttpRequest ? 'default/_form_delete_modal.html.twig' : 'default/_form_delete.html.twig', {
|
{{ include(app.request.xmlHttpRequest ? 'default/_form_delete_modal.html.twig' : 'default/_form_delete.html.twig', {
|
||||||
'message': message|raw,
|
'message': "admin_user.short_stats"|trans(params),
|
||||||
'form': form,
|
'form': form,
|
||||||
'used': inUse,
|
'used': inUse,
|
||||||
'back': path('admin_user')
|
'back': path('admin_user')
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use App\Entity\Activity;
|
|||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use App\Twig\Extensions;
|
use App\Twig\Extensions;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Twig\Node\Node;
|
||||||
use Twig\TwigFilter;
|
use Twig\TwigFilter;
|
||||||
use Twig\TwigFunction;
|
use Twig\TwigFunction;
|
||||||
|
|
||||||
@@ -29,7 +30,7 @@ class ExtensionsTest extends TestCase
|
|||||||
|
|
||||||
public function testGetFilters()
|
public function testGetFilters()
|
||||||
{
|
{
|
||||||
$filters = ['docu_link', 'multiline_indent', 'color', 'font_contrast'];
|
$filters = ['docu_link', 'multiline_indent', 'color', 'font_contrast', 'nl2str'];
|
||||||
$sut = $this->getSut();
|
$sut = $this->getSut();
|
||||||
$twigFilters = $sut->getFilters();
|
$twigFilters = $sut->getFilters();
|
||||||
$this->assertCount(\count($filters), $twigFilters);
|
$this->assertCount(\count($filters), $twigFilters);
|
||||||
@@ -39,6 +40,11 @@ class ExtensionsTest extends TestCase
|
|||||||
$this->assertInstanceOf(TwigFilter::class, $filter);
|
$this->assertInstanceOf(TwigFilter::class, $filter);
|
||||||
$this->assertEquals($filters[$i++], $filter->getName());
|
$this->assertEquals($filters[$i++], $filter->getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// make sure that the nl2str filters does proper escaping
|
||||||
|
self::assertEquals('nl2str', $twigFilters[4]->getName());
|
||||||
|
self::assertEquals('html', $twigFilters[4]->getPreEscape());
|
||||||
|
self::assertEquals(['html'], $twigFilters[4]->getSafe(new Node()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGetFunctions()
|
public function testGetFunctions()
|
||||||
@@ -162,4 +168,25 @@ sdfsdf' . PHP_EOL . "\n" .
|
|||||||
self::assertEquals(1, $sut->getIsoDayByName(''));
|
self::assertEquals(1, $sut->getIsoDayByName(''));
|
||||||
self::assertEquals(1, $sut->getIsoDayByName('sdfgsdf'));
|
self::assertEquals(1, $sut->getIsoDayByName('sdfgsdf'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getTestDataReplaceNewline()
|
||||||
|
{
|
||||||
|
yield [',', new \stdClass(), new \stdClass()];
|
||||||
|
yield [',', null, null];
|
||||||
|
yield [',', '', ''];
|
||||||
|
yield ['*', PHP_EOL, '*'];
|
||||||
|
yield [',', 'foo' . PHP_EOL . 'bar', 'foo,bar'];
|
||||||
|
yield [' – ', 'foo' . PHP_EOL . 'bar', 'foo – bar'];
|
||||||
|
yield [' – ', "foo\r\nbar\rtest\nhello", 'foo – bar – test – hello'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider getTestDataReplaceNewline
|
||||||
|
*/
|
||||||
|
public function testReplaceNewline(string $replacer, $input, $expected)
|
||||||
|
{
|
||||||
|
$sut = $this->getSut();
|
||||||
|
|
||||||
|
self::assertEquals($expected, $sut->replaceNewline($input, $replacer));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use App\Configuration\TimesheetConfiguration;
|
|||||||
use App\Twig\MarkdownExtension;
|
use App\Twig\MarkdownExtension;
|
||||||
use App\Utils\Markdown;
|
use App\Utils\Markdown;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Twig\Node\Node;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @covers \App\Twig\MarkdownExtension
|
* @covers \App\Twig\MarkdownExtension
|
||||||
@@ -27,9 +28,21 @@ class MarkdownExtensionTest extends TestCase
|
|||||||
$sut = new MarkdownExtension(new Markdown(), $config);
|
$sut = new MarkdownExtension(new Markdown(), $config);
|
||||||
$filters = $sut->getFilters();
|
$filters = $sut->getFilters();
|
||||||
$this->assertCount(3, $filters);
|
$this->assertCount(3, $filters);
|
||||||
|
|
||||||
|
// make sure that the md2html filter does proper escaping
|
||||||
$this->assertEquals('md2html', $filters[0]->getName());
|
$this->assertEquals('md2html', $filters[0]->getName());
|
||||||
|
self::assertEquals('html', $filters[0]->getPreEscape());
|
||||||
|
self::assertEquals(['html'], $filters[0]->getSafe(new Node()));
|
||||||
|
|
||||||
|
// make sure that the desc2html filter does proper escaping
|
||||||
$this->assertEquals('desc2html', $filters[1]->getName());
|
$this->assertEquals('desc2html', $filters[1]->getName());
|
||||||
|
self::assertEquals('html', $filters[1]->getPreEscape());
|
||||||
|
self::assertEquals(['html'], $filters[1]->getSafe(new Node()));
|
||||||
|
|
||||||
|
// make sure that the comment2html filter does proper escaping
|
||||||
$this->assertEquals('comment2html', $filters[2]->getName());
|
$this->assertEquals('comment2html', $filters[2]->getName());
|
||||||
|
self::assertEquals('html', $filters[2]->getPreEscape());
|
||||||
|
self::assertEquals(['html'], $filters[2]->getSafe(new Node()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testMarkdownToHtml()
|
public function testMarkdownToHtml()
|
||||||
|
|||||||
Reference in New Issue
Block a user