Release 2.47 (#5784)
This commit is contained in:
1
.github/workflows/docker.yaml
vendored
1
.github/workflows/docker.yaml
vendored
@@ -60,7 +60,6 @@ jobs:
|
||||
tags: |
|
||||
kimai/kimai2:latest
|
||||
kimai/kimai2:fpm
|
||||
kimai/kimai2:fpm-${{ env.kimai_version }}
|
||||
push: true
|
||||
|
||||
- name: Apache image
|
||||
|
||||
@@ -73,6 +73,7 @@ export default class KimaiAPILink extends KimaiPlugin {
|
||||
const ALERT = this.getContainer().getPlugin('alert');
|
||||
const successHandle = () => {
|
||||
EVENTS.trigger(eventName);
|
||||
document.dispatchEvent(new CustomEvent('kimai.reloadedContent'));
|
||||
if (attributes['msgSuccess'] !== undefined) {
|
||||
ALERT.success(attributes['msgSuccess']);
|
||||
}
|
||||
@@ -82,6 +83,7 @@ export default class KimaiAPILink extends KimaiPlugin {
|
||||
if (attributes['msgError'] !== undefined) {
|
||||
message = attributes['msgError'];
|
||||
}
|
||||
document.dispatchEvent(new CustomEvent('kimai.reloadedContent'));
|
||||
API.handleError(message, error);
|
||||
};
|
||||
|
||||
@@ -90,6 +92,8 @@ export default class KimaiAPILink extends KimaiPlugin {
|
||||
data = attributes['payload'];
|
||||
}
|
||||
|
||||
document.dispatchEvent(new CustomEvent('kimai.reloadContent'));
|
||||
|
||||
if (method === 'PATCH') {
|
||||
API.patch(url, data, successHandle, errorHandle);
|
||||
} else if (method === 'POST') {
|
||||
|
||||
@@ -217,8 +217,6 @@ export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
|
||||
const eventName = form.dataset['formEvent'];
|
||||
/** @type {KimaiEvent} alert */
|
||||
const events = this.getContainer().getPlugin('event');
|
||||
/** @type {KimaiAlert} alert */
|
||||
const alert = this.getContainer().getPlugin('alert');
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -259,15 +257,8 @@ export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
|
||||
} else {
|
||||
events.trigger(eventName);
|
||||
|
||||
// try to find form defined message first, but
|
||||
let msg = form.dataset['msgSuccess'];
|
||||
// if that is not available: use a generic fallback message
|
||||
if (msg === null || msg === undefined || msg === '') {
|
||||
msg = 'action.update.success';
|
||||
}
|
||||
this._isDirty = false;
|
||||
this._getModal().hide();
|
||||
alert.success(msg);
|
||||
}
|
||||
});
|
||||
})
|
||||
@@ -277,6 +268,8 @@ export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
|
||||
message = 'action.update.error';
|
||||
}
|
||||
|
||||
/** @type {KimaiAlert} alert */
|
||||
const alert = this.getContainer().getPlugin('alert');
|
||||
alert.error(message, error.message);
|
||||
|
||||
// this is useful for changing form fields and retrying to save (and in development to test form changes)
|
||||
|
||||
@@ -42,7 +42,7 @@ export default class KimaiThemeInitializer extends KimaiPlugin {
|
||||
}
|
||||
|
||||
// at which element we append the loading screen
|
||||
let container = 'body';
|
||||
let container = 'div.page-wrapper';
|
||||
if (event.detail !== undefined && event.detail !== null) {
|
||||
container = event.detail;
|
||||
}
|
||||
|
||||
@@ -70,8 +70,6 @@ export default class KimaiCalendar {
|
||||
const DATES = this.kimai.getPlugin('date');
|
||||
/** @type {KimaiAjaxModalForm} MODAL */
|
||||
const MODAL = this.kimai.getPlugin('modal');
|
||||
/** @type {KimaiAlert} ALERT */
|
||||
const ALERT = this.kimai.getPlugin('alert');
|
||||
|
||||
// Instead of using "buttonIcons" the theme needs to be adjusted directly
|
||||
// https://fullcalendar.io/docs/buttonIcons
|
||||
@@ -282,6 +280,7 @@ export default class KimaiCalendar {
|
||||
droppable: true,
|
||||
// drop function handles external draggable events
|
||||
drop: (dropInfo) => {
|
||||
document.dispatchEvent(new CustomEvent('kimai.reloadContent'));
|
||||
const entry = dropInfo.draggedEl;
|
||||
const source = entry.parentElement;
|
||||
let data = JSON.parse(entry.dataset.entry);
|
||||
@@ -324,7 +323,7 @@ export default class KimaiCalendar {
|
||||
(result) => {
|
||||
const newItem = this.convertSourceForCalendar(result);
|
||||
this.getCalendar().addEvent(newItem, true);
|
||||
ALERT.success('action.update.success');
|
||||
document.dispatchEvent(new CustomEvent('kimai.reloadedContent'));
|
||||
}
|
||||
);
|
||||
} else {
|
||||
@@ -334,7 +333,7 @@ export default class KimaiCalendar {
|
||||
(result) => {
|
||||
const newItem = this.convertSourceForCalendar(result);
|
||||
this.getCalendar().addEvent(newItem, true);
|
||||
ALERT.success('action.update.success');
|
||||
document.dispatchEvent(new CustomEvent('kimai.reloadedContent'));
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -703,8 +702,6 @@ export default class KimaiCalendar {
|
||||
|
||||
/** @type {KimaiAPI} API */
|
||||
const API = this.kimai.getPlugin('api');
|
||||
/** @type {KimaiAlert} ALERT */
|
||||
const ALERT = this.kimai.getPlugin('alert');
|
||||
/** @type {KimaiDateUtils} DATE */
|
||||
const DATES = this.kimai.getPlugin('date');
|
||||
|
||||
@@ -716,11 +713,14 @@ export default class KimaiCalendar {
|
||||
payload.end = null;
|
||||
}
|
||||
|
||||
document.dispatchEvent(new CustomEvent('kimai.reloadContent'));
|
||||
|
||||
const updateUrl = this.options.url.update(event.id);
|
||||
API.patch(updateUrl, JSON.stringify(payload), () => {
|
||||
ALERT.success('action.update.success');
|
||||
document.dispatchEvent(new CustomEvent('kimai.reloadedContent'));
|
||||
}, (error) => {
|
||||
eventArg.revert();
|
||||
document.dispatchEvent(new CustomEvent('kimai.reloadedContent'));
|
||||
API.handleError('action.update.error', error);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,6 +12,6 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1021;
|
||||
background-color: var(--tblr-backdrop-bg);
|
||||
/*background-color: var(--tblr-backdrop-bg);*/
|
||||
opacity: $modal-backdrop-opacity;
|
||||
}
|
||||
|
||||
@@ -48,4 +48,12 @@ fieldset.form-fieldset > legend {
|
||||
.page-title {
|
||||
color: var(--tblr-body-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Highlighted text is not visible - https://github.com/tabler/tabler/issues/2603 */
|
||||
[data-bs-theme=dark] {
|
||||
::selection,
|
||||
.text-selected {
|
||||
background-color: var(--#{$prefix}primary);
|
||||
}
|
||||
}
|
||||
|
||||
312
composer.lock
generated
312
composer.lock
generated
@@ -435,16 +435,16 @@
|
||||
},
|
||||
{
|
||||
"name": "doctrine/collections",
|
||||
"version": "2.4.0",
|
||||
"version": "2.6.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/doctrine/collections.git",
|
||||
"reference": "9acfeea2e8666536edff3d77c531261c63680160"
|
||||
"reference": "7713da39d8e237f28411d6a616a3dce5e20d5de2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/doctrine/collections/zipball/9acfeea2e8666536edff3d77c531261c63680160",
|
||||
"reference": "9acfeea2e8666536edff3d77c531261c63680160",
|
||||
"url": "https://api.github.com/repos/doctrine/collections/zipball/7713da39d8e237f28411d6a616a3dce5e20d5de2",
|
||||
"reference": "7713da39d8e237f28411d6a616a3dce5e20d5de2",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -501,7 +501,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/doctrine/collections/issues",
|
||||
"source": "https://github.com/doctrine/collections/tree/2.4.0"
|
||||
"source": "https://github.com/doctrine/collections/tree/2.6.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -517,7 +517,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-10-25T09:18:13+00:00"
|
||||
"time": "2026-01-15T10:01:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "doctrine/common",
|
||||
@@ -980,16 +980,16 @@
|
||||
},
|
||||
{
|
||||
"name": "doctrine/event-manager",
|
||||
"version": "2.0.1",
|
||||
"version": "2.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/doctrine/event-manager.git",
|
||||
"reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e"
|
||||
"reference": "c07799fcf5ad362050960a0fd068dded40b1e312"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/doctrine/event-manager/zipball/b680156fa328f1dfd874fd48c7026c41570b9c6e",
|
||||
"reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e",
|
||||
"url": "https://api.github.com/repos/doctrine/event-manager/zipball/c07799fcf5ad362050960a0fd068dded40b1e312",
|
||||
"reference": "c07799fcf5ad362050960a0fd068dded40b1e312",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -999,10 +999,10 @@
|
||||
"doctrine/common": "<2.9"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/coding-standard": "^12",
|
||||
"phpstan/phpstan": "^1.8.8",
|
||||
"phpunit/phpunit": "^10.5",
|
||||
"vimeo/psalm": "^5.24"
|
||||
"doctrine/coding-standard": "^14",
|
||||
"phpdocumentor/guides-cli": "^1.4",
|
||||
"phpstan/phpstan": "^2.1.32",
|
||||
"phpunit/phpunit": "^10.5.58"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
@@ -1051,7 +1051,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/doctrine/event-manager/issues",
|
||||
"source": "https://github.com/doctrine/event-manager/tree/2.0.1"
|
||||
"source": "https://github.com/doctrine/event-manager/tree/2.1.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1067,7 +1067,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-05-22T20:47:39+00:00"
|
||||
"time": "2026-01-17T22:40:21+00:00"
|
||||
},
|
||||
{
|
||||
"name": "doctrine/inflector",
|
||||
@@ -2157,22 +2157,22 @@
|
||||
},
|
||||
{
|
||||
"name": "goetas-webservices/xsd2php-runtime",
|
||||
"version": "v0.2.17",
|
||||
"version": "0.2.18",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/goetas-webservices/xsd2php-runtime.git",
|
||||
"reference": "be15c48cda6adfab82e180a69dfa1937e208cfe1"
|
||||
"reference": "1215ce8504f2726b1a0c5025478da86a2791a0ba"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/goetas-webservices/xsd2php-runtime/zipball/be15c48cda6adfab82e180a69dfa1937e208cfe1",
|
||||
"reference": "be15c48cda6adfab82e180a69dfa1937e208cfe1",
|
||||
"url": "https://api.github.com/repos/goetas-webservices/xsd2php-runtime/zipball/1215ce8504f2726b1a0c5025478da86a2791a0ba",
|
||||
"reference": "1215ce8504f2726b1a0c5025478da86a2791a0ba",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"jms/serializer": "^1.2|^2.0|^3.0",
|
||||
"php": ">=7.1",
|
||||
"symfony/yaml": "^2.2|^3.0|^4.0|^5.0|^6.0|^7.0"
|
||||
"symfony/yaml": "^2.2|^3.0|^4.0|^5.0|^6.0|^7.0|^8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"jms/serializer": "1.4.1|1.6.1|1.6.2"
|
||||
@@ -2211,9 +2211,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/goetas-webservices/xsd2php-runtime/issues",
|
||||
"source": "https://github.com/goetas-webservices/xsd2php-runtime/tree/v0.2.17"
|
||||
"source": "https://github.com/goetas-webservices/xsd2php-runtime/tree/0.2.18"
|
||||
},
|
||||
"time": "2024-04-12T22:55:31+00:00"
|
||||
"time": "2026-01-16T19:24:00+00:00"
|
||||
},
|
||||
{
|
||||
"name": "horstoeko/mimedb",
|
||||
@@ -2329,16 +2329,16 @@
|
||||
},
|
||||
{
|
||||
"name": "horstoeko/zugferd",
|
||||
"version": "v1.0.118",
|
||||
"version": "v1.0.120",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/horstoeko/zugferd.git",
|
||||
"reference": "5259a34da6a5e5e92a764e7dd39cd7330335a579"
|
||||
"reference": "c143ec75a7ffc62e2b64a87d175db9f13ab843c1"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/horstoeko/zugferd/zipball/5259a34da6a5e5e92a764e7dd39cd7330335a579",
|
||||
"reference": "5259a34da6a5e5e92a764e7dd39cd7330335a579",
|
||||
"url": "https://api.github.com/repos/horstoeko/zugferd/zipball/c143ec75a7ffc62e2b64a87d175db9f13ab843c1",
|
||||
"reference": "c143ec75a7ffc62e2b64a87d175db9f13ab843c1",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2397,9 +2397,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/horstoeko/zugferd/issues",
|
||||
"source": "https://github.com/horstoeko/zugferd/tree/v1.0.118"
|
||||
"source": "https://github.com/horstoeko/zugferd/tree/v1.0.120"
|
||||
},
|
||||
"time": "2025-12-10T06:08:16+00:00"
|
||||
"time": "2026-01-07T09:45:19+00:00"
|
||||
},
|
||||
{
|
||||
"name": "horstoeko/zugferdublbridge",
|
||||
@@ -2722,16 +2722,16 @@
|
||||
},
|
||||
{
|
||||
"name": "kevinpapst/tabler-bundle",
|
||||
"version": "2.0.2",
|
||||
"version": "2.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/kevinpapst/TablerBundle.git",
|
||||
"reference": "6a3942ea16c97e8163bf20b9a24f4263ce6d8bd7"
|
||||
"reference": "b608ff05a7ba77b8fc32540bca086f6d18c2761e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/kevinpapst/TablerBundle/zipball/6a3942ea16c97e8163bf20b9a24f4263ce6d8bd7",
|
||||
"reference": "6a3942ea16c97e8163bf20b9a24f4263ce6d8bd7",
|
||||
"url": "https://api.github.com/repos/kevinpapst/TablerBundle/zipball/b608ff05a7ba77b8fc32540bca086f6d18c2761e",
|
||||
"reference": "b608ff05a7ba77b8fc32540bca086f6d18c2761e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2781,7 +2781,7 @@
|
||||
"description": "Admin/Backend theme bundle for Symfony based on Tabler.io",
|
||||
"support": {
|
||||
"issues": "https://github.com/kevinpapst/TablerBundle/issues",
|
||||
"source": "https://github.com/kevinpapst/TablerBundle/tree/2.0.2"
|
||||
"source": "https://github.com/kevinpapst/TablerBundle/tree/2.1.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2793,7 +2793,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-12-19T17:20:55+00:00"
|
||||
"time": "2026-01-19T09:05:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/csv",
|
||||
@@ -3584,16 +3584,16 @@
|
||||
},
|
||||
{
|
||||
"name": "nelmio/cors-bundle",
|
||||
"version": "2.6.0",
|
||||
"version": "2.6.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/nelmio/NelmioCorsBundle.git",
|
||||
"reference": "530217472204881cacd3671909f634b960c7b948"
|
||||
"reference": "3d80dbcd5d1eb5f8b20ed5199e1778d44c2e4d1c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/nelmio/NelmioCorsBundle/zipball/530217472204881cacd3671909f634b960c7b948",
|
||||
"reference": "530217472204881cacd3671909f634b960c7b948",
|
||||
"url": "https://api.github.com/repos/nelmio/NelmioCorsBundle/zipball/3d80dbcd5d1eb5f8b20ed5199e1778d44c2e4d1c",
|
||||
"reference": "3d80dbcd5d1eb5f8b20ed5199e1778d44c2e4d1c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3643,9 +3643,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/nelmio/NelmioCorsBundle/issues",
|
||||
"source": "https://github.com/nelmio/NelmioCorsBundle/tree/2.6.0"
|
||||
"source": "https://github.com/nelmio/NelmioCorsBundle/tree/2.6.1"
|
||||
},
|
||||
"time": "2025-10-23T06:57:22+00:00"
|
||||
"time": "2026-01-12T15:59:08+00:00"
|
||||
},
|
||||
{
|
||||
"name": "nikic/php-parser",
|
||||
@@ -4311,16 +4311,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpoffice/phpspreadsheet",
|
||||
"version": "2.4.2",
|
||||
"version": "2.4.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
|
||||
"reference": "931ad61fb2c229063fc4e7e665fb52b87249cc56"
|
||||
"reference": "3b204d00c19f9d809f8d2374f408b197f37ad0bd"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/931ad61fb2c229063fc4e7e665fb52b87249cc56",
|
||||
"reference": "931ad61fb2c229063fc4e7e665fb52b87249cc56",
|
||||
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/3b204d00c19f9d809f8d2374f408b197f37ad0bd",
|
||||
"reference": "3b204d00c19f9d809f8d2374f408b197f37ad0bd",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4342,8 +4342,6 @@
|
||||
"markbaker/complex": "^3.0",
|
||||
"markbaker/matrix": "^3.0",
|
||||
"php": ">=8.1.0 <8.6.0",
|
||||
"psr/http-client": "^1.0",
|
||||
"psr/http-factory": "^1.0",
|
||||
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
@@ -4394,6 +4392,9 @@
|
||||
},
|
||||
{
|
||||
"name": "Adrien Crivelli"
|
||||
},
|
||||
{
|
||||
"name": "Owen Leibman"
|
||||
}
|
||||
],
|
||||
"description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
|
||||
@@ -4410,9 +4411,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
|
||||
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/2.4.2"
|
||||
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/2.4.3"
|
||||
},
|
||||
"time": "2025-11-24T15:59:19+00:00"
|
||||
"time": "2026-01-11T06:08:40+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpoffice/phpword",
|
||||
@@ -4524,16 +4525,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpdoc-parser",
|
||||
"version": "2.3.0",
|
||||
"version": "2.3.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpdoc-parser.git",
|
||||
"reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495"
|
||||
"reference": "16dbf9937da8d4528ceb2145c9c7c0bd29e26374"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/1e0cd5370df5dd2e556a36b9c62f62e555870495",
|
||||
"reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495",
|
||||
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/16dbf9937da8d4528ceb2145c9c7c0bd29e26374",
|
||||
"reference": "16dbf9937da8d4528ceb2145c9c7c0bd29e26374",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4565,9 +4566,9 @@
|
||||
"description": "PHPDoc parser with support for nullable, intersection and generic types",
|
||||
"support": {
|
||||
"issues": "https://github.com/phpstan/phpdoc-parser/issues",
|
||||
"source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.0"
|
||||
"source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.1"
|
||||
},
|
||||
"time": "2025-08-30T15:50:23+00:00"
|
||||
"time": "2026-01-12T11:33:04+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/cache",
|
||||
@@ -4769,113 +4770,6 @@
|
||||
},
|
||||
"time": "2019-01-08T18:20:26+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/http-client",
|
||||
"version": "1.0.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/http-client.git",
|
||||
"reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
|
||||
"reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.0 || ^8.0",
|
||||
"psr/http-message": "^1.0 || ^2.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Psr\\Http\\Client\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "PHP-FIG",
|
||||
"homepage": "https://www.php-fig.org/"
|
||||
}
|
||||
],
|
||||
"description": "Common interface for HTTP clients",
|
||||
"homepage": "https://github.com/php-fig/http-client",
|
||||
"keywords": [
|
||||
"http",
|
||||
"http-client",
|
||||
"psr",
|
||||
"psr-18"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/php-fig/http-client"
|
||||
},
|
||||
"time": "2023-09-23T14:17:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/http-factory",
|
||||
"version": "1.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/http-factory.git",
|
||||
"reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
|
||||
"reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1",
|
||||
"psr/http-message": "^1.0 || ^2.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Psr\\Http\\Message\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "PHP-FIG",
|
||||
"homepage": "https://www.php-fig.org/"
|
||||
}
|
||||
],
|
||||
"description": "PSR-17: Common interfaces for PSR-7 HTTP message factories",
|
||||
"keywords": [
|
||||
"factory",
|
||||
"http",
|
||||
"message",
|
||||
"psr",
|
||||
"psr-17",
|
||||
"psr-7",
|
||||
"request",
|
||||
"response"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/php-fig/http-factory"
|
||||
},
|
||||
"time": "2024-04-15T12:06:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/http-message",
|
||||
"version": "2.0",
|
||||
@@ -5360,16 +5254,16 @@
|
||||
},
|
||||
{
|
||||
"name": "smalot/pdfparser",
|
||||
"version": "v2.12.2",
|
||||
"version": "v2.12.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/smalot/pdfparser.git",
|
||||
"reference": "370b7e983fafecb787a9bcfd73baab8038212ad1"
|
||||
"reference": "61c9bcafcb92899b76d8ebda6508267bae77e264"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/smalot/pdfparser/zipball/370b7e983fafecb787a9bcfd73baab8038212ad1",
|
||||
"reference": "370b7e983fafecb787a9bcfd73baab8038212ad1",
|
||||
"url": "https://api.github.com/repos/smalot/pdfparser/zipball/61c9bcafcb92899b76d8ebda6508267bae77e264",
|
||||
"reference": "61c9bcafcb92899b76d8ebda6508267bae77e264",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5405,22 +5299,22 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/smalot/pdfparser/issues",
|
||||
"source": "https://github.com/smalot/pdfparser/tree/v2.12.2"
|
||||
"source": "https://github.com/smalot/pdfparser/tree/v2.12.3"
|
||||
},
|
||||
"time": "2025-09-04T08:49:09+00:00"
|
||||
"time": "2026-01-08T08:04:04+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spomky-labs/otphp",
|
||||
"version": "11.4.1",
|
||||
"version": "11.4.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Spomky-Labs/otphp.git",
|
||||
"reference": "126c99b6cbbc18992cf3fba3b87931ba4e312482"
|
||||
"reference": "2a1b503fd1c1a5c751ab3c5cd37f2d2d26ab74ad"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Spomky-Labs/otphp/zipball/126c99b6cbbc18992cf3fba3b87931ba4e312482",
|
||||
"reference": "126c99b6cbbc18992cf3fba3b87931ba4e312482",
|
||||
"url": "https://api.github.com/repos/Spomky-Labs/otphp/zipball/2a1b503fd1c1a5c751ab3c5cd37f2d2d26ab74ad",
|
||||
"reference": "2a1b503fd1c1a5c751ab3c5cd37f2d2d26ab74ad",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5465,7 +5359,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/Spomky-Labs/otphp/issues",
|
||||
"source": "https://github.com/Spomky-Labs/otphp/tree/11.4.1"
|
||||
"source": "https://github.com/Spomky-Labs/otphp/tree/11.4.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -5477,7 +5371,7 @@
|
||||
"type": "patreon"
|
||||
}
|
||||
],
|
||||
"time": "2026-01-05T13:20:36+00:00"
|
||||
"time": "2026-01-23T10:53:01+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/asset",
|
||||
@@ -10907,16 +10801,16 @@
|
||||
},
|
||||
{
|
||||
"name": "zircote/swagger-php",
|
||||
"version": "5.7.7",
|
||||
"version": "5.7.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/zircote/swagger-php.git",
|
||||
"reference": "a9cf18ac6610fcb1673f108380a76cf9b145a74e"
|
||||
"reference": "5266730a2e5ba40430069dd9972ede2ec0b021a4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/zircote/swagger-php/zipball/a9cf18ac6610fcb1673f108380a76cf9b145a74e",
|
||||
"reference": "a9cf18ac6610fcb1673f108380a76cf9b145a74e",
|
||||
"url": "https://api.github.com/repos/zircote/swagger-php/zipball/5266730a2e5ba40430069dd9972ede2ec0b021a4",
|
||||
"reference": "5266730a2e5ba40430069dd9972ede2ec0b021a4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -10989,7 +10883,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/zircote/swagger-php/issues",
|
||||
"source": "https://github.com/zircote/swagger-php/tree/5.7.7"
|
||||
"source": "https://github.com/zircote/swagger-php/tree/5.7.8"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -10997,7 +10891,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-01-02T21:36:49+00:00"
|
||||
"time": "2026-01-13T22:19:24+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [
|
||||
@@ -11543,16 +11437,16 @@
|
||||
},
|
||||
{
|
||||
"name": "friendsofphp/php-cs-fixer",
|
||||
"version": "v3.92.4",
|
||||
"version": "v3.92.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
|
||||
"reference": "9e7488b19403423e02e8403cc1eb596baf4673b0"
|
||||
"reference": "260cc8c4a1d2f6d2f22cd4f9c70aa72e55ebac58"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/9e7488b19403423e02e8403cc1eb596baf4673b0",
|
||||
"reference": "9e7488b19403423e02e8403cc1eb596baf4673b0",
|
||||
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/260cc8c4a1d2f6d2f22cd4f9c70aa72e55ebac58",
|
||||
"reference": "260cc8c4a1d2f6d2f22cd4f9c70aa72e55ebac58",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -11635,7 +11529,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues",
|
||||
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.92.4"
|
||||
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.92.5"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -11643,7 +11537,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-01-04T00:38:52+00:00"
|
||||
"time": "2026-01-08T21:57:37+00:00"
|
||||
},
|
||||
{
|
||||
"name": "masterminds/html5",
|
||||
@@ -11832,11 +11726,11 @@
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "2.1.33",
|
||||
"version": "2.1.36",
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/9e800e6bee7d5bd02784d4c6069b48032d16224f",
|
||||
"reference": "9e800e6bee7d5bd02784d4c6069b48032d16224f",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/2132e5e2361d11d40af4c17faa16f043269a4cf3",
|
||||
"reference": "2132e5e2361d11d40af4c17faa16f043269a4cf3",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -11881,7 +11775,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-12-05T10:24:31+00:00"
|
||||
"time": "2026-01-21T13:58:26+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan-deprecation-rules",
|
||||
@@ -11932,21 +11826,21 @@
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan-doctrine",
|
||||
"version": "2.0.12",
|
||||
"version": "2.0.13",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpstan-doctrine.git",
|
||||
"reference": "d20ee0373d22735271f1eb4d631856b5f847d399"
|
||||
"reference": "2d2ad04a0ac14ac52e21ad47ec67a54a14355c1f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/d20ee0373d22735271f1eb4d631856b5f847d399",
|
||||
"reference": "d20ee0373d22735271f1eb4d631856b5f847d399",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/2d2ad04a0ac14ac52e21ad47ec67a54a14355c1f",
|
||||
"reference": "2d2ad04a0ac14ac52e21ad47ec67a54a14355c1f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.4 || ^8.0",
|
||||
"phpstan/phpstan": "^2.1.13"
|
||||
"phpstan/phpstan": "^2.1.34"
|
||||
},
|
||||
"conflict": {
|
||||
"doctrine/collections": "<1.0",
|
||||
@@ -11999,22 +11893,22 @@
|
||||
"description": "Doctrine extensions for PHPStan",
|
||||
"support": {
|
||||
"issues": "https://github.com/phpstan/phpstan-doctrine/issues",
|
||||
"source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.12"
|
||||
"source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.13"
|
||||
},
|
||||
"time": "2025-12-01T11:34:02+00:00"
|
||||
"time": "2026-01-18T16:15:40+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan-phpunit",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.12",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpstan-phpunit.git",
|
||||
"reference": "5e30669bef866eff70db8b58d72a5c185aa82414"
|
||||
"reference": "e4c5a22bf43d3d2bd5a780ad261a622ff62c49a4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/5e30669bef866eff70db8b58d72a5c185aa82414",
|
||||
"reference": "5e30669bef866eff70db8b58d72a5c185aa82414",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/e4c5a22bf43d3d2bd5a780ad261a622ff62c49a4",
|
||||
"reference": "e4c5a22bf43d3d2bd5a780ad261a622ff62c49a4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -12052,9 +11946,9 @@
|
||||
"description": "PHPUnit extensions and rules for PHPStan",
|
||||
"support": {
|
||||
"issues": "https://github.com/phpstan/phpstan-phpunit/issues",
|
||||
"source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.11"
|
||||
"source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.12"
|
||||
},
|
||||
"time": "2025-12-19T09:05:35+00:00"
|
||||
"time": "2026-01-22T13:40:00+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan-strict-rules",
|
||||
@@ -12106,16 +12000,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan-symfony",
|
||||
"version": "2.0.9",
|
||||
"version": "2.0.12",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpstan-symfony.git",
|
||||
"reference": "24d8c157aa483141b0579d705ef0aac9e1b95436"
|
||||
"reference": "a46dd92eaf15146cd932d897a272e59cd4108ce2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/24d8c157aa483141b0579d705ef0aac9e1b95436",
|
||||
"reference": "24d8c157aa483141b0579d705ef0aac9e1b95436",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/a46dd92eaf15146cd932d897a272e59cd4108ce2",
|
||||
"reference": "a46dd92eaf15146cd932d897a272e59cd4108ce2",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -12171,9 +12065,9 @@
|
||||
"description": "Symfony Framework extensions and rules for PHPStan",
|
||||
"support": {
|
||||
"issues": "https://github.com/phpstan/phpstan-symfony/issues",
|
||||
"source": "https://github.com/phpstan/phpstan-symfony/tree/2.0.9"
|
||||
"source": "https://github.com/phpstan/phpstan-symfony/tree/2.0.12"
|
||||
},
|
||||
"time": "2025-11-29T11:17:28+00:00"
|
||||
"time": "2026-01-23T09:04:33+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-code-coverage",
|
||||
|
||||
70
phpstan.neon
70
phpstan.neon
@@ -2644,16 +2644,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Form/Type/UserPreferenceType.php
|
||||
|
||||
-
|
||||
message: "#^PHPDoc tag @var for variable \\$collection contains generic class Doctrine\\\\Common\\\\Collections\\\\ArrayCollection but does not specify its types\\: TKey, T$#"
|
||||
count: 1
|
||||
path: src/Form/Type/UserPreferencesCollectionType.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$key of method Doctrine\\\\Common\\\\Collections\\\\ArrayCollection\\<\\(int\\|string\\),mixed\\>\\:\\:set\\(\\) expects \\(int\\|string\\), string\\|null given\\.$#"
|
||||
count: 1
|
||||
path: src/Form/Type/UserPreferencesCollectionType.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset mixed on mixed\\.$#"
|
||||
count: 1
|
||||
@@ -3819,61 +3809,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Utils/ProfileManager.php
|
||||
|
||||
-
|
||||
message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#"
|
||||
count: 1
|
||||
path: src/Utils/ReleaseVersion.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 0 on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/Utils/ReleaseVersion.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access property \\$body on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/Utils/ReleaseVersion.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access property \\$draft on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/Utils/ReleaseVersion.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access property \\$html_url on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/Utils/ReleaseVersion.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access property \\$prerelease on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/Utils/ReleaseVersion.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access property \\$published_at on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/Utils/ReleaseVersion.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access property \\$tag_name on mixed\\.$#"
|
||||
count: 2
|
||||
path: src/Utils/ReleaseVersion.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access property \\$zipball_url on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/Utils/ReleaseVersion.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Utils\\\\ReleaseVersion\\:\\:getReleasesFromGithub\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Utils/ReleaseVersion.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$json of function json_decode expects string, string\\|false given\\.$#"
|
||||
count: 1
|
||||
path: src/Utils/ReleaseVersion.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 'pattern' on mixed\\.$#"
|
||||
count: 1
|
||||
@@ -4008,8 +3943,3 @@ parameters:
|
||||
message: "#^Method App\\\\Form\\\\MultiUpdate\\\\MultiUpdateTableDTO\\:\\:setEntities\\(\\) has parameter \\$entities with generic interface Doctrine\\\\Common\\\\Collections\\\\Collection but does not specify its types\\: TKey, T$#"
|
||||
count: 1
|
||||
path: src/Form/MultiUpdate/MultiUpdateTableDTO.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Entity\\\\InvoiceTemplate\\:\\:getMetaFields\\(\\) return type with generic interface Doctrine\\\\Common\\\\Collections\\\\Collection does not specify its types\\: TKey, T$#"
|
||||
count: 1
|
||||
path: src/Entity/InvoiceTemplate.php
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<ini name="error_reporting" value="-1"/>
|
||||
<ini name="max_execution_time" value="-1"/>
|
||||
<ini name="date.timezone" value="UTC"/>
|
||||
<ini name="intl.default_locale" value="en_US"/>
|
||||
<ini name="intl.default_locale" value="en"/>
|
||||
<ini name="date.timezone" value="Europe/Vienna"/>
|
||||
<env name="KERNEL_CLASS" value="App\Kernel" force="true"/>
|
||||
<env name="SYMFONY_DEPRECATIONS_HELPER" value="weak"/>
|
||||
|
||||
1
public/build/app-rtl.15853b82.js
Normal file
1
public/build/app-rtl.15853b82.js
Normal file
@@ -0,0 +1 @@
|
||||
(self.webpackChunkkimai=self.webpackChunkkimai||[]).push([[113],{876:function(i,n,u){"use strict";u.r(n)},2395:function(i,n,u){u(876)}},function(i){var n;n=2395,i(i.s=n)}]);
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
(self.webpackChunkkimai=self.webpackChunkkimai||[]).push([[113],{177:function(i,n,u){u(7996)},7996:function(i,n,u){"use strict";u.r(n)}},function(i){var n;n=177,i(i.s=n)}]);
|
||||
File diff suppressed because one or more lines are too long
2
public/build/app.41748d55.js
Normal file
2
public/build/app.41748d55.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
public/build/calendar.36a92145.js
Normal file
2
public/build/calendar.36a92145.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
public/build/chart.56f16a68.js
Normal file
2
public/build/chart.56f16a68.js
Normal file
File diff suppressed because one or more lines are too long
@@ -1,7 +1,7 @@
|
||||
/*!
|
||||
* @kurkle/color v0.3.2
|
||||
* @kurkle/color v0.3.4
|
||||
* https://github.com/kurkle/color#readme
|
||||
* (c) 2023 Jukka Kurkela
|
||||
* (c) 2024 Jukka Kurkela
|
||||
* Released under the MIT License
|
||||
*/
|
||||
|
||||
2
public/build/dashboard.9708ae5e.js
Normal file
2
public/build/dashboard.9708ae5e.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -2,26 +2,26 @@
|
||||
"entrypoints": {
|
||||
"app": {
|
||||
"js": [
|
||||
"/build/runtime.6c399d29.js",
|
||||
"/build/app.307ea672.js"
|
||||
"/build/runtime.684e9f6d.js",
|
||||
"/build/app.41748d55.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/app.c18ba3c6.css"
|
||||
"/build/app.e64b4d08.css"
|
||||
]
|
||||
},
|
||||
"app-rtl": {
|
||||
"js": [
|
||||
"/build/runtime.6c399d29.js",
|
||||
"/build/app-rtl.fe7c5bf2.js"
|
||||
"/build/runtime.684e9f6d.js",
|
||||
"/build/app-rtl.15853b82.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/app-rtl.2003dce5.css"
|
||||
"/build/app-rtl.6b5a0ee9.css"
|
||||
]
|
||||
},
|
||||
"export-pdf": {
|
||||
"js": [
|
||||
"/build/runtime.6c399d29.js",
|
||||
"/build/export-pdf.5905454a.js"
|
||||
"/build/runtime.684e9f6d.js",
|
||||
"/build/export-pdf.395749ab.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/export-pdf.d8a6c23b.css"
|
||||
@@ -29,8 +29,8 @@
|
||||
},
|
||||
"invoice": {
|
||||
"js": [
|
||||
"/build/runtime.6c399d29.js",
|
||||
"/build/invoice.0217cc18.js"
|
||||
"/build/runtime.684e9f6d.js",
|
||||
"/build/invoice.42b319e4.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/invoice.36018785.css"
|
||||
@@ -38,8 +38,8 @@
|
||||
},
|
||||
"invoice-pdf": {
|
||||
"js": [
|
||||
"/build/runtime.6c399d29.js",
|
||||
"/build/invoice-pdf.1b6e735c.js"
|
||||
"/build/runtime.684e9f6d.js",
|
||||
"/build/invoice-pdf.26d98626.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/invoice-pdf.2b749265.css"
|
||||
@@ -47,14 +47,14 @@
|
||||
},
|
||||
"chart": {
|
||||
"js": [
|
||||
"/build/runtime.6c399d29.js",
|
||||
"/build/chart.2c85f027.js"
|
||||
"/build/runtime.684e9f6d.js",
|
||||
"/build/chart.56f16a68.js"
|
||||
]
|
||||
},
|
||||
"calendar": {
|
||||
"js": [
|
||||
"/build/runtime.6c399d29.js",
|
||||
"/build/calendar.cef61816.js"
|
||||
"/build/runtime.684e9f6d.js",
|
||||
"/build/calendar.36a92145.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/calendar.d757753e.css"
|
||||
@@ -62,8 +62,8 @@
|
||||
},
|
||||
"dashboard": {
|
||||
"js": [
|
||||
"/build/runtime.6c399d29.js",
|
||||
"/build/dashboard.faf42d4e.js"
|
||||
"/build/runtime.684e9f6d.js",
|
||||
"/build/dashboard.9708ae5e.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/dashboard.b7129fa1.css"
|
||||
@@ -71,8 +71,8 @@
|
||||
},
|
||||
"highlight": {
|
||||
"js": [
|
||||
"/build/runtime.6c399d29.js",
|
||||
"/build/highlight.718fe73d.js"
|
||||
"/build/runtime.684e9f6d.js",
|
||||
"/build/highlight.0296a734.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/highlight.98bf3927.css"
|
||||
@@ -80,23 +80,23 @@
|
||||
}
|
||||
},
|
||||
"integrity": {
|
||||
"/build/runtime.6c399d29.js": "sha384-/rm616f12czi8l/27GvWXtb3g608vJZf2XTUKxqCRI4tsa2vUHP+BW90edTok5zC",
|
||||
"/build/app.307ea672.js": "sha384-XNjtkar2JbWyJQan/C6jHZAYFI9X5rPcExLeDV86FYZMPJl0vNWMh8Pa8NgXX/EB",
|
||||
"/build/app.c18ba3c6.css": "sha384-qkIgqLzngG2NchdFVImbGMU49lWlZg6Y9D0z2P0u1j2NQstDicA4KqIkkNYcpErm",
|
||||
"/build/app-rtl.fe7c5bf2.js": "sha384-UyLXlR/3/H6oFjRrIMH04PA8bAjVQ8rdVWkWoiO4LLYasg4H3UNWJ+Xh+FrYekKh",
|
||||
"/build/app-rtl.2003dce5.css": "sha384-pl8GyGo8sRRw1zLh9D42ZAvo450onfxeszZWocUUuzq70jAOwOlaoPYh/yyc20U7",
|
||||
"/build/export-pdf.5905454a.js": "sha384-AkNj018YEQJSxC1e/3jIvlh0uR5tHnlUe9ciBbHKIoRIwj7658tCLNVaT98SNFEX",
|
||||
"/build/runtime.684e9f6d.js": "sha384-suKiEX2de4fdNqQzdYbUd6osp4AepD9FiMXl+1QdvgMW9dcQqUWQNQasf3KWzwLr",
|
||||
"/build/app.41748d55.js": "sha384-iVPHnOXYjVDuq8567h8I3U2gl5SCV3mNMFNQCJdgMiNuFMZ4KO7PXOLanjgcGzTt",
|
||||
"/build/app.e64b4d08.css": "sha384-Rapl6RNn58g7oayglp7MWOER7tDNPpChx6RKOOWe0mg6uFgaJjy3M51ZSkxXm+Hg",
|
||||
"/build/app-rtl.15853b82.js": "sha384-UnKKgLMu9FnRT+CFE0no/+UiUks012bYriQdUWa6f02mo6Lswl947mPybjvKL503",
|
||||
"/build/app-rtl.6b5a0ee9.css": "sha384-eW8BEaA6S1jAFPB8lUCBTvcm4wZZQ3SHbiHVZlxe814TXGT9L4/4BN8CuuGehZ6o",
|
||||
"/build/export-pdf.395749ab.js": "sha384-3Hjvmu4FC/0dhHnR8kyRBU7k2xMNy1lxBpGgOkrw8PxXnwyQDM8/5bQmkJbjVT1+",
|
||||
"/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg",
|
||||
"/build/invoice.0217cc18.js": "sha384-ORySQp4DBSYc7so90AAb8h5abgQtz3tdoO+Zv1uHJ2XqSPMAGtFBrb+Rdzc6sZg1",
|
||||
"/build/invoice.42b319e4.js": "sha384-xxK7sCe/ZhTjMPFPeX1xvILURxNRZz2hJHZxAGVaw9zE6TC++2/6y2eKqg8cz852",
|
||||
"/build/invoice.36018785.css": "sha384-jukM9uZ6pexDxXKgZThSxiqXimzsxzniBMHz08N9x8ryXZYkM5r/ZgaainCV0+J6",
|
||||
"/build/invoice-pdf.1b6e735c.js": "sha384-8I5YMkETBNl7wD12V/hKX2YGfwH+V6PXWsu4I+1xXXx9bDzqDhtq+S7x/jey2/Wr",
|
||||
"/build/invoice-pdf.26d98626.js": "sha384-gwNzQiU1y6qU/M9DPGiNW0MVZkLctEHk37sCES2X9ov+zugEaDABdkMjKBYOC9lz",
|
||||
"/build/invoice-pdf.2b749265.css": "sha384-DXXgkz2WWnrWnfBnXX5fmfPQSPb98upMnWxYKwTGYS04EhrPIWfDCutB2unIrWh7",
|
||||
"/build/chart.2c85f027.js": "sha384-5Dm9aFCCSzlh5QY/vtU7/UrBkKt6aWnTccjljmpbQVFfzSEfFh0wUUV8qlXqyFJj",
|
||||
"/build/calendar.cef61816.js": "sha384-qCpJrSB6fZLXT94oiWHofGbX8C0TFl86oCMTdeKWqCmNGJxABkIVo+8k19p7sGlY",
|
||||
"/build/chart.56f16a68.js": "sha384-SWnYjAbZ8OWEvTP+IZfGuBMWJIcH9OZWLYQ4p38KMPmqfufsc2zhzqWkALO3mCBO",
|
||||
"/build/calendar.36a92145.js": "sha384-ZPLxxbep6OnPeUiIeBXRE1iscZFgbNuTpn3rx2Jpf1KR2l6sxIuNsieDUskbmQ3m",
|
||||
"/build/calendar.d757753e.css": "sha384-cTmQMgHYjd2gfObFWmEUph7qQLCyXaIkneSf+bQ2mqVmZwqOB+pJOm/UYTyTjALJ",
|
||||
"/build/dashboard.faf42d4e.js": "sha384-xkM5D8wBK1sJekLI9DUQBxTjxjomfPQWWtWCo7NX03jNnFZtMv5anM0IISLWaFY0",
|
||||
"/build/dashboard.9708ae5e.js": "sha384-QN7XIQuxFZu76sHVrgdpZL81+Q2VTwcgF3aI2CnpwYZiMoPbzrcTGfPL3RDy696t",
|
||||
"/build/dashboard.b7129fa1.css": "sha384-2nn5hLA+3YedgHYBpge62S8Losj8aoPwK9Zk9EvN1xEYatvOUQ7H3rIR2UUJAGOS",
|
||||
"/build/highlight.718fe73d.js": "sha384-eHb21blXJ7SKqCFlMSxl4oblpusDif1B0Tsr2Iw1xQfUqxZxwtFwYYvMPllfw7BY",
|
||||
"/build/highlight.0296a734.js": "sha384-vGFKI/KM+uyGIlzKsKhQh4w41hQteB4u14oRYqObMT03/YTZXq6Re5nUN6g3sGQH",
|
||||
"/build/highlight.98bf3927.css": "sha384-YgweSwDwN0dI4DEmh478xYVw/TewJYvCO1QTbWHpaHFDPuLrZvYMR0Tc+QfFxVPE"
|
||||
}
|
||||
}
|
||||
1
public/build/export-pdf.395749ab.js
Normal file
1
public/build/export-pdf.395749ab.js
Normal file
@@ -0,0 +1 @@
|
||||
(self.webpackChunkkimai=self.webpackChunkkimai||[]).push([[872],{878:function(i,n,u){u(3385)},3385:function(i,n,u){"use strict";u.r(n)}},function(i){var n;n=878,i(i.s=n)}]);
|
||||
@@ -1 +0,0 @@
|
||||
(self.webpackChunkkimai=self.webpackChunkkimai||[]).push([[872],{585:function(i,n,u){"use strict";u.r(n)},2352:function(i,n,u){u(585)}},function(i){var n;n=2352,i(i.s=n)}]);
|
||||
1
public/build/highlight.0296a734.js
Normal file
1
public/build/highlight.0296a734.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
(self.webpackChunkkimai=self.webpackChunkkimai||[]).push([[117],{2049:function(i,n,u){u(4860)},4860:function(i,n,u){"use strict";u.r(n)}},function(i){var n;n=2049,i(i.s=n)}]);
|
||||
1
public/build/invoice-pdf.26d98626.js
Normal file
1
public/build/invoice-pdf.26d98626.js
Normal file
@@ -0,0 +1 @@
|
||||
(self.webpackChunkkimai=self.webpackChunkkimai||[]).push([[117],{1555:function(i,n,u){u(6620)},6620:function(i,n,u){"use strict";u.r(n)}},function(i){var n;n=1555,i(i.s=n)}]);
|
||||
@@ -1 +0,0 @@
|
||||
(self.webpackChunkkimai=self.webpackChunkkimai||[]).push([[896],{239:function(i,n,u){"use strict";u.r(n)},8478:function(i,n,u){u(239)}},function(i){var n;n=8478,i(i.s=n)}]);
|
||||
1
public/build/invoice.42b319e4.js
Normal file
1
public/build/invoice.42b319e4.js
Normal file
@@ -0,0 +1 @@
|
||||
(self.webpackChunkkimai=self.webpackChunkkimai||[]).push([[896],{3631:function(i,n,u){"use strict";u.r(n)},4820:function(i,n,u){u(3631)}},function(i){var n;n=4820,i(i.s=n)}]);
|
||||
@@ -1,22 +1,22 @@
|
||||
{
|
||||
"build/app.css": "/build/app.c18ba3c6.css",
|
||||
"build/app.js": "/build/app.307ea672.js",
|
||||
"build/app-rtl.css": "/build/app-rtl.2003dce5.css",
|
||||
"build/app-rtl.js": "/build/app-rtl.fe7c5bf2.js",
|
||||
"build/app.css": "/build/app.e64b4d08.css",
|
||||
"build/app.js": "/build/app.41748d55.js",
|
||||
"build/app-rtl.css": "/build/app-rtl.6b5a0ee9.css",
|
||||
"build/app-rtl.js": "/build/app-rtl.15853b82.js",
|
||||
"build/export-pdf.css": "/build/export-pdf.d8a6c23b.css",
|
||||
"build/export-pdf.js": "/build/export-pdf.5905454a.js",
|
||||
"build/export-pdf.js": "/build/export-pdf.395749ab.js",
|
||||
"build/invoice.css": "/build/invoice.36018785.css",
|
||||
"build/invoice.js": "/build/invoice.0217cc18.js",
|
||||
"build/invoice.js": "/build/invoice.42b319e4.js",
|
||||
"build/invoice-pdf.css": "/build/invoice-pdf.2b749265.css",
|
||||
"build/invoice-pdf.js": "/build/invoice-pdf.1b6e735c.js",
|
||||
"build/chart.js": "/build/chart.2c85f027.js",
|
||||
"build/invoice-pdf.js": "/build/invoice-pdf.26d98626.js",
|
||||
"build/chart.js": "/build/chart.56f16a68.js",
|
||||
"build/calendar.css": "/build/calendar.d757753e.css",
|
||||
"build/calendar.js": "/build/calendar.cef61816.js",
|
||||
"build/calendar.js": "/build/calendar.36a92145.js",
|
||||
"build/dashboard.css": "/build/dashboard.b7129fa1.css",
|
||||
"build/dashboard.js": "/build/dashboard.faf42d4e.js",
|
||||
"build/dashboard.js": "/build/dashboard.9708ae5e.js",
|
||||
"build/highlight.css": "/build/highlight.98bf3927.css",
|
||||
"build/highlight.js": "/build/highlight.718fe73d.js",
|
||||
"build/runtime.js": "/build/runtime.6c399d29.js",
|
||||
"build/highlight.js": "/build/highlight.0296a734.js",
|
||||
"build/runtime.js": "/build/runtime.684e9f6d.js",
|
||||
"build/fonts/fa-solid-900.ttf": "/build/fonts/fa-solid-900.2582b0e4.ttf",
|
||||
"build/fonts/fa-brands-400.ttf": "/build/fonts/fa-brands-400.1815e004.ttf",
|
||||
"build/fonts/fa-solid-900.woff2": "/build/fonts/fa-solid-900.2463b90d.woff2",
|
||||
|
||||
1
public/build/runtime.684e9f6d.js
Normal file
1
public/build/runtime.684e9f6d.js
Normal file
@@ -0,0 +1 @@
|
||||
!function(){"use strict";var e,r={},n={};function t(e){var o=n[e];if(void 0!==o)return o.exports;var i=n[e]={id:e,loaded:!1,exports:{}};return r[e].call(i.exports,i,i.exports,t),i.loaded=!0,i.exports}t.m=r,t.amdO={},e=[],t.O=function(r,n,o,i){if(!n){var u=1/0;for(l=0;l<e.length;l++){n=e[l][0],o=e[l][1],i=e[l][2];for(var f=!0,a=0;a<n.length;a++)(!1&i||u>=i)&&Object.keys(t.O).every(function(e){return t.O[e](n[a])})?n.splice(a--,1):(f=!1,i<u&&(u=i));if(f){e.splice(l--,1);var c=o();void 0!==c&&(r=c)}}return r}i=i||0;for(var l=e.length;l>0&&e[l-1][2]>i;l--)e[l]=e[l-1];e[l]=[n,o,i]},t.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(r,{a:r}),r},t.d=function(e,r){for(var n in r)t.o(r,n)&&!t.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:r[n]})},t.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),t.hmd=function(e){return(e=Object.create(e)).children||(e.children=[]),Object.defineProperty(e,"exports",{enumerable:!0,set:function(){throw new Error("ES Modules may not assign module.exports or exports.*, Use ESM export syntax, instead: "+e.id)}}),e},t.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},function(){var e={121:0};t.O.j=function(r){return 0===e[r]};var r=function(r,n){var o,i,u=n[0],f=n[1],a=n[2],c=0;if(u.some(function(r){return 0!==e[r]})){for(o in f)t.o(f,o)&&(t.m[o]=f[o]);if(a)var l=a(t)}for(r&&r(n);c<u.length;c++)i=u[c],t.o(e,i)&&e[i]&&e[i][0](),e[i]=0;return t.O(l)},n=self.webpackChunkkimai=self.webpackChunkkimai||[];n.forEach(r.bind(null,0)),n.push=r.bind(null,n.push.bind(n))}()}();
|
||||
@@ -1 +0,0 @@
|
||||
!function(){"use strict";var e,r={},n={};function t(e){var o=n[e];if(void 0!==o)return o.exports;var i=n[e]={id:e,loaded:!1,exports:{}};return r[e].call(i.exports,i,i.exports,t),i.loaded=!0,i.exports}t.m=r,t.amdO={},e=[],t.O=function(r,n,o,i){if(!n){var u=1/0;for(l=0;l<e.length;l++){n=e[l][0],o=e[l][1],i=e[l][2];for(var f=!0,a=0;a<n.length;a++)(!1&i||u>=i)&&Object.keys(t.O).every((function(e){return t.O[e](n[a])}))?n.splice(a--,1):(f=!1,i<u&&(u=i));if(f){e.splice(l--,1);var c=o();void 0!==c&&(r=c)}}return r}i=i||0;for(var l=e.length;l>0&&e[l-1][2]>i;l--)e[l]=e[l-1];e[l]=[n,o,i]},t.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(r,{a:r}),r},t.d=function(e,r){for(var n in r)t.o(r,n)&&!t.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:r[n]})},t.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),t.hmd=function(e){return(e=Object.create(e)).children||(e.children=[]),Object.defineProperty(e,"exports",{enumerable:!0,set:function(){throw new Error("ES Modules may not assign module.exports or exports.*, Use ESM export syntax, instead: "+e.id)}}),e},t.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},function(){var e={121:0};t.O.j=function(r){return 0===e[r]};var r=function(r,n){var o,i,u=n[0],f=n[1],a=n[2],c=0;if(u.some((function(r){return 0!==e[r]}))){for(o in f)t.o(f,o)&&(t.m[o]=f[o]);if(a)var l=a(t)}for(r&&r(n);c<u.length;c++)i=u[c],t.o(e,i)&&e[i]&&e[i][0](),e[i]=0;return t.O(l)},n=self.webpackChunkkimai=self.webpackChunkkimai||[];n.forEach(r.bind(null,0)),n.push=r.bind(null,n.push.bind(n))}()}();
|
||||
@@ -508,7 +508,7 @@ final class TranslationCommand extends Command
|
||||
\sprintf('Missing english translation for key: %s in file %s', $key, $file)
|
||||
);
|
||||
}
|
||||
$unit->target[0] = $translations[$key];
|
||||
$unit->target[0] = $translations[$key]; // @phpstan-ignore-line
|
||||
$unit->target['state'] = 'needs-translation';
|
||||
$foundEmpty = true;
|
||||
}
|
||||
|
||||
@@ -17,11 +17,11 @@ final class Constants
|
||||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '2.46.0';
|
||||
public const VERSION = '2.47.0';
|
||||
/**
|
||||
* The current release: major * 10000 + minor * 100 + patch
|
||||
*/
|
||||
public const VERSION_ID = 24600;
|
||||
public const VERSION_ID = 24700;
|
||||
/**
|
||||
* The software name
|
||||
*/
|
||||
|
||||
@@ -301,7 +301,7 @@ class InvoiceTemplate implements EntityWithMetaFields
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection|MetaTableTypeInterface[]
|
||||
* @return Collection<int, InvoiceTemplateMeta>
|
||||
*/
|
||||
public function getMetaFields(): Collection
|
||||
{
|
||||
|
||||
@@ -376,14 +376,24 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called from the "edit user preferences" form.
|
||||
* Therefor it just merges the values for existing preferences and adds new ones.
|
||||
* But it will NOT remove existing preferences or replace the underlying collection.
|
||||
*
|
||||
* @param iterable<UserPreference> $preferences
|
||||
*/
|
||||
public function setPreferences(iterable $preferences): User
|
||||
{
|
||||
$this->preferences = new ArrayCollection();
|
||||
|
||||
foreach ($preferences as $preference) {
|
||||
$this->addPreference($preference);
|
||||
if (($name = $preference->getName()) === null) {
|
||||
continue;
|
||||
}
|
||||
$p = $this->getPreference($name);
|
||||
if ($p === null) {
|
||||
$this->addPreference($preference);
|
||||
} else {
|
||||
$p->setValue($preference->getValue());
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
|
||||
@@ -24,7 +24,7 @@ use App\Repository\Query\ActivityQuery;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Twig\SecurityPolicy\ExportPolicy;
|
||||
use App\Twig\SecurityPolicy\StrictPolicy;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Exclude;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@@ -100,9 +100,12 @@ class HtmlRenderer implements ExportRendererInterface
|
||||
$summary = $this->calculateSummary($exportItems);
|
||||
|
||||
// enable basic security measures
|
||||
$sandbox = new SandboxExtension(new ExportPolicy());
|
||||
if (!$this->twig->hasExtension(SandboxExtension::class)) {
|
||||
$this->twig->addExtension(new SandboxExtension(new StrictPolicy()));
|
||||
}
|
||||
|
||||
$sandbox = $this->twig->getExtension(SandboxExtension::class);
|
||||
$sandbox->enableSandbox();
|
||||
$this->twig->addExtension($sandbox);
|
||||
|
||||
$content = $this->twig->render($this->getTemplate(), array_merge([
|
||||
'entries' => $exportItems,
|
||||
|
||||
@@ -17,7 +17,7 @@ use App\Pdf\PdfContext;
|
||||
use App\Pdf\PdfRendererTrait;
|
||||
use App\Project\ProjectStatisticService;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Twig\SecurityPolicy\ExportPolicy;
|
||||
use App\Twig\SecurityPolicy\StrictPolicy;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Exclude;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Twig\Environment;
|
||||
@@ -103,9 +103,12 @@ class PDFRenderer implements DispositionInlineInterface, ExportRendererInterface
|
||||
$summary = $this->calculateSummary($exportItems);
|
||||
|
||||
// enable basic security measures
|
||||
$sandbox = new SandboxExtension(new ExportPolicy());
|
||||
if (!$this->twig->hasExtension(SandboxExtension::class)) {
|
||||
$this->twig->addExtension(new SandboxExtension(new StrictPolicy()));
|
||||
}
|
||||
|
||||
$sandbox = $this->twig->getExtension(SandboxExtension::class);
|
||||
$sandbox->enableSandbox();
|
||||
$this->twig->addExtension($sandbox);
|
||||
|
||||
$content = $this->twig->render($this->getTemplate(), array_merge([
|
||||
'entries' => $exportItems,
|
||||
@@ -116,6 +119,8 @@ class PDFRenderer implements DispositionInlineInterface, ExportRendererInterface
|
||||
'pdfContext' => $context
|
||||
], $this->getOptions($query)));
|
||||
|
||||
$sandbox->disableSandbox();
|
||||
|
||||
$pdfOptions = array_merge($context->getOptions(), $this->getPdfOptions());
|
||||
|
||||
$content = $this->converter->convertToPdf($content, $pdfOptions);
|
||||
|
||||
@@ -30,11 +30,10 @@ final class UserPreferencesCollectionType extends AbstractType
|
||||
$builder->addEventListener(
|
||||
FormEvents::PRE_SET_DATA,
|
||||
function (FormEvent $event): void {
|
||||
/** @var ArrayCollection<UserPreference> $collection */
|
||||
/** @var ArrayCollection<int, UserPreference> $collection */
|
||||
$collection = $event->getData();
|
||||
$preferences = new ArrayCollection();
|
||||
foreach ($collection as $collectionItem) {
|
||||
$collection->removeElement($collectionItem);
|
||||
|
||||
if (!($collectionItem instanceof UserPreference)) {
|
||||
continue;
|
||||
}
|
||||
@@ -44,8 +43,9 @@ final class UserPreferencesCollectionType extends AbstractType
|
||||
continue;
|
||||
}
|
||||
|
||||
$collection->set($collectionItem->getName(), $collectionItem);
|
||||
$preferences->set($collectionItem->getName(), clone $collectionItem);
|
||||
}
|
||||
$event->setData($preferences);
|
||||
},
|
||||
// must be a higher priority then the listener in UserPreferenceType
|
||||
100
|
||||
|
||||
@@ -13,7 +13,7 @@ use App\Invoice\InvoiceModel;
|
||||
use App\Invoice\RendererInterface;
|
||||
use App\Model\InvoiceDocument;
|
||||
use App\Twig\LocaleFormatExtensions;
|
||||
use App\Twig\SecurityPolicy\InvoicePolicy;
|
||||
use App\Twig\SecurityPolicy\StrictPolicy;
|
||||
use Symfony\Bridge\Twig\Extension\TranslationExtension;
|
||||
use Symfony\Contracts\Translation\LocaleAwareInterface;
|
||||
use Twig\Environment;
|
||||
@@ -64,7 +64,7 @@ abstract class AbstractTwigRenderer implements RendererInterface
|
||||
}
|
||||
|
||||
if (!$twig->hasExtension(SandboxExtension::class)) {
|
||||
$twig->addExtension(new SandboxExtension(new InvoicePolicy()));
|
||||
$twig->addExtension(new SandboxExtension(new StrictPolicy()));
|
||||
}
|
||||
|
||||
$sandbox = $twig->getExtension(SandboxExtension::class);
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Twig\SecurityPolicy;
|
||||
|
||||
use Twig\Sandbox\SecurityPolicyInterface;
|
||||
|
||||
final class ChainPolicy implements SecurityPolicyInterface
|
||||
{
|
||||
/** @var array<SecurityPolicyInterface> */
|
||||
private array $policies = [];
|
||||
|
||||
public function addPolicy(SecurityPolicyInterface $policy): void
|
||||
{
|
||||
$this->policies[] = $policy;
|
||||
}
|
||||
|
||||
public function checkSecurity($tags, $filters, $functions): void
|
||||
{
|
||||
foreach ($this->policies as $policy) {
|
||||
$policy->checkSecurity($tags, $filters, $functions);
|
||||
}
|
||||
}
|
||||
|
||||
public function checkMethodAllowed($obj, $method): void
|
||||
{
|
||||
foreach ($this->policies as $policy) {
|
||||
$policy->checkMethodAllowed($obj, $method);
|
||||
}
|
||||
}
|
||||
|
||||
public function checkPropertyAllowed($obj, $property): void
|
||||
{
|
||||
foreach ($this->policies as $policy) {
|
||||
$policy->checkPropertyAllowed($obj, $property);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Twig\SecurityPolicy;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Pdf\PdfContext;
|
||||
use Symfony\Bridge\Twig\AppVariable;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\ServerBag;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
use Twig\Sandbox\SecurityNotAllowedMethodError;
|
||||
use Twig\Sandbox\SecurityPolicyInterface;
|
||||
|
||||
/**
|
||||
* The Twig environment needs the sandbox extension, which itself needs a policy to start working.
|
||||
*/
|
||||
final class DefaultPolicy implements SecurityPolicyInterface
|
||||
{
|
||||
public function checkSecurity($tags, $filters, $functions): void
|
||||
{
|
||||
}
|
||||
|
||||
public function checkMethodAllowed($obj, $method): void
|
||||
{
|
||||
if ($obj instanceof ServerBag) {
|
||||
throw new SecurityNotAllowedMethodError('Tried to access server environment', ServerBag::class, $method);
|
||||
}
|
||||
|
||||
if ($obj instanceof SessionInterface) {
|
||||
throw new SecurityNotAllowedMethodError('Tried to access session', SessionInterface::class, $method);
|
||||
}
|
||||
|
||||
$lcm = strtolower($method);
|
||||
|
||||
if ($obj instanceof PdfContext) {
|
||||
if ($lcm !== 'setoption') {
|
||||
throw new SecurityNotAllowedMethodError('Tried to access forbidden method on PdfContext', PdfContext::class, $method);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!str_starts_with($lcm, 'has') && !str_starts_with($lcm, 'is') && !str_starts_with($lcm, 'get') && $lcm !== '__tostring') {
|
||||
throw new SecurityNotAllowedMethodError('Tried to access non-read method', $obj::class, $method);
|
||||
}
|
||||
|
||||
if ($obj instanceof Request) {
|
||||
if (!str_starts_with($lcm, 'get')) {
|
||||
throw new SecurityNotAllowedMethodError('Tried to call setter() of app variable', AppVariable::class, $method);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($obj instanceof AppVariable) {
|
||||
if (!\in_array($lcm, ['getrequest', 'getuser', 'getlocale'], true)) {
|
||||
throw new SecurityNotAllowedMethodError('Tried to access forbidden app variable method', User::class, $method);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($obj instanceof User) {
|
||||
if (\in_array($lcm, ['getpassword', 'gettotpsecret', 'getplainpassword', 'getconfirmationtoken', 'gettotpauthenticationconfiguration'], true)) {
|
||||
throw new SecurityNotAllowedMethodError('Tried to access user secrets', User::class, $method);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function checkPropertyAllowed($obj, $property): void
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Twig\SecurityPolicy;
|
||||
|
||||
use Twig\Sandbox\SecurityPolicyInterface;
|
||||
|
||||
/**
|
||||
* Represents the security policy for custom Twig export templates.
|
||||
*/
|
||||
final class ExportPolicy implements SecurityPolicyInterface
|
||||
{
|
||||
private ChainPolicy $policy;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->policy = new ChainPolicy();
|
||||
$this->policy->addPolicy(new DefaultPolicy());
|
||||
}
|
||||
|
||||
public function checkSecurity($tags, $filters, $functions): void
|
||||
{
|
||||
$this->policy->checkSecurity($tags, $filters, $functions);
|
||||
}
|
||||
|
||||
public function checkMethodAllowed($obj, $method): void
|
||||
{
|
||||
$this->policy->checkMethodAllowed($obj, $method);
|
||||
}
|
||||
|
||||
public function checkPropertyAllowed($obj, $property): void
|
||||
{
|
||||
$this->policy->checkPropertyAllowed($obj, $property);
|
||||
}
|
||||
}
|
||||
@@ -1,228 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Twig\SecurityPolicy;
|
||||
|
||||
use App\Invoice\InvoiceModel;
|
||||
use App\Pdf\PdfContext;
|
||||
use Symfony\Component\String\UnicodeString;
|
||||
use Twig\Markup;
|
||||
use Twig\Sandbox\SecurityPolicy;
|
||||
use Twig\Sandbox\SecurityPolicyInterface;
|
||||
use Twig\Template;
|
||||
|
||||
/**
|
||||
* Represents the security policy for custom Twig invoice templates.
|
||||
*/
|
||||
final class InvoicePolicy implements SecurityPolicyInterface
|
||||
{
|
||||
private SecurityPolicyInterface $default;
|
||||
private SecurityPolicyInterface $security;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->default = new DefaultPolicy();
|
||||
$this->security = new SecurityPolicy(
|
||||
['block', 'if', 'for', 'set', 'extends', 'import'],
|
||||
[
|
||||
// =================================================================
|
||||
// vendor/twig/twig/src/Extension/CoreExtension.php
|
||||
|
||||
// formatting filters
|
||||
'date',
|
||||
'date_modify',
|
||||
'format',
|
||||
'replace',
|
||||
'number_format',
|
||||
'abs',
|
||||
'round',
|
||||
|
||||
// encoding
|
||||
'url_encode',
|
||||
'json_encode',
|
||||
'convert_encoding',
|
||||
|
||||
// string filters
|
||||
'title',
|
||||
'capitalize',
|
||||
'upper',
|
||||
'lower',
|
||||
'striptags',
|
||||
'trim',
|
||||
'nl2br',
|
||||
'spaceless',
|
||||
|
||||
// array helpers
|
||||
'join',
|
||||
'split',
|
||||
'sort',
|
||||
'merge',
|
||||
'batch',
|
||||
'column',
|
||||
'filter',
|
||||
'map',
|
||||
'reduce',
|
||||
|
||||
// string/array filters
|
||||
'reverse',
|
||||
'length',
|
||||
'slice',
|
||||
'first',
|
||||
'last',
|
||||
|
||||
// iteration and runtime
|
||||
'default',
|
||||
'keys',
|
||||
|
||||
// =================================================================
|
||||
// vendor/twig/twig/src/Extension/EscaperExtension.php
|
||||
'escape',
|
||||
'e',
|
||||
'raw',
|
||||
|
||||
// =================================================================
|
||||
// vendor/symfony/twig-bridge/Extension/TranslationExtension.php
|
||||
'trans',
|
||||
|
||||
// =================================================================
|
||||
// vendor/twig/string-extra/StringExtension.php
|
||||
'u',
|
||||
'slug',
|
||||
|
||||
// =================================================================
|
||||
// vendor/twig/intl-extra/IntlExtension.php
|
||||
'country_name',
|
||||
'currency_name',
|
||||
'currency_symbol',
|
||||
'language_name',
|
||||
'locale_name',
|
||||
'timezone_name',
|
||||
'format_currency',
|
||||
'format_number',
|
||||
'format_decimal_number',
|
||||
'format_currency_number',
|
||||
'format_percent_number',
|
||||
'format_scientific_number',
|
||||
'format_spellout_number',
|
||||
'format_ordinal_number',
|
||||
'format_duration_number',
|
||||
'format_datetime',
|
||||
'format_date',
|
||||
'format_time',
|
||||
|
||||
// =================================================================
|
||||
// src/Twig/LocaleFormatExtensions.php
|
||||
'month_name',
|
||||
'day_name',
|
||||
'date_short',
|
||||
'date_time',
|
||||
'date_full',
|
||||
'date_format',
|
||||
'date_weekday',
|
||||
'time',
|
||||
'duration',
|
||||
'duration_decimal',
|
||||
'money',
|
||||
'amount',
|
||||
|
||||
// =================================================================
|
||||
// src/Twig/RuntimeExtensions.php
|
||||
'md2html',
|
||||
'desc2html',
|
||||
'comment2html',
|
||||
'comment1line',
|
||||
|
||||
// =================================================================
|
||||
// src/Twig/Extensions.php
|
||||
'multiline_indent',
|
||||
'color',
|
||||
'font_contrast',
|
||||
'default_color',
|
||||
'nl2str',
|
||||
],
|
||||
[
|
||||
PdfContext::class => ['setoption'],
|
||||
InvoiceModel::class => ['toarray'],
|
||||
],
|
||||
[], // properties
|
||||
[
|
||||
// =================================================================
|
||||
// vendor/twig/twig/src/Extension/CoreExtension.php
|
||||
'max',
|
||||
'min',
|
||||
'range',
|
||||
'constant',
|
||||
'cycle',
|
||||
'random',
|
||||
'date',
|
||||
'asset',
|
||||
'range',
|
||||
|
||||
// =================================================================
|
||||
// vendor/symfony/twig-bridge/Extension/TranslationExtension.php
|
||||
't',
|
||||
|
||||
// =================================================================
|
||||
// vendor/symfony/webpack-encore-bundle/src/Twig/EntryFilesTwigExtension.php
|
||||
'encore_entry_css_source',
|
||||
|
||||
// =================================================================
|
||||
// vendor/symfony/twig-bridge/Extension/AssetExtension.php
|
||||
'asset',
|
||||
|
||||
// =================================================================
|
||||
// vendor/symfony/twig-bridge/Extension/SecurityExtension.php
|
||||
'is_granted',
|
||||
|
||||
// =================================================================
|
||||
// Twig/RuntimeExtensions.php
|
||||
'qr_code_data_uri',
|
||||
|
||||
// =================================================================
|
||||
// Twig/Configuration.php
|
||||
'config',
|
||||
|
||||
// =================================================================
|
||||
// Twig/LocaleFormatExtensions.php
|
||||
'create_date',
|
||||
'month_names',
|
||||
'locale_format',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function checkSecurity($tags, $filters, $functions): void
|
||||
{
|
||||
$this->default->checkSecurity($tags, $filters, $functions);
|
||||
$this->security->checkSecurity($tags, $filters, $functions);
|
||||
}
|
||||
|
||||
public function checkMethodAllowed($obj, $method): void
|
||||
{
|
||||
if ($obj instanceof Template || $obj instanceof Markup || $obj instanceof UnicodeString) { // @phpstan-ignore instanceof.internalClass
|
||||
return;
|
||||
}
|
||||
|
||||
$this->default->checkMethodAllowed($obj, $method);
|
||||
|
||||
$lm = strtolower($method);
|
||||
|
||||
if (str_starts_with($lm, 'get') || str_starts_with($lm, 'is') || str_starts_with($lm, 'has') || $lm === '__tostring') {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->security->checkMethodAllowed($obj, $method);
|
||||
}
|
||||
|
||||
public function checkPropertyAllowed($obj, $property): void
|
||||
{
|
||||
$this->default->checkPropertyAllowed($obj, $property);
|
||||
$this->security->checkPropertyAllowed($obj, $property);
|
||||
}
|
||||
}
|
||||
165
src/Twig/SecurityPolicy/StrictPolicy.php
Normal file
165
src/Twig/SecurityPolicy/StrictPolicy.php
Normal file
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Twig\SecurityPolicy;
|
||||
|
||||
use App\Entity\MetaTableTypeInterface;
|
||||
use App\Entity\User;
|
||||
use App\Pdf\PdfContext;
|
||||
use Symfony\Bridge\Twig\AppVariable;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\ServerBag;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
use Symfony\Component\String\UnicodeString;
|
||||
use Twig\Sandbox\SecurityNotAllowedFilterError;
|
||||
use Twig\Sandbox\SecurityNotAllowedFunctionError;
|
||||
use Twig\Sandbox\SecurityNotAllowedMethodError;
|
||||
use Twig\Sandbox\SecurityNotAllowedTagError;
|
||||
use Twig\Sandbox\SecurityPolicyInterface;
|
||||
|
||||
/**
|
||||
* The Twig environment needs the sandbox extension, which itself needs a policy to start working.
|
||||
*/
|
||||
final class StrictPolicy implements SecurityPolicyInterface
|
||||
{
|
||||
/** @var string[] */
|
||||
private array $allowedTags = ['block', 'if', 'for', 'set', 'macro', 'import', 'extends', 'from'];
|
||||
/** @var string[] */
|
||||
private array $allowedFunctions = [
|
||||
// vendor/twig/twig/src/Extension/CoreExtension.php
|
||||
'max', 'min', 'range', 'constant', 'cycle', 'random', 'date',
|
||||
// vendor/symfony/twig-bridge/Extension/TranslationExtension.php
|
||||
't',
|
||||
// vendor/symfony/webpack-encore-bundle/src/Twig/EntryFilesTwigExtension.php
|
||||
'encore_entry_css_source', 'encore_entry_link_tags', 'encore_entry_script_tags',
|
||||
// vendor/symfony/twig-bridge/Extension/SecurityExtension.php
|
||||
'is_granted',
|
||||
// Twig/RuntimeExtensions.php
|
||||
'qr_code_data_uri',
|
||||
// Twig/Configuration.php
|
||||
'config',
|
||||
// Twig/LocaleFormatExtensions.php
|
||||
'create_date', 'month_names', 'locale_format',
|
||||
// Twig/Extensions.php
|
||||
'class_name'
|
||||
];
|
||||
/** @var string[] */
|
||||
private array $allowedFilters = [
|
||||
// vendor/twig/twig/src/Extension/CoreExtension.php
|
||||
// formatting filters
|
||||
'date', 'date_modify', 'format', 'replace', 'number_format', 'abs', 'round',
|
||||
// encoding
|
||||
'url_encode', 'json_encode',
|
||||
// string filters
|
||||
'title', 'capitalize', 'upper', 'lower', 'striptags', 'trim', 'nl2br', 'spaceless',
|
||||
// array helpers
|
||||
'join', 'split', 'sort', 'merge', 'column', 'filter', 'map',
|
||||
// string/array filters
|
||||
'reverse', 'length', 'slice', 'first', 'last',
|
||||
// iteration and runtime
|
||||
'default', 'keys',
|
||||
// vendor/twig/twig/src/Extension/EscaperExtension.php
|
||||
'escape', 'e', 'raw',
|
||||
// vendor/symfony/twig-bridge/Extension/TranslationExtension.php
|
||||
'trans',
|
||||
// vendor/twig/string-extra/StringExtension.php
|
||||
'u', 'slug',
|
||||
// vendor/twig/intl-extra/IntlExtension.php
|
||||
'country_name', 'currency_name', 'currency_symbol', 'language_name', 'locale_name', 'timezone_name',
|
||||
'format_currency', 'format_number', 'format_decimal_number', 'format_currency_number',
|
||||
'format_duration_number', 'format_datetime', 'format_date', 'format_time',
|
||||
// src/Twig/LocaleFormatExtensions.php
|
||||
'month_name', 'day_name', 'date_short', 'date_time', 'date_full', 'date_format',
|
||||
'date_weekday', 'time', 'duration', 'duration_decimal', 'money', 'amount',
|
||||
// src/Twig/RuntimeExtensions.php
|
||||
'md2html', 'desc2html', 'comment2html', 'comment1line',
|
||||
// src/Twig/Extensions.php
|
||||
'multiline_indent', 'color', 'nl2str'
|
||||
];
|
||||
|
||||
public function checkSecurity($tags, $filters, $functions): void
|
||||
{
|
||||
foreach ($tags as $tag) {
|
||||
if (!\in_array($tag, $this->allowedTags, true)) {
|
||||
throw new SecurityNotAllowedTagError(\sprintf('Tag "%s" is not allowed.', $tag), $tag);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($filters as $filter) {
|
||||
if (!\in_array($filter, $this->allowedFilters, true)) {
|
||||
throw new SecurityNotAllowedFilterError(\sprintf('Filter "%s" is not allowed.', $filter), $filter);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($functions as $function) {
|
||||
if (!\in_array($function, $this->allowedFunctions, true)) {
|
||||
throw new SecurityNotAllowedFunctionError(\sprintf('Function "%s" is not allowed.', $function), $function);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function checkMethodAllowed($obj, $method): void
|
||||
{
|
||||
if ($obj instanceof UnicodeString) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($obj instanceof ServerBag) {
|
||||
throw new SecurityNotAllowedMethodError('Tried to access server environment', ServerBag::class, $method);
|
||||
}
|
||||
|
||||
if ($obj instanceof SessionInterface) {
|
||||
throw new SecurityNotAllowedMethodError('Tried to access session', SessionInterface::class, $method);
|
||||
}
|
||||
|
||||
$lcm = strtolower($method);
|
||||
|
||||
if ($obj instanceof PdfContext) {
|
||||
if ($lcm !== 'setoption') {
|
||||
throw new SecurityNotAllowedMethodError('Tried to access forbidden method on PdfContext', PdfContext::class, $method);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($obj instanceof MetaTableTypeInterface && $lcm === 'merge') {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($obj instanceof Request) {
|
||||
if (!str_starts_with($lcm, 'get')) {
|
||||
throw new SecurityNotAllowedMethodError('Tried to call setter() of app variable', AppVariable::class, $method);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($obj instanceof AppVariable) {
|
||||
if (!\in_array($lcm, ['getrequest', 'getuser', 'getlocale'], true)) {
|
||||
throw new SecurityNotAllowedMethodError('Tried to access forbidden app variable method', User::class, $method);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!str_starts_with($lcm, 'get') && !str_starts_with($lcm, 'has') && !str_starts_with($lcm, 'is') && $lcm !== '__tostring') {
|
||||
throw new SecurityNotAllowedMethodError('Tried to access non-read method', $obj::class, $method);
|
||||
}
|
||||
|
||||
if ($obj instanceof User) {
|
||||
if (\in_array($lcm, ['getpassword', 'gettotpsecret', 'getplainpassword', 'getconfirmationtoken', 'gettotpauthenticationconfiguration'], true)) {
|
||||
throw new SecurityNotAllowedMethodError('Tried to access user secrets', User::class, $method);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function checkPropertyAllowed($obj, $property): void
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -21,10 +21,7 @@ final class ReleaseVersion
|
||||
/**
|
||||
* Get all releases from GitHub.
|
||||
*
|
||||
* @throws \Exception
|
||||
* @return array|null
|
||||
* @return array<string, array{'version': string, 'date': \DateTimeInterface, 'url': string, 'download': string, 'content': string}>
|
||||
* @return array
|
||||
* @return array<string, array{'version': non-empty-string, 'date': \DateTimeInterface, 'url': non-empty-string, 'download': non-empty-string, 'content': string}>
|
||||
*/
|
||||
private function getReleasesFromGithub(): array
|
||||
{
|
||||
@@ -41,19 +38,24 @@ final class ReleaseVersion
|
||||
$context = stream_context_create($opts);
|
||||
|
||||
$releases = file_get_contents('https://api.github.com/repos/' . Constants::GITHUB_REPO . '/releases', false, $context);
|
||||
$releases = json_decode($releases);
|
||||
|
||||
if (!isset($releases[0])) {
|
||||
throw new \Exception('API error - no release found at GitHub repository: ' . Constants::GITHUB_REPO);
|
||||
if ($releases === false) {
|
||||
throw new \Exception('Could not load releases from GitHub repository: ' . Constants::GITHUB_REPO);
|
||||
}
|
||||
/** @var array<string, array{url: non-empty-string, html_url: non-empty-string, tag_name: non-empty-string, name: non-empty-string, draft: bool, immutable: bool, prerelease: bool, created_at: non-empty-string, updated_at: non-empty-string, published_at: non-empty-string, zipball_url: non-empty-string, body: string}> $releases */
|
||||
$releases = json_decode($releases, true);
|
||||
|
||||
if ($releases === false) {
|
||||
throw new \Exception('Failed parsing release found at GitHub repository: ' . Constants::GITHUB_REPO);
|
||||
}
|
||||
|
||||
$parsed = [];
|
||||
foreach ($releases as $release) {
|
||||
if ($release->draft || $release->prerelease) {
|
||||
if ($release['draft'] || $release['prerelease']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$normalized = $versionParser->normalize($release->tag_name);
|
||||
$normalized = $versionParser->normalize($release['tag_name']);
|
||||
} catch (\UnexpectedValueException $e) {
|
||||
continue;
|
||||
}
|
||||
@@ -62,19 +64,19 @@ final class ReleaseVersion
|
||||
continue;
|
||||
}
|
||||
|
||||
$date = $release->published_at;
|
||||
$date = $release['published_at'];
|
||||
try {
|
||||
$date = new \DateTimeImmutable($date);
|
||||
} catch (\Exception $ex) {
|
||||
// can be ignored, we return a string
|
||||
continue;
|
||||
}
|
||||
|
||||
$parsed[$normalized] = [
|
||||
'version' => $release->tag_name,
|
||||
'version' => $release['tag_name'],
|
||||
'date' => $date,
|
||||
'url' => $release->html_url,
|
||||
'download' => $release->zipball_url,
|
||||
'content' => $release->body,
|
||||
'url' => $release['html_url'],
|
||||
'download' => $release['zipball_url'],
|
||||
'content' => $release['body'],
|
||||
];
|
||||
}
|
||||
$versions = Semver::rsort(array_keys($parsed));
|
||||
|
||||
@@ -261,12 +261,6 @@
|
||||
"psr/event-dispatcher": {
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"psr/http-client": {
|
||||
"version": "1.0.1"
|
||||
},
|
||||
"psr/http-factory": {
|
||||
"version": "1.0.1"
|
||||
},
|
||||
"psr/http-message": {
|
||||
"version": "1.0.1"
|
||||
},
|
||||
|
||||
@@ -26,19 +26,21 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block page_content_start %}
|
||||
{% if app.session and app.session.started and app.session.flashbag.peekAll|length > 0 %}
|
||||
{% set flashes = app.flashes %}
|
||||
{% if flashes|length > 0 %}
|
||||
{% set domain = 'flashmessages' %}
|
||||
<script type="text/javascript">
|
||||
document.addEventListener('kimai.initialized', function(options) {
|
||||
const ALERT = options.detail.kimai.getPlugin('alert');
|
||||
{% for type, messages in app.session.flashbag.all %}
|
||||
{% for type, messages in flashes %}
|
||||
{% for message in messages %}
|
||||
{% if type == 'error' %}
|
||||
ALERT.error('{{ message|trans({}, domain)|e('js') }}');
|
||||
{% elseif type == 'warning' %}
|
||||
ALERT.warning('{{ message|trans({}, domain)|e('js') }}');
|
||||
{% elseif type == 'success' %}
|
||||
ALERT.success('{{ message|trans({}, domain)|e('js') }}');
|
||||
// success is expected so we do not need to show messages like "saved successfully"
|
||||
// ALERT.success('{{ message|trans({}, domain)|e('js') }}');
|
||||
{% else %}
|
||||
ALERT.info('{{ message|trans({}, domain)|e('js') }}');
|
||||
{% endif %}
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
<td class="actions">
|
||||
<div class="btn-group">
|
||||
<a href="{{ path(edit_route, {'id': entity.id, 'rate': rate.id}) }}" class="modal-ajax-form btn btn-icon">{{ icon('edit', true) }}</a>
|
||||
<a href="{{ path(delete_route, {'id': entity.id, 'rateId': rate.id}) }}" class="btn btn-icon api-link" data-question="confirm.delete" data-event="kimai.rateUpdate kimai.rateDelete" data-method="DELETE" data-msg-error="action.delete.error" data-msg-success="action.delete.success">{{ icon('delete', true) }}</a>
|
||||
<a href="{{ path(delete_route, {'id': entity.id, 'rateId': rate.id}) }}" class="btn btn-icon api-link" data-question="confirm.delete" data-event="kimai.rateUpdate kimai.rateDelete" data-method="DELETE" data-msg-error="action.delete.error">{{ icon('delete', true) }}</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -158,7 +158,7 @@
|
||||
{{ btnTitle }}
|
||||
{% if allowCreate and cfg.internal %}
|
||||
<span class="dropdown-action modal-ajax-form open-edit ps-2 ms-auto" data-href="{{ path('export_template_edit', {exportTemplate: id}) }}">{{ icon('edit') }}</span>
|
||||
<span class="dropdown-action api-link text-red dd-ts-trash text-danger ms-2" href="{{ path('delete_export_template', {id: id}) }}" data-event="kimai.exportTemplate" data-method="DELETE" data-question="confirm.delete" data-msg-error="action.delete.error" data-msg-success="action.delete.success">{{ icon('delete') }}</span>
|
||||
<span class="dropdown-action api-link text-red dd-ts-trash text-danger ms-2" href="{{ path('delete_export_template', {id: id}) }}" data-event="kimai.exportTemplate" data-method="DELETE" data-question="confirm.delete" data-msg-error="action.delete.error">{{ icon('delete') }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% endfor %}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ app.request.locale }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport">
|
||||
<link rel="stylesheet" href="{{ asset('bundles/tabler/tabler.css') }}">
|
||||
{{ encore_entry_link_tags('app') }}
|
||||
{{ encore_entry_script_tags('app') }}
|
||||
{% block styles %}{% endblock %}
|
||||
{% if block('document_title') is defined %}
|
||||
<title>{{ block('document_title') }}</title>
|
||||
{% endif %}
|
||||
</head>
|
||||
<body class="pad export_print">
|
||||
<div class="wrapper">
|
||||
<section class="export">
|
||||
{% block export %}{% endblock %}
|
||||
</section>
|
||||
</div>
|
||||
{% block javascripts %}
|
||||
<script src="{{ asset('bundles/tabler/tabler.js') }}"></script>
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
42
templates/macros/meta_fields.html.twig
Normal file
42
templates/macros/meta_fields.html.twig
Normal file
@@ -0,0 +1,42 @@
|
||||
|
||||
{% macro meta_field_value(entity, field) %}
|
||||
{% set metaField = entity.metaField(field.name) %}
|
||||
{% if not metaField is null %}
|
||||
{% set metaField = metaField.merge(field) %}
|
||||
{% set type = metaField.type %}
|
||||
{% set value = metaField.value %}
|
||||
{% if 'DurationType' in type %}
|
||||
{{ value|duration }}
|
||||
{% elseif 'YesNoType' in type or 'CheckboxType' in type %}
|
||||
{{ value ? ('yes'|trans) : ('no'|trans) }}
|
||||
{% elseif 'DatePickerType' in type %}
|
||||
{{ value|date_short }}
|
||||
{% elseif 'DateTimePickerType' in type %}
|
||||
{{ value|date_time }}
|
||||
{% elseif 'CountryType' in type %}
|
||||
{{ value|country_name }}
|
||||
{% elseif 'CurrencyType' in type %}
|
||||
{{ value|currency_name }}
|
||||
{% elseif 'LanguageType' in type %}
|
||||
{{ value|locale_name }}
|
||||
{% elseif 'MoneyType' in type %}
|
||||
{% set classname = class_name(entity) %}
|
||||
{% if classname == 'App\\Entity\\Timesheet' %}
|
||||
{{ value|money(entity.project.customer.currency) }}
|
||||
{% elseif classname == 'App\\Entity\\Customer' %}
|
||||
{{ value|money(entity.currency) }}
|
||||
{% elseif classname == 'App\\Entity\\Project' %}
|
||||
{{ value|money(entity.customer.currency) }}
|
||||
{% elseif classname == 'App\\Entity\\Activity' and entity.project is not null %}
|
||||
{{ value|money(entity.project.customer.currency) }}
|
||||
{% else %}
|
||||
{{ value }}
|
||||
{% endif %}
|
||||
{% elseif 'TextareaType' in type %}
|
||||
{{ value|nl2br }}
|
||||
{% else %}
|
||||
{# EmailType, UrlType, TagsType, ColorPickerType, ColorChoiceType #}
|
||||
{{ value }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
@@ -314,9 +314,7 @@
|
||||
{% macro form_type_value(type, value, entity) %}
|
||||
{% if 'ColorPickerType' in type or 'ColorChoiceType' in type %}
|
||||
{% if value is not empty %}
|
||||
<span class="label-color" data-toggle="tooltip" data-placement="top" title="{{ value }}">
|
||||
<i class="dot {{ 'dot'|icon(true) }} fa-fw" style="color:{{ value }}"></i>
|
||||
</span>
|
||||
{{ _self.label_color_dot('color', true, value, null, value) }}
|
||||
{% endif %}
|
||||
{% elseif 'DurationType' in type %}
|
||||
{{ value|duration }}
|
||||
@@ -340,7 +338,7 @@
|
||||
{% endfor %}
|
||||
{% elseif 'MoneyType' in type %}
|
||||
{% set classname = class_name(entity) %}
|
||||
{% if entity is null %}
|
||||
{% if classname is null %}
|
||||
{{ value }}
|
||||
{% elseif classname == 'App\\Entity\\Timesheet' %}
|
||||
{{ value|money(entity.project.customer.currency) }}
|
||||
|
||||
@@ -20,16 +20,14 @@
|
||||
|
||||
{% block main %}
|
||||
{% if form is defined %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
{% block report_form %}
|
||||
{{ form_start(form, {'attr': {'class': 'form-reporting', 'id': 'report-form'}}) }}
|
||||
<div class="{% block report_form_container_class %}btn-list w-100{% endblock %}">
|
||||
{% block report_form_layout %}{{ form_rest(form) }}{% endblock %}
|
||||
</div>
|
||||
{{ form_end(form) }}
|
||||
{% endblock %}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
{% block report_form %}
|
||||
{{ form_start(form, {'attr': {'class': 'form-reporting', 'id': 'report-form'}}) }}
|
||||
<div class="{% block report_form_container_class %}btn-list w-100{% endblock %}">
|
||||
{% block report_form_layout %}{{ form_rest(form) }}{% endblock %}
|
||||
</div>
|
||||
{{ form_end(form) }}
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -42,27 +42,34 @@
|
||||
document.addEventListener('kimai.initialized', (event) => {
|
||||
/** @type {KimaiAPI} API */
|
||||
const API = event.detail.kimai.getPlugin('api');
|
||||
/** @type {KimaiAlert} ALERT */
|
||||
const ALERT = event.detail.kimai.getPlugin('alert');
|
||||
|
||||
const successHandle = (json) => {
|
||||
document.dispatchEvent(new CustomEvent('kimai.reloadedContent'));
|
||||
};
|
||||
const errorHandle = (error) => {
|
||||
document.dispatchEvent(new CustomEvent('kimai.reloadedContent'));
|
||||
API.handleError('action.update.error', error);
|
||||
};
|
||||
|
||||
{% if customerForm is defined and customerForm is not null %}
|
||||
document.getElementById('team_customer_form').addEventListener('change',
|
||||
(event) => {
|
||||
if (event.target.matches('input[type=checkbox]')) {
|
||||
document.dispatchEvent(new CustomEvent('kimai.reloadContent'));
|
||||
const checkbox = event.target;
|
||||
const customerId = checkbox.value;
|
||||
if (checkbox.checked) {
|
||||
API.post(
|
||||
'{{ path('post_team_customer', {'id': team.id, 'customerId': '000'}) }}'.replace(/000/, customerId),
|
||||
{},
|
||||
(json) => {ALERT.success('action.update.success');},
|
||||
(error) => {ALERT.error('action.update.error', error);}, // TODO test me
|
||||
successHandle,
|
||||
errorHandle,
|
||||
);
|
||||
} else {
|
||||
API.delete(
|
||||
'{{ path('delete_team_customer', {'id': team.id, 'customerId': '000'}) }}'.replace(/000/, customerId),
|
||||
(json) => {ALERT.success('action.delete.success');},
|
||||
(error) => {ALERT.error('action.delete.error', error);}, // TODO test me
|
||||
successHandle,
|
||||
errorHandle,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -74,20 +81,21 @@
|
||||
document.getElementById('team_project_form').addEventListener('change',
|
||||
(event) => {
|
||||
if (event.target.matches('input[type=checkbox]')) {
|
||||
document.dispatchEvent(new CustomEvent('kimai.reloadContent'));
|
||||
const checkbox = event.target;
|
||||
const projectId = checkbox.value;
|
||||
if (checkbox.checked) {
|
||||
API.post(
|
||||
'{{ path('post_team_project', {'id': team.id, 'projectId': '000'}) }}'.replace(/000/, projectId),
|
||||
{},
|
||||
(json) => {ALERT.success('action.update.success');},
|
||||
(error) => {ALERT.error('action.update.error', error);}, // TODO test me
|
||||
successHandle,
|
||||
errorHandle,
|
||||
);
|
||||
} else {
|
||||
API.delete(
|
||||
'{{ path('delete_team_project', {'id': team.id, 'projectId': '000'}) }}'.replace(/000/, projectId),
|
||||
(json) => {ALERT.success('action.delete.success');},
|
||||
(error) => {ALERT.error('action.delete.error', error);}, // TODO test me
|
||||
successHandle,
|
||||
errorHandle,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
{% import "macros/datatables.html.twig" as tables %}
|
||||
{% set language = app.request is not null ? app.request.locale : 'en' %}
|
||||
{% set decimal = false %}
|
||||
{% set showUserColumn = true %}
|
||||
{% if query.user %}
|
||||
{% set showUserColumn = false %}
|
||||
{% endif %}
|
||||
<html lang="{{ language }}">
|
||||
<html lang="{{ app.locale }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
|
||||
@@ -79,10 +76,11 @@
|
||||
{{ 'customer'|trans }}: {{ entry.project.customer.name }}
|
||||
</span>
|
||||
</td>
|
||||
{% from "macros/meta_fields.html.twig" import meta_field_value %}
|
||||
{% for field in timesheetMetaFields %}
|
||||
<td>{{ tables.datatable_meta_column(entry, field) }}</td>
|
||||
<td>{{ meta_field_value(entry, field) }}</td>
|
||||
{% endfor %}
|
||||
<td class="text-nowrap">{{ entry.duration|duration(decimal) }}</td>
|
||||
<td class="text-nowrap">{{ entry.duration|duration }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@@ -96,7 +94,7 @@
|
||||
<th></th>
|
||||
{% endfor %}
|
||||
<th>{{ 'invoice.total_working_time'|trans }}</th>
|
||||
<th class="text-nowrap">{{ timeWorked|duration(decimal) }}</th>
|
||||
<th class="text-nowrap">{{ timeWorked|duration }}</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
@@ -69,7 +69,6 @@
|
||||
'data-method': 'DELETE',
|
||||
'data-question': 'confirm.delete',
|
||||
'data-msg-error': 'action.delete.error',
|
||||
'data-msg-success': 'action.delete.success'
|
||||
}}) }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -55,7 +55,7 @@ abstract class AbstractUserPeriodControllerTestCase extends AbstractControllerBa
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
|
||||
$this->importReportingFixture(User::ROLE_SUPER_ADMIN);
|
||||
$this->assertAccessIsGranted($client, \sprintf('%s?user=%s&date=12999119191&sumType=%s', $this->getReportUrl(), $user, $dataType));
|
||||
self::assertStringContainsString(\sprintf('<div class="card-body %s', $this->getBoxId()), $client->getResponse()->getContent());
|
||||
self::assertStringContainsString(\sprintf('<div class="card-body p-0 %s', $this->getBoxId()), $client->getResponse()->getContent());
|
||||
$option = $client->getCrawler()->filterXPath("//select[@id='user']/option[@selected]");
|
||||
self::assertEquals($user, $option->attr('value'));
|
||||
$cell = $client->getCrawler()->filterXPath("//th[contains(@class, 'reportDataTypeTitle')]");
|
||||
@@ -90,7 +90,7 @@ abstract class AbstractUserPeriodControllerTestCase extends AbstractControllerBa
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||
$this->importReportingFixture(User::ROLE_USER);
|
||||
$this->assertAccessIsGranted($client, \sprintf('%s?date=12999119191', $this->getReportUrl()));
|
||||
self::assertStringContainsString(\sprintf('<div class="card-body %s', $this->getBoxId()), $client->getResponse()->getContent());
|
||||
self::assertStringContainsString(\sprintf('<div class="card-body p-0 %s', $this->getBoxId()), $client->getResponse()->getContent());
|
||||
$select = $client->getCrawler()->filterXPath("//select[@id='user']");
|
||||
self::assertEquals(0, $select->count());
|
||||
$cell = $client->getCrawler()->filterXPath("//th[contains(@class, 'reportDataTypeTitle')]");
|
||||
|
||||
@@ -55,7 +55,7 @@ abstract class AbstractUsersPeriodControllerTestCase extends AbstractControllerB
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
|
||||
$this->importReportingFixture(User::ROLE_SUPER_ADMIN);
|
||||
$this->assertAccessIsGranted($client, \sprintf('%s?date=12999119191&sumType=%s', $this->getReportUrl(), $dataType));
|
||||
self::assertStringContainsString(\sprintf('<div class="card-body %s', $this->getBoxId()), $client->getResponse()->getContent());
|
||||
self::assertStringContainsString(\sprintf('<div class="card-body p-0 %s', $this->getBoxId()), $client->getResponse()->getContent());
|
||||
$cell = $client->getCrawler()->filterXPath("//th[contains(@class, 'reportDataTypeTitle')]");
|
||||
self::assertEquals($title, $cell->text());
|
||||
}
|
||||
@@ -66,7 +66,7 @@ abstract class AbstractUsersPeriodControllerTestCase extends AbstractControllerB
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||
$this->importReportingFixture(User::ROLE_TEAMLEAD);
|
||||
$this->assertAccessIsGranted($client, \sprintf('%s?date=12999119191&sumType=%s', $this->getReportUrl(), $dataType));
|
||||
self::assertStringContainsString(\sprintf('<div class="card-body %s', $this->getBoxId()), $client->getResponse()->getContent());
|
||||
self::assertStringContainsString(\sprintf('<div class="card-body p-0 %s', $this->getBoxId()), $client->getResponse()->getContent());
|
||||
$select = $client->getCrawler()->filterXPath("//select[@id='user']");
|
||||
self::assertEquals(0, $select->count());
|
||||
$cell = $client->getCrawler()->filterXPath("//th[contains(@class, 'reportDataTypeTitle')]");
|
||||
|
||||
@@ -294,9 +294,6 @@ class UserTest extends TestCase
|
||||
|
||||
self::assertInstanceOf(UserPreference::class, $prefs[2]);
|
||||
self::assertEquals('export_decimal', $prefs[2]->getName());
|
||||
|
||||
$user->setPreferences(new ArrayCollection([]));
|
||||
self::assertCount(0, $user->getPreferences());
|
||||
}
|
||||
|
||||
public function testDisplayName(): void
|
||||
|
||||
@@ -26,6 +26,8 @@ use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
use Twig\Environment;
|
||||
use Twig\Loader\FilesystemLoader;
|
||||
|
||||
#[CoversClass(ServiceExport::class)]
|
||||
#[CoversClass(CsvRenderer::class)]
|
||||
@@ -35,7 +37,7 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
#[Group('integration')]
|
||||
class DefaultRendererTest extends AbstractRendererTestCase
|
||||
{
|
||||
private function createServiceExport(): ServiceExport
|
||||
private function createServiceExport(?Environment $environment = null): ServiceExport
|
||||
{
|
||||
$repository = $this->createMock(ExportTemplateRepository::class);
|
||||
$repository->expects($this->once())->method('findAll')->willReturn([]);
|
||||
@@ -43,8 +45,8 @@ class DefaultRendererTest extends AbstractRendererTestCase
|
||||
|
||||
return new ServiceExport(
|
||||
$this->createMock(EventDispatcherInterface::class),
|
||||
(new HtmlRendererFactoryMock($this))->create(),
|
||||
(new PdfRendererFactoryMock($this))->create(),
|
||||
(new HtmlRendererFactoryMock($this))->create($environment),
|
||||
(new PdfRendererFactoryMock($this))->create($environment),
|
||||
(new CsvRendererFactoryMock($this))->create(),
|
||||
(new XlsxRendererFactoryMock($this))->create(),
|
||||
$repository,
|
||||
@@ -54,7 +56,10 @@ class DefaultRendererTest extends AbstractRendererTestCase
|
||||
|
||||
public function testRenderDefaultTemplates(): void
|
||||
{
|
||||
$sut = $this->createServiceExport();
|
||||
/** @var Environment $twig */
|
||||
$twig = $this->getContainer()->get(Environment::class);
|
||||
|
||||
$sut = $this->createServiceExport($twig);
|
||||
|
||||
$renderer = $sut->getRenderer();
|
||||
self::assertCount(4, $renderer);
|
||||
@@ -98,12 +103,21 @@ class DefaultRendererTest extends AbstractRendererTestCase
|
||||
->files()
|
||||
;
|
||||
|
||||
/** @var Environment $twig */
|
||||
$twig = $this->getContainer()->get(Environment::class);
|
||||
|
||||
/** @var FilesystemLoader $loader */
|
||||
$loader = $twig->getLoader();
|
||||
|
||||
$files = [];
|
||||
$dirs = [];
|
||||
foreach ($finder->getIterator() as $filename => $splFile) {
|
||||
$files[] = $splFile->getRealPath();
|
||||
$dir = \dirname($splFile->getRealPath());
|
||||
$dirs[$dir] = $dir;
|
||||
if (!\array_key_exists($dir, $dirs)) {
|
||||
$dirs[$dir] = $dir;
|
||||
$loader->addPath($dir . '/', 'export');
|
||||
}
|
||||
}
|
||||
$dirs = array_keys($dirs);
|
||||
|
||||
@@ -113,7 +127,7 @@ class DefaultRendererTest extends AbstractRendererTestCase
|
||||
return;
|
||||
}
|
||||
|
||||
$sut = $this->createServiceExport();
|
||||
$sut = $this->createServiceExport($twig);
|
||||
foreach ($dirs as $dir) {
|
||||
$sut->addDirectory($dir);
|
||||
}
|
||||
|
||||
@@ -25,10 +25,10 @@ use Twig\Environment;
|
||||
#[Group('integration')]
|
||||
class HtmlRendererTest extends AbstractRendererTestCase
|
||||
{
|
||||
protected function getAbstractRenderer(): HtmlRenderer
|
||||
protected function getAbstractRenderer(?Environment $environment = null): HtmlRenderer
|
||||
{
|
||||
return new HtmlRenderer(
|
||||
$this->createMock(Environment::class),
|
||||
$environment ?? $this->createMock(Environment::class),
|
||||
$this->createMock(EventDispatcherInterface::class),
|
||||
$this->createMock(ProjectStatisticService::class),
|
||||
$this->createMock(ActivityStatisticService::class),
|
||||
@@ -60,7 +60,10 @@ class HtmlRendererTest extends AbstractRendererTestCase
|
||||
|
||||
public function testRender(): void
|
||||
{
|
||||
$sut = $this->getAbstractRenderer();
|
||||
/** @var Environment $twig */
|
||||
$twig = $this->getContainer()->get(Environment::class);
|
||||
|
||||
$sut = $this->getAbstractRenderer($twig);
|
||||
|
||||
$response = $this->render($sut);
|
||||
self::assertInstanceOf(Response::class, $response);
|
||||
|
||||
@@ -26,14 +26,13 @@ use Twig\Environment;
|
||||
#[Group('integration')]
|
||||
class PdfRendererTest extends AbstractRendererTestCase
|
||||
{
|
||||
protected function getAbstractRenderer(bool $exportDecimal = false): PDFRenderer
|
||||
protected function getAbstractRenderer(?Environment $environment = null): PDFRenderer
|
||||
{
|
||||
$twig = $this->createMock(Environment::class);
|
||||
$converter = $this->createMock(HtmlToPdfConverter::class);
|
||||
$projectStatisticService = $this->createMock(ProjectStatisticService::class);
|
||||
|
||||
return new PDFRenderer(
|
||||
$twig,
|
||||
$environment ?? $this->createMock(Environment::class),
|
||||
$converter,
|
||||
$projectStatisticService,
|
||||
'foo',
|
||||
@@ -70,7 +69,10 @@ class PdfRendererTest extends AbstractRendererTestCase
|
||||
|
||||
public function testRender(): void
|
||||
{
|
||||
$sut = $this->getAbstractRenderer();
|
||||
/** @var Environment $twig */
|
||||
$twig = $this->getContainer()->get(Environment::class);
|
||||
|
||||
$sut = $this->getAbstractRenderer($twig);
|
||||
|
||||
$response = $this->render($sut);
|
||||
self::assertInstanceOf(Response::class, $response);
|
||||
|
||||
@@ -131,6 +131,7 @@ abstract class AbstractRendererTestCase extends KernelTestCase
|
||||
|
||||
$currentUser = new User();
|
||||
$currentUser->setPreferenceValue('export_decimal', $exportDecimal);
|
||||
$currentUser->setUserIdentifier('foo-bar');
|
||||
|
||||
$query = new TimesheetQuery();
|
||||
$query->setActivities([$activity]);
|
||||
|
||||
@@ -59,7 +59,7 @@ class PdfRendererTest extends KernelTestCase
|
||||
|
||||
/** @var FilesystemLoader $loader */
|
||||
$loader = $twig->getLoader();
|
||||
$loader->addPath(__DIR__ . '/../templates/', 'invoice');
|
||||
$loader->addPath($this->getInvoiceTemplatePath(), 'invoice');
|
||||
|
||||
$sut = new PdfRenderer($twig, new MPdfConverter((new FileHelperFactory($this))->create(), $cacheDir));
|
||||
$model = $this->getInvoiceModel();
|
||||
@@ -115,46 +115,85 @@ class PdfRendererTest extends KernelTestCase
|
||||
|
||||
/** @var FilesystemLoader $loader */
|
||||
$loader = $twig->getLoader();
|
||||
$loader->addPath(__DIR__ . '/../templates/', 'invoice');
|
||||
|
||||
$dirs = [
|
||||
__DIR__ . '/../../../templates/invoice/renderer/',
|
||||
];
|
||||
|
||||
$files = [];
|
||||
|
||||
$additionalTemplatesDir = __DIR__ . '/../../../var/templates';
|
||||
if (is_dir($additionalTemplatesDir)) {
|
||||
$dirs = [
|
||||
realpath($this->getInvoiceTemplatePath()),
|
||||
realpath(__DIR__ . '/../templates/'),
|
||||
realpath(__DIR__ . '/../../../var/invoices/'),
|
||||
];
|
||||
|
||||
foreach ($dirs as $dir) {
|
||||
if ($dir === false || !is_dir($dir)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$finder = new Finder();
|
||||
$finder
|
||||
->in($additionalTemplatesDir)
|
||||
->in($dir)
|
||||
->name('*.pdf.twig')
|
||||
->path('invoice-tpl/')
|
||||
->sortByName()
|
||||
->files();
|
||||
|
||||
foreach ($finder->getIterator() as $splFile) {
|
||||
$filename = $splFile->getRealPath();
|
||||
$files[] = $filename;
|
||||
$loader->addPath(\dirname($filename) . '/', 'invoice');
|
||||
if ($filename === false) {
|
||||
continue;
|
||||
}
|
||||
$dir = \dirname($filename) . '/';
|
||||
if (!\array_key_exists($dir, $files)) {
|
||||
$loader->addPath($dir . '/', 'invoice');
|
||||
}
|
||||
$files[$dir][] = $filename;
|
||||
}
|
||||
}
|
||||
|
||||
// search for custom templates, that shall not be shipped
|
||||
$dirs = [
|
||||
realpath(__DIR__ . '/../../../var/templates/'),
|
||||
];
|
||||
|
||||
foreach ($dirs as $dir) {
|
||||
if (!is_dir($dir)) {
|
||||
if ($dir === false || !is_dir($dir)) {
|
||||
continue;
|
||||
}
|
||||
$dir = realpath($dir);
|
||||
$loader->addPath($dir . '/', 'invoice');
|
||||
$found = glob($dir . '/*.pdf.twig');
|
||||
if ($found !== false) {
|
||||
$files = array_merge($files, $found);
|
||||
|
||||
$finder = new Finder();
|
||||
$finder
|
||||
->in($dir)
|
||||
->name('*.pdf.twig')
|
||||
->path('invoice-tpl/')
|
||||
->sortByName()
|
||||
->files();
|
||||
|
||||
foreach ($finder->getIterator() as $splFile) {
|
||||
$filename = $splFile->getRealPath();
|
||||
if ($filename === false) {
|
||||
continue;
|
||||
}
|
||||
$dir = \dirname($filename) . '/';
|
||||
if (!\array_key_exists($dir, $files)) {
|
||||
$loader->addPath($dir . '/', 'invoice');
|
||||
}
|
||||
$files[$dir][] = $filename;
|
||||
}
|
||||
}
|
||||
|
||||
$sut = new PdfRenderer($twig, new MPdfConverter((new FileHelperFactory($this))->create(), $cacheDir));
|
||||
$model = $this->getInvoiceModel();
|
||||
|
||||
foreach ($files as $filename) {
|
||||
$allFiles = [];
|
||||
|
||||
foreach ($files as $templates) {
|
||||
foreach ($templates as $filename) {
|
||||
$allFiles[] = $filename;
|
||||
}
|
||||
}
|
||||
|
||||
self::assertGreaterThanOrEqual(3, \count($allFiles));
|
||||
|
||||
foreach ($allFiles as $filename) {
|
||||
$document = new InvoiceDocument(new \SplFileInfo($filename));
|
||||
|
||||
$response = $sut->render($document, $model);
|
||||
|
||||
@@ -15,6 +15,7 @@ use App\Model\InvoiceDocument;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Twig\Environment;
|
||||
@@ -93,34 +94,85 @@ class TwigRendererTest extends KernelTestCase
|
||||
|
||||
/** @var FilesystemLoader $loader */
|
||||
$loader = $twig->getLoader();
|
||||
$loader->addPath($this->getInvoiceTemplatePath(), 'invoice');
|
||||
|
||||
$dirs = [
|
||||
__DIR__ . '/../../../templates/invoice/renderer/',
|
||||
__DIR__ . '/../../../var/invoices/',
|
||||
__DIR__ . '/../../../var/invoices_customer/',
|
||||
__DIR__ . '/../../../var/invoices_old/',
|
||||
];
|
||||
|
||||
$files = [];
|
||||
|
||||
$dirs = [
|
||||
realpath($this->getInvoiceTemplatePath()),
|
||||
realpath(__DIR__ . '/../templates/'),
|
||||
realpath(__DIR__ . '/../../../var/invoices/'),
|
||||
];
|
||||
|
||||
foreach ($dirs as $dir) {
|
||||
if (!is_dir($dir)) {
|
||||
if ($dir === false || !is_dir($dir)) {
|
||||
continue;
|
||||
}
|
||||
$dir = realpath($dir);
|
||||
$loader->addPath($dir . '/', 'invoice');
|
||||
$found = glob($dir . '/*.html.twig');
|
||||
if ($found !== false) {
|
||||
$files = array_merge($files, $found);
|
||||
|
||||
$finder = new Finder();
|
||||
$finder
|
||||
->in($dir)
|
||||
->name('*.html.twig')
|
||||
->sortByName()
|
||||
->files();
|
||||
|
||||
foreach ($finder->getIterator() as $splFile) {
|
||||
$filename = $splFile->getRealPath();
|
||||
if ($filename === false) {
|
||||
continue;
|
||||
}
|
||||
$dir = \dirname($filename) . '/';
|
||||
if (!\array_key_exists($dir, $files)) {
|
||||
$loader->addPath($dir . '/', 'invoice');
|
||||
}
|
||||
$files[$dir][] = $filename;
|
||||
}
|
||||
}
|
||||
|
||||
// search for custom templates, that shall not be shipped
|
||||
$dirs = [
|
||||
realpath(__DIR__ . '/../../../var/templates/'),
|
||||
];
|
||||
|
||||
foreach ($dirs as $dir) {
|
||||
if ($dir === false || !is_dir($dir)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$finder = new Finder();
|
||||
$finder
|
||||
->in($dir)
|
||||
->name('*.html.twig')
|
||||
->path('invoice-tpl/')
|
||||
->sortByName()
|
||||
->files();
|
||||
|
||||
foreach ($finder->getIterator() as $splFile) {
|
||||
$filename = $splFile->getRealPath();
|
||||
if ($filename === false) {
|
||||
continue;
|
||||
}
|
||||
$dir = \dirname($filename) . '/';
|
||||
if (!\array_key_exists($dir, $files)) {
|
||||
$loader->addPath($dir . '/', 'invoice');
|
||||
}
|
||||
$files[$dir][] = $filename;
|
||||
}
|
||||
}
|
||||
|
||||
$allFiles = [];
|
||||
|
||||
foreach ($files as $templates) {
|
||||
foreach ($templates as $filename) {
|
||||
$allFiles[] = $filename;
|
||||
}
|
||||
}
|
||||
|
||||
self::assertGreaterThanOrEqual(2, \count($allFiles));
|
||||
|
||||
$sut = new TwigRenderer($twig);
|
||||
|
||||
$model = $this->getInvoiceModel();
|
||||
$model->getTemplate()->setLanguage('de');
|
||||
|
||||
foreach ($files as $filename) {
|
||||
foreach ($allFiles as $filename) {
|
||||
$document = new InvoiceDocument(new \SplFileInfo($filename));
|
||||
|
||||
$response = $sut->render($document, $model);
|
||||
|
||||
@@ -18,10 +18,10 @@ use Twig\Environment;
|
||||
|
||||
class HtmlRendererFactoryMock extends AbstractMockFactory
|
||||
{
|
||||
public function create(): HtmlRendererFactory
|
||||
public function create(?Environment $environment = null): HtmlRendererFactory
|
||||
{
|
||||
return new HtmlRendererFactory(
|
||||
$this->createMock(Environment::class),
|
||||
$environment ?? $this->createMock(Environment::class),
|
||||
$this->createMock(EventDispatcherInterface::class),
|
||||
$this->createMock(ProjectStatisticService::class),
|
||||
$this->createMock(ActivityStatisticService::class)
|
||||
|
||||
@@ -22,7 +22,7 @@ use Twig\Environment;
|
||||
|
||||
class PdfRendererFactoryMock extends AbstractMockFactory
|
||||
{
|
||||
public function create(): PdfRendererFactory
|
||||
public function create(?Environment $environment = null): PdfRendererFactory
|
||||
{
|
||||
$converter = new ColumnConverter(
|
||||
$this->createMock(EventDispatcherInterface::class),
|
||||
@@ -31,7 +31,7 @@ class PdfRendererFactoryMock extends AbstractMockFactory
|
||||
);
|
||||
|
||||
return new PdfRendererFactory(
|
||||
$this->createMock(Environment::class),
|
||||
$environment ?? $this->createMock(Environment::class),
|
||||
$this->createMock(HtmlToPdfConverter::class),
|
||||
$this->createMock(ProjectStatisticService::class),
|
||||
$this->createMock(LocaleSwitcher::class),
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Twig\SecurityPolicy;
|
||||
|
||||
use App\Twig\SecurityPolicy\ChainPolicy;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Twig\Sandbox\SecurityPolicyInterface;
|
||||
|
||||
#[CoversClass(ChainPolicy::class)]
|
||||
class ChainPolicyTest extends TestCase
|
||||
{
|
||||
public function testCheckSecurity(): void
|
||||
{
|
||||
$policy1 = $this->createMock(SecurityPolicyInterface::class);
|
||||
$policy1->expects(self::once())->method('checkSecurity')->with(['tag'], ['filter'], ['function']);
|
||||
|
||||
$policy2 = $this->createMock(SecurityPolicyInterface::class);
|
||||
$policy2->expects(self::once())->method('checkSecurity')->with(['tag'], ['filter'], ['function']);
|
||||
|
||||
$sut = new ChainPolicy();
|
||||
$sut->addPolicy($policy1);
|
||||
$sut->addPolicy($policy2);
|
||||
|
||||
$sut->checkSecurity(['tag'], ['filter'], ['function']);
|
||||
}
|
||||
|
||||
public function testCheckMethodAllowed(): void
|
||||
{
|
||||
$obj = new \stdClass();
|
||||
$policy1 = $this->createMock(SecurityPolicyInterface::class);
|
||||
$policy1->expects(self::once())->method('checkMethodAllowed')->with($obj, 'method');
|
||||
|
||||
$policy2 = $this->createMock(SecurityPolicyInterface::class);
|
||||
$policy2->expects(self::once())->method('checkMethodAllowed')->with($obj, 'method');
|
||||
|
||||
$sut = new ChainPolicy();
|
||||
$sut->addPolicy($policy1);
|
||||
$sut->addPolicy($policy2);
|
||||
|
||||
$sut->checkMethodAllowed($obj, 'method');
|
||||
}
|
||||
|
||||
public function testCheckPropertyAllowed(): void
|
||||
{
|
||||
$obj = new \stdClass();
|
||||
$policy1 = $this->createMock(SecurityPolicyInterface::class);
|
||||
$policy1->expects(self::once())->method('checkPropertyAllowed')->with($obj, 'property');
|
||||
|
||||
$policy2 = $this->createMock(SecurityPolicyInterface::class);
|
||||
$policy2->expects(self::once())->method('checkPropertyAllowed')->with($obj, 'property');
|
||||
|
||||
$sut = new ChainPolicy();
|
||||
$sut->addPolicy($policy1);
|
||||
$sut->addPolicy($policy2);
|
||||
|
||||
$sut->checkPropertyAllowed($obj, 'property');
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Twig\SecurityPolicy;
|
||||
|
||||
use App\Twig\SecurityPolicy\DefaultPolicy;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use Twig\Sandbox\SecurityPolicyInterface;
|
||||
|
||||
#[CoversClass(DefaultPolicy::class)]
|
||||
class DefaultPolicyTest extends AbstractPolicyTestCase
|
||||
{
|
||||
protected function createPolicy(): SecurityPolicyInterface
|
||||
{
|
||||
return new DefaultPolicy();
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Twig\SecurityPolicy;
|
||||
|
||||
use App\Twig\SecurityPolicy\ExportPolicy;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use Twig\Sandbox\SecurityPolicyInterface;
|
||||
|
||||
#[CoversClass(ExportPolicy::class)]
|
||||
class ExportPolicyTest extends AbstractPolicyTestCase
|
||||
{
|
||||
protected function createPolicy(): SecurityPolicyInterface
|
||||
{
|
||||
return new ExportPolicy();
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Twig\SecurityPolicy;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Pdf\PdfContext;
|
||||
use App\Twig\SecurityPolicy\InvoicePolicy;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Bridge\Twig\AppVariable;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\ServerBag;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
use Symfony\Component\String\UnicodeString;
|
||||
use Twig\Sandbox\SecurityNotAllowedMethodError;
|
||||
use Twig\Sandbox\SecurityPolicyInterface;
|
||||
|
||||
#[CoversClass(InvoicePolicy::class)]
|
||||
class InvoicePolicyTest extends TestCase
|
||||
{
|
||||
protected function createPolicy(): SecurityPolicyInterface
|
||||
{
|
||||
return new InvoicePolicy();
|
||||
}
|
||||
|
||||
public function testCheckSecurity(): void
|
||||
{
|
||||
$sut = $this->createPolicy();
|
||||
$sut->checkSecurity([], [], []);
|
||||
$this->expectNotToPerformAssertions();
|
||||
}
|
||||
|
||||
#[DataProvider('getCheckMethodAllowedData')]
|
||||
public function testCheckMethodAllowed(object $obj, string $method, ?string $expectedExceptionMessage = null): void
|
||||
{
|
||||
$sut = $this->createPolicy();
|
||||
|
||||
if ($expectedExceptionMessage !== null) {
|
||||
$this->expectException(SecurityNotAllowedMethodError::class);
|
||||
$this->expectExceptionMessage($expectedExceptionMessage);
|
||||
}
|
||||
|
||||
$sut->checkMethodAllowed($obj, $method);
|
||||
|
||||
if ($expectedExceptionMessage === null) {
|
||||
$this->expectNotToPerformAssertions();
|
||||
}
|
||||
}
|
||||
|
||||
public static function getCheckMethodAllowedData(): array
|
||||
{
|
||||
return [
|
||||
[new ServerBag(), 'get', 'Tried to access server environment'],
|
||||
[self::createStub(SessionInterface::class), 'getId', 'Tried to access session'],
|
||||
[new \stdClass(), 'foo', 'Tried to access non-read method'],
|
||||
[new \stdClass(), 'setFoo', 'Tried to access non-read method'],
|
||||
[new \stdClass(), 'getFoo'],
|
||||
[new \stdClass(), 'hasFoo'],
|
||||
[new \stdClass(), 'isFoo'],
|
||||
[new UnicodeString(), '__toString'],
|
||||
// Request
|
||||
[new Request(), 'get', null],
|
||||
[new Request(), 'isXmlHttpRequest', 'Tried to call setter() of app variable'],
|
||||
[new Request(), 'hasSession', 'Tried to call setter() of app variable'],
|
||||
// PdfContext
|
||||
[new PdfContext(), 'setOption'],
|
||||
[new PdfContext(), 'getOption', 'Tried to access forbidden method on PdfContext'],
|
||||
// AppVariable
|
||||
[new AppVariable(), 'getRequest'],
|
||||
[new AppVariable(), 'getUser'],
|
||||
[new AppVariable(), 'getLocale'],
|
||||
[new AppVariable(), 'getCharset', 'Tried to access forbidden app variable method'],
|
||||
// User
|
||||
[new User(), 'getUsername'],
|
||||
[new User(), 'getPassword', 'Tried to access user secrets'],
|
||||
[new User(), 'getTotpSecret', 'Tried to access user secrets'],
|
||||
[new User(), 'getPlainPassword', 'Tried to access user secrets'],
|
||||
[new User(), 'getConfirmationToken', 'Tried to access user secrets'],
|
||||
[new User(), 'getTotpAuthenticationConfiguration', 'Tried to access user secrets'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,8 @@ namespace App\Tests\Twig\SecurityPolicy;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Pdf\PdfContext;
|
||||
use App\Twig\SecurityPolicy\StrictPolicy;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Bridge\Twig\AppVariable;
|
||||
@@ -21,9 +23,13 @@ use Symfony\Component\String\UnicodeString;
|
||||
use Twig\Sandbox\SecurityNotAllowedMethodError;
|
||||
use Twig\Sandbox\SecurityPolicyInterface;
|
||||
|
||||
abstract class AbstractPolicyTestCase extends TestCase
|
||||
#[CoversClass(StrictPolicy::class)]
|
||||
class StrictPolicyTestCase extends TestCase
|
||||
{
|
||||
abstract protected function createPolicy(): SecurityPolicyInterface;
|
||||
private function createPolicy(): SecurityPolicyInterface
|
||||
{
|
||||
return new StrictPolicy();
|
||||
}
|
||||
|
||||
public function testCheckSecurity(): void
|
||||
{
|
||||
Reference in New Issue
Block a user