Release 2.46 (#5757)
This commit is contained in:
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -5,8 +5,10 @@ tests export-ignore
|
||||
.codecov.yml export-ignore
|
||||
.editorconfig export-ignore
|
||||
eslint.config.js export-ignore
|
||||
eslint.config.mjs export-ignore
|
||||
.gitattributes export-ignore
|
||||
.gitignore export-ignore
|
||||
.php-cs-fixer.dist.php export-ignore
|
||||
php-cs-fixer.dist.php export-ignore
|
||||
babel.config.js export-ignore
|
||||
package.json export-ignore
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,6 +3,8 @@
|
||||
/.env-*
|
||||
/.idea/
|
||||
.DS_Store
|
||||
var/templates/
|
||||
|
||||
|
||||
# custom apache rules e.g. to deactivate ioncube loader
|
||||
/public/.user.ini
|
||||
@@ -14,6 +16,7 @@
|
||||
# YARN 2
|
||||
/.yarnrc.yml
|
||||
/.yarn
|
||||
/.pnp.*
|
||||
|
||||
# for keeping empty directories
|
||||
/config/packages/local.yaml
|
||||
|
||||
@@ -10,7 +10,7 @@ Send your ideas, code reviews, pull requests and feature requests to help to imp
|
||||
- Make your changes in a new git branch, based on the latest code in `main`
|
||||
- Apply our code-style by running `composer codestyle-fix`
|
||||
- Run the static code analysis with `composer phpstan`
|
||||
- Verify everything still works with `composer tests-unit`
|
||||
- Verify everything still works with `composer tests`
|
||||
- Add tests for your changes
|
||||
|
||||
Further documentation can be found in the [developer documentation](https://www.kimai.org/documentation/developers.html).
|
||||
|
||||
@@ -24,7 +24,7 @@ export default class KimaiAutocompleteTags extends KimaiAutocomplete {
|
||||
API.get(apiUrl, {'name': query}, (data) => {
|
||||
let results = [];
|
||||
for (let item of data) {
|
||||
results.push({text: item.name, value: item.name, color: item.color});
|
||||
results.push({text: item.name, value: item.name, color: item['color-safe']});
|
||||
}
|
||||
callback(results);
|
||||
}, () => {
|
||||
|
||||
397
composer.lock
generated
397
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
||||
<tile>
|
||||
<square150x150logo src="favicon/mstile-150x150.png"/>
|
||||
<square310x310logo src="favicon/mstile-large.jpg"/>
|
||||
<TileColor>#00a300</TileColor>
|
||||
<TileColor>#ffffff</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
||||
@@ -1 +0,0 @@
|
||||
(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)}]);
|
||||
1
public/build/app-rtl.fe7c5bf2.js
Normal file
1
public/build/app-rtl.fe7c5bf2.js
Normal file
@@ -0,0 +1 @@
|
||||
(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.307ea672.js
Normal file
2
public/build/app.307ea672.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/calendar.cef61816.js
Normal file
2
public/build/calendar.cef61816.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/dashboard.faf42d4e.js
Normal file
2
public/build/dashboard.faf42d4e.js
Normal file
File diff suppressed because one or more lines are too long
@@ -3,7 +3,7 @@
|
||||
"app": {
|
||||
"js": [
|
||||
"/build/runtime.6c399d29.js",
|
||||
"/build/app.9662939e.js"
|
||||
"/build/app.307ea672.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/app.c18ba3c6.css"
|
||||
@@ -12,7 +12,7 @@
|
||||
"app-rtl": {
|
||||
"js": [
|
||||
"/build/runtime.6c399d29.js",
|
||||
"/build/app-rtl.15853b82.js"
|
||||
"/build/app-rtl.fe7c5bf2.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/app-rtl.2003dce5.css"
|
||||
@@ -21,7 +21,7 @@
|
||||
"export-pdf": {
|
||||
"js": [
|
||||
"/build/runtime.6c399d29.js",
|
||||
"/build/export-pdf.395749ab.js"
|
||||
"/build/export-pdf.5905454a.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/export-pdf.d8a6c23b.css"
|
||||
@@ -30,7 +30,7 @@
|
||||
"invoice": {
|
||||
"js": [
|
||||
"/build/runtime.6c399d29.js",
|
||||
"/build/invoice.42b319e4.js"
|
||||
"/build/invoice.0217cc18.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/invoice.36018785.css"
|
||||
@@ -39,7 +39,7 @@
|
||||
"invoice-pdf": {
|
||||
"js": [
|
||||
"/build/runtime.6c399d29.js",
|
||||
"/build/invoice-pdf.26d98626.js"
|
||||
"/build/invoice-pdf.1b6e735c.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/invoice-pdf.2b749265.css"
|
||||
@@ -48,13 +48,13 @@
|
||||
"chart": {
|
||||
"js": [
|
||||
"/build/runtime.6c399d29.js",
|
||||
"/build/chart.bafa38e7.js"
|
||||
"/build/chart.2c85f027.js"
|
||||
]
|
||||
},
|
||||
"calendar": {
|
||||
"js": [
|
||||
"/build/runtime.6c399d29.js",
|
||||
"/build/calendar.b2d70caa.js"
|
||||
"/build/calendar.cef61816.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/calendar.d757753e.css"
|
||||
@@ -63,7 +63,7 @@
|
||||
"dashboard": {
|
||||
"js": [
|
||||
"/build/runtime.6c399d29.js",
|
||||
"/build/dashboard.6ce7ac9c.js"
|
||||
"/build/dashboard.faf42d4e.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/dashboard.b7129fa1.css"
|
||||
@@ -72,7 +72,7 @@
|
||||
"highlight": {
|
||||
"js": [
|
||||
"/build/runtime.6c399d29.js",
|
||||
"/build/highlight.13d5d50e.js"
|
||||
"/build/highlight.718fe73d.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/highlight.98bf3927.css"
|
||||
@@ -81,22 +81,22 @@
|
||||
},
|
||||
"integrity": {
|
||||
"/build/runtime.6c399d29.js": "sha384-/rm616f12czi8l/27GvWXtb3g608vJZf2XTUKxqCRI4tsa2vUHP+BW90edTok5zC",
|
||||
"/build/app.9662939e.js": "sha384-bRL78SVI1wjTweqKeb+ZSSbA78i6By89S8/xx+55zmbOVtF1TCu3lf3pvdki5ZVJ",
|
||||
"/build/app.307ea672.js": "sha384-XNjtkar2JbWyJQan/C6jHZAYFI9X5rPcExLeDV86FYZMPJl0vNWMh8Pa8NgXX/EB",
|
||||
"/build/app.c18ba3c6.css": "sha384-qkIgqLzngG2NchdFVImbGMU49lWlZg6Y9D0z2P0u1j2NQstDicA4KqIkkNYcpErm",
|
||||
"/build/app-rtl.15853b82.js": "sha384-UnKKgLMu9FnRT+CFE0no/+UiUks012bYriQdUWa6f02mo6Lswl947mPybjvKL503",
|
||||
"/build/app-rtl.fe7c5bf2.js": "sha384-UyLXlR/3/H6oFjRrIMH04PA8bAjVQ8rdVWkWoiO4LLYasg4H3UNWJ+Xh+FrYekKh",
|
||||
"/build/app-rtl.2003dce5.css": "sha384-pl8GyGo8sRRw1zLh9D42ZAvo450onfxeszZWocUUuzq70jAOwOlaoPYh/yyc20U7",
|
||||
"/build/export-pdf.395749ab.js": "sha384-3Hjvmu4FC/0dhHnR8kyRBU7k2xMNy1lxBpGgOkrw8PxXnwyQDM8/5bQmkJbjVT1+",
|
||||
"/build/export-pdf.5905454a.js": "sha384-AkNj018YEQJSxC1e/3jIvlh0uR5tHnlUe9ciBbHKIoRIwj7658tCLNVaT98SNFEX",
|
||||
"/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg",
|
||||
"/build/invoice.42b319e4.js": "sha384-xxK7sCe/ZhTjMPFPeX1xvILURxNRZz2hJHZxAGVaw9zE6TC++2/6y2eKqg8cz852",
|
||||
"/build/invoice.0217cc18.js": "sha384-ORySQp4DBSYc7so90AAb8h5abgQtz3tdoO+Zv1uHJ2XqSPMAGtFBrb+Rdzc6sZg1",
|
||||
"/build/invoice.36018785.css": "sha384-jukM9uZ6pexDxXKgZThSxiqXimzsxzniBMHz08N9x8ryXZYkM5r/ZgaainCV0+J6",
|
||||
"/build/invoice-pdf.26d98626.js": "sha384-gwNzQiU1y6qU/M9DPGiNW0MVZkLctEHk37sCES2X9ov+zugEaDABdkMjKBYOC9lz",
|
||||
"/build/invoice-pdf.1b6e735c.js": "sha384-8I5YMkETBNl7wD12V/hKX2YGfwH+V6PXWsu4I+1xXXx9bDzqDhtq+S7x/jey2/Wr",
|
||||
"/build/invoice-pdf.2b749265.css": "sha384-DXXgkz2WWnrWnfBnXX5fmfPQSPb98upMnWxYKwTGYS04EhrPIWfDCutB2unIrWh7",
|
||||
"/build/chart.bafa38e7.js": "sha384-Ays2qGKvOqs4NSeN/zJOPcrnzIEC/uYSCIkC3kN0KQye+mA5Lq7UkfjZDjulvFUX",
|
||||
"/build/calendar.b2d70caa.js": "sha384-FS7Q9iCWHpo2Nzk0tmNvnuTbxJKLdbXFba9F6WcXfHbKFAS2Q+lW29H9Mv+ap5kA",
|
||||
"/build/chart.2c85f027.js": "sha384-5Dm9aFCCSzlh5QY/vtU7/UrBkKt6aWnTccjljmpbQVFfzSEfFh0wUUV8qlXqyFJj",
|
||||
"/build/calendar.cef61816.js": "sha384-qCpJrSB6fZLXT94oiWHofGbX8C0TFl86oCMTdeKWqCmNGJxABkIVo+8k19p7sGlY",
|
||||
"/build/calendar.d757753e.css": "sha384-cTmQMgHYjd2gfObFWmEUph7qQLCyXaIkneSf+bQ2mqVmZwqOB+pJOm/UYTyTjALJ",
|
||||
"/build/dashboard.6ce7ac9c.js": "sha384-5BOoyjZx/ZKr9IbogWfT/aBRHx7XA1fePADdUpUNlSF/rqVo3taXLLcrIoaDZ+8b",
|
||||
"/build/dashboard.faf42d4e.js": "sha384-xkM5D8wBK1sJekLI9DUQBxTjxjomfPQWWtWCo7NX03jNnFZtMv5anM0IISLWaFY0",
|
||||
"/build/dashboard.b7129fa1.css": "sha384-2nn5hLA+3YedgHYBpge62S8Losj8aoPwK9Zk9EvN1xEYatvOUQ7H3rIR2UUJAGOS",
|
||||
"/build/highlight.13d5d50e.js": "sha384-+RqBBPIzp5YQpbBeHgVNXzHiewK1PZtizPh4in1PlBU6RcT1bDleW3QSJMDeBnPO",
|
||||
"/build/highlight.718fe73d.js": "sha384-eHb21blXJ7SKqCFlMSxl4oblpusDif1B0Tsr2Iw1xQfUqxZxwtFwYYvMPllfw7BY",
|
||||
"/build/highlight.98bf3927.css": "sha384-YgweSwDwN0dI4DEmh478xYVw/TewJYvCO1QTbWHpaHFDPuLrZvYMR0Tc+QfFxVPE"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
(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
public/build/export-pdf.5905454a.js
Normal file
1
public/build/export-pdf.5905454a.js
Normal file
@@ -0,0 +1 @@
|
||||
(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)}]);
|
||||
File diff suppressed because one or more lines are too long
1
public/build/highlight.718fe73d.js
Normal file
1
public/build/highlight.718fe73d.js
Normal file
File diff suppressed because one or more lines are too long
1
public/build/invoice-pdf.1b6e735c.js
Normal file
1
public/build/invoice-pdf.1b6e735c.js
Normal file
@@ -0,0 +1 @@
|
||||
(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 +0,0 @@
|
||||
(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
public/build/invoice.0217cc18.js
Normal file
1
public/build/invoice.0217cc18.js
Normal file
@@ -0,0 +1 @@
|
||||
(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 +0,0 @@
|
||||
(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,21 +1,21 @@
|
||||
{
|
||||
"build/app.css": "/build/app.c18ba3c6.css",
|
||||
"build/app.js": "/build/app.9662939e.js",
|
||||
"build/app.js": "/build/app.307ea672.js",
|
||||
"build/app-rtl.css": "/build/app-rtl.2003dce5.css",
|
||||
"build/app-rtl.js": "/build/app-rtl.15853b82.js",
|
||||
"build/app-rtl.js": "/build/app-rtl.fe7c5bf2.js",
|
||||
"build/export-pdf.css": "/build/export-pdf.d8a6c23b.css",
|
||||
"build/export-pdf.js": "/build/export-pdf.395749ab.js",
|
||||
"build/export-pdf.js": "/build/export-pdf.5905454a.js",
|
||||
"build/invoice.css": "/build/invoice.36018785.css",
|
||||
"build/invoice.js": "/build/invoice.42b319e4.js",
|
||||
"build/invoice.js": "/build/invoice.0217cc18.js",
|
||||
"build/invoice-pdf.css": "/build/invoice-pdf.2b749265.css",
|
||||
"build/invoice-pdf.js": "/build/invoice-pdf.26d98626.js",
|
||||
"build/chart.js": "/build/chart.bafa38e7.js",
|
||||
"build/invoice-pdf.js": "/build/invoice-pdf.1b6e735c.js",
|
||||
"build/chart.js": "/build/chart.2c85f027.js",
|
||||
"build/calendar.css": "/build/calendar.d757753e.css",
|
||||
"build/calendar.js": "/build/calendar.b2d70caa.js",
|
||||
"build/calendar.js": "/build/calendar.cef61816.js",
|
||||
"build/dashboard.css": "/build/dashboard.b7129fa1.css",
|
||||
"build/dashboard.js": "/build/dashboard.6ce7ac9c.js",
|
||||
"build/dashboard.js": "/build/dashboard.faf42d4e.js",
|
||||
"build/highlight.css": "/build/highlight.98bf3927.css",
|
||||
"build/highlight.js": "/build/highlight.13d5d50e.js",
|
||||
"build/highlight.js": "/build/highlight.718fe73d.js",
|
||||
"build/runtime.js": "/build/runtime.6c399d29.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",
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
],
|
||||
"scope": "./",
|
||||
"start_url": "./",
|
||||
"theme-color": "#1d273b",
|
||||
"theme-color": "#262626",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
@@ -17,11 +17,11 @@ final class Constants
|
||||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '2.45.0';
|
||||
public const VERSION = '2.46.0';
|
||||
/**
|
||||
* The current release: major * 10000 + minor * 100 + patch
|
||||
*/
|
||||
public const VERSION_ID = 24500;
|
||||
public const VERSION_ID = 24600;
|
||||
/**
|
||||
* The software name
|
||||
*/
|
||||
|
||||
@@ -23,7 +23,8 @@ trait ColorTrait
|
||||
* The assigned color in HTML hex format, e.g. #dd1d00
|
||||
*/
|
||||
#[ORM\Column(name: 'color', type: Types::STRING, length: 7, nullable: true)]
|
||||
#[Serializer\Exclude]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'color')]
|
||||
#[Constraints\HexColor]
|
||||
private ?string $color = null;
|
||||
@@ -50,10 +51,10 @@ trait ColorTrait
|
||||
abstract public function getName(): ?string;
|
||||
|
||||
/**
|
||||
* Internal value: this color will never be empty and is generated by the tag name if not set explicit.
|
||||
* Color will never be empty and is generated from the entity tag name if not set explicit.
|
||||
*/
|
||||
#[Serializer\VirtualProperty]
|
||||
#[Serializer\SerializedName('color')]
|
||||
#[Serializer\SerializedName('color-safe')]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
public function getColorSafe(): string
|
||||
{
|
||||
|
||||
@@ -117,7 +117,11 @@ class HtmlRenderer implements ExportRendererInterface
|
||||
'userPreferences' => $userPreferences,
|
||||
], $this->getOptions($query)));
|
||||
|
||||
// allows to run in development mode, otherwise toolbar would be blocked
|
||||
$sandbox->disableSandbox();
|
||||
|
||||
$response = new Response();
|
||||
$response->headers->set('Content-Type', 'text/html');
|
||||
$response->setContent($content);
|
||||
|
||||
return $response;
|
||||
|
||||
@@ -16,10 +16,6 @@ final class ChainPolicy implements SecurityPolicyInterface
|
||||
/** @var array<SecurityPolicyInterface> */
|
||||
private array $policies = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function addPolicy(SecurityPolicyInterface $policy): void
|
||||
{
|
||||
$this->policies[] = $policy;
|
||||
|
||||
@@ -9,6 +9,13 @@
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
@@ -22,6 +29,49 @@ final class DefaultPolicy implements SecurityPolicyInterface
|
||||
|
||||
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,109 +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\Markup;
|
||||
use Twig\Sandbox\SecurityNotAllowedFilterError;
|
||||
use Twig\Sandbox\SecurityNotAllowedFunctionError;
|
||||
use Twig\Sandbox\SecurityNotAllowedMethodError;
|
||||
use Twig\Sandbox\SecurityNotAllowedPropertyError;
|
||||
use Twig\Sandbox\SecurityNotAllowedTagError;
|
||||
use Twig\Sandbox\SecurityPolicyInterface;
|
||||
use Twig\Template;
|
||||
|
||||
/**
|
||||
* A blocking approach for Twig templates.
|
||||
*/
|
||||
final class ForbiddenPolicy implements SecurityPolicyInterface
|
||||
{
|
||||
/** @var array<string, array<string>> */
|
||||
private array $forbiddenMethods = [];
|
||||
|
||||
/**
|
||||
* @param array<string> $forbiddenTags
|
||||
* @param array<string> $forbiddenFilters
|
||||
* @param array<string, array<string>> $forbiddenMethods
|
||||
* @param array<string, array<string>> $forbiddenProperties
|
||||
* @param array<string> $forbiddenFunctions
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly array $forbiddenTags = [],
|
||||
private readonly array $forbiddenFilters = [],
|
||||
array $forbiddenMethods = [],
|
||||
private readonly array $forbiddenProperties = [],
|
||||
private readonly array $forbiddenFunctions = []
|
||||
)
|
||||
{
|
||||
$this->forbiddenMethods = [];
|
||||
foreach ($forbiddenMethods as $class => $m) {
|
||||
$this->forbiddenMethods[$class] = array_map(function ($value) { return strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); }, \is_array($m) ? $m : [$m]);
|
||||
}
|
||||
}
|
||||
|
||||
public function checkSecurity($tags, $filters, $functions): void
|
||||
{
|
||||
foreach ($tags as $tag) {
|
||||
if (\in_array($tag, $this->forbiddenTags)) {
|
||||
throw new SecurityNotAllowedTagError(\sprintf('Tag "%s" is not allowed.', $tag), $tag);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($filters as $filter) {
|
||||
if (\in_array($filter, $this->forbiddenFilters)) {
|
||||
throw new SecurityNotAllowedFilterError(\sprintf('Filter "%s" is not allowed.', $filter), $filter);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($functions as $function) {
|
||||
if (\in_array($function, $this->forbiddenFunctions)) {
|
||||
throw new SecurityNotAllowedFunctionError(\sprintf('Function "%s" is not allowed.', $function), $function);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function checkMethodAllowed($obj, $method): void
|
||||
{
|
||||
if ($obj instanceof Template || $obj instanceof Markup) { // @phpstan-ignore instanceof.internalClass
|
||||
return;
|
||||
}
|
||||
|
||||
$forbidden = false;
|
||||
$method = strtr($method, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
|
||||
foreach ($this->forbiddenMethods as $class => $methods) {
|
||||
if ($obj instanceof $class) {
|
||||
$forbidden = \in_array($method, $methods);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($forbidden) {
|
||||
$class = \get_class($obj);
|
||||
throw new SecurityNotAllowedMethodError(\sprintf('Calling "%s" method on a "%s" object is not allowed.', $method, $class), $class, $method);
|
||||
}
|
||||
}
|
||||
|
||||
public function checkPropertyAllowed($obj, $property): void
|
||||
{
|
||||
$forbidden = false;
|
||||
foreach ($this->forbiddenProperties as $class => $properties) {
|
||||
if ($obj instanceof $class) {
|
||||
$forbidden = \in_array($property, \is_array($properties) ? $properties : [$properties]);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($forbidden) {
|
||||
$class = \get_class($obj);
|
||||
throw new SecurityNotAllowedPropertyError(\sprintf('Calling "%s" property on a "%s" object is not allowed.', $property, $class), $class, $property);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,13 +22,13 @@ use Twig\Template;
|
||||
*/
|
||||
final class InvoicePolicy implements SecurityPolicyInterface
|
||||
{
|
||||
private ChainPolicy $policy;
|
||||
private SecurityPolicyInterface $default;
|
||||
private SecurityPolicyInterface $security;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->policy = new ChainPolicy();
|
||||
$this->policy->addPolicy(new DefaultPolicy());
|
||||
$this->policy->addPolicy(new SecurityPolicy(
|
||||
$this->default = new DefaultPolicy();
|
||||
$this->security = new SecurityPolicy(
|
||||
['block', 'if', 'for', 'set', 'extends', 'import'],
|
||||
[
|
||||
// =================================================================
|
||||
@@ -194,12 +194,13 @@ final class InvoicePolicy implements SecurityPolicyInterface
|
||||
'month_names',
|
||||
'locale_format',
|
||||
]
|
||||
));
|
||||
);
|
||||
}
|
||||
|
||||
public function checkSecurity($tags, $filters, $functions): void
|
||||
{
|
||||
$this->policy->checkSecurity($tags, $filters, $functions);
|
||||
$this->default->checkSecurity($tags, $filters, $functions);
|
||||
$this->security->checkSecurity($tags, $filters, $functions);
|
||||
}
|
||||
|
||||
public function checkMethodAllowed($obj, $method): void
|
||||
@@ -208,21 +209,20 @@ final class InvoicePolicy implements SecurityPolicyInterface
|
||||
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')) {
|
||||
if (str_starts_with($lm, 'get') || str_starts_with($lm, 'is') || str_starts_with($lm, 'has') || $lm === '__tostring') {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($lm === '__tostring') {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->policy->checkMethodAllowed($obj, $method);
|
||||
$this->security->checkMethodAllowed($obj, $method);
|
||||
}
|
||||
|
||||
public function checkPropertyAllowed($obj, $property): void
|
||||
{
|
||||
$this->policy->checkPropertyAllowed($obj, $property);
|
||||
$this->default->checkPropertyAllowed($obj, $property);
|
||||
$this->security->checkPropertyAllowed($obj, $property);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{% extends 'export/layout.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "macros/datatables.html.twig" as tables %}
|
||||
{% import "macros/webloader.html.twig" as webloader %}
|
||||
|
||||
{% block document_title %}{{ 'export'|trans }}{% endblock %}
|
||||
|
||||
@@ -52,147 +51,144 @@
|
||||
}) %}
|
||||
|
||||
{% block javascripts %}
|
||||
{{ webloader.init_frontend_loader() }}
|
||||
<script type="text/javascript">
|
||||
let initialized = false;
|
||||
|
||||
document.addEventListener('kimai.initialized', function(event) {
|
||||
document.getElementById('duration-decimal').addEventListener('click', function(event) {
|
||||
var spans = document.getElementsByClassName('duration-format');
|
||||
for (var span of spans) {
|
||||
if (!event.target.checked) {
|
||||
if (span.dataset['duration'] !== undefined) {
|
||||
span.innerHTML = span.dataset['duration'];
|
||||
}
|
||||
} else {
|
||||
if (span.dataset['durationDecimal'] !== undefined) {
|
||||
span.innerHTML = span.dataset['durationDecimal'];
|
||||
}
|
||||
document.getElementById('duration-decimal').addEventListener('click', function(event) {
|
||||
var spans = document.getElementsByClassName('duration-format');
|
||||
for (var span of spans) {
|
||||
if (!event.target.checked) {
|
||||
if (span.dataset['duration'] !== undefined) {
|
||||
span.innerHTML = span.dataset['duration'];
|
||||
}
|
||||
} else {
|
||||
if (span.dataset['durationDecimal'] !== undefined) {
|
||||
span.innerHTML = span.dataset['durationDecimal'];
|
||||
}
|
||||
}
|
||||
saveVisibility();
|
||||
});
|
||||
|
||||
document.getElementById('summary-by-activities').addEventListener('click', function(event) {
|
||||
document.getElementById('summary-project').style.display = event.target.checked ? 'none' : 'table';
|
||||
document.getElementById('summary-activity').style.display = event.target.checked ? 'table' : 'none';
|
||||
saveVisibility();
|
||||
});
|
||||
document.getElementById('date-format').addEventListener('change', function(event) {
|
||||
changedDateFormat(event.target.value, 'dateformat');
|
||||
});
|
||||
document.getElementById('begin-format').addEventListener('change', function(event) {
|
||||
changedDateFormat(event.target.value, 'beginformat');
|
||||
});
|
||||
document.getElementById('end-format').addEventListener('change', function(event) {
|
||||
changedDateFormat(event.target.value, 'endformat');
|
||||
});
|
||||
document.getElementById('summary-show').addEventListener('click', function(event) {
|
||||
document.getElementById('export-summary').style.display = event.target.checked ? 'block' : 'none';
|
||||
saveVisibility();
|
||||
});
|
||||
document.getElementById('summary-timeBudget').addEventListener('click', function(event) {
|
||||
let cells = document.getElementsByClassName('export-timeBudget');
|
||||
for (let columnCell of cells) {
|
||||
columnCell.style.display = event.target.checked ? 'table-cell' : 'none';
|
||||
}
|
||||
saveVisibility();
|
||||
});
|
||||
document.getElementById('summary-budget').addEventListener('click', function(event) {
|
||||
let cells = document.getElementsByClassName('export-budget');
|
||||
for (let columnCell of cells) {
|
||||
columnCell.style.display = event.target.checked ? 'table-cell' : 'none';
|
||||
}
|
||||
saveVisibility();
|
||||
});
|
||||
document.getElementById('summary-duration').addEventListener('click', function(event) {
|
||||
let cells = document.getElementsByClassName('summary-duration');
|
||||
for (let columnCell of cells) {
|
||||
columnCell.style.display = event.target.checked ? 'table-cell' : 'none';
|
||||
}
|
||||
saveVisibility();
|
||||
});
|
||||
document.getElementById('summary-rate').addEventListener('click', function(event) {
|
||||
let cells = document.getElementsByClassName('summary-rate');
|
||||
for (let columnCell of cells) {
|
||||
columnCell.style.display = event.target.checked ? 'table-cell' : 'none';
|
||||
}
|
||||
saveVisibility();
|
||||
});
|
||||
document.getElementById('summary-internalRate').addEventListener('click', function(event) {
|
||||
let cells = document.getElementsByClassName('summary-internalRate');
|
||||
for (let columnCell of cells) {
|
||||
columnCell.style.display = event.target.checked ? 'table-cell' : 'none';
|
||||
}
|
||||
saveVisibility();
|
||||
});
|
||||
document.getElementById('summary-show').addEventListener('click', function(event) {
|
||||
document.getElementById('export-summary').style.display = event.target.checked ? 'block' : 'none';
|
||||
saveVisibility();
|
||||
});
|
||||
document.getElementById('records-show').addEventListener('click', function(event) {
|
||||
document.getElementById('export-records').style.display = event.target.checked ? 'block' : 'none';
|
||||
saveVisibility();
|
||||
});
|
||||
|
||||
let columnCheckboxes = document.getElementsByClassName('column-visibility-changer');
|
||||
|
||||
for (let checkbox of columnCheckboxes) {
|
||||
checkbox.addEventListener('click', function(event) {
|
||||
changeVisibility(event.target.name, event.target.checked);
|
||||
});
|
||||
}
|
||||
saveVisibility();
|
||||
});
|
||||
|
||||
let editableTitles = document.querySelectorAll('[contenteditable=true]');
|
||||
document.getElementById('summary-by-activities').addEventListener('click', function(event) {
|
||||
document.getElementById('summary-project').style.display = event.target.checked ? 'none' : 'table';
|
||||
document.getElementById('summary-activity').style.display = event.target.checked ? 'table' : 'none';
|
||||
saveVisibility();
|
||||
});
|
||||
document.getElementById('date-format').addEventListener('change', function(event) {
|
||||
changedDateFormat(event.target.value, 'dateformat');
|
||||
});
|
||||
document.getElementById('begin-format').addEventListener('change', function(event) {
|
||||
changedDateFormat(event.target.value, 'beginformat');
|
||||
});
|
||||
document.getElementById('end-format').addEventListener('change', function(event) {
|
||||
changedDateFormat(event.target.value, 'endformat');
|
||||
});
|
||||
document.getElementById('summary-show').addEventListener('click', function(event) {
|
||||
document.getElementById('export-summary').style.display = event.target.checked ? 'block' : 'none';
|
||||
saveVisibility();
|
||||
});
|
||||
document.getElementById('summary-timeBudget').addEventListener('click', function(event) {
|
||||
let cells = document.getElementsByClassName('export-timeBudget');
|
||||
for (let columnCell of cells) {
|
||||
columnCell.style.display = event.target.checked ? 'table-cell' : 'none';
|
||||
}
|
||||
saveVisibility();
|
||||
});
|
||||
document.getElementById('summary-budget').addEventListener('click', function(event) {
|
||||
let cells = document.getElementsByClassName('export-budget');
|
||||
for (let columnCell of cells) {
|
||||
columnCell.style.display = event.target.checked ? 'table-cell' : 'none';
|
||||
}
|
||||
saveVisibility();
|
||||
});
|
||||
document.getElementById('summary-duration').addEventListener('click', function(event) {
|
||||
let cells = document.getElementsByClassName('summary-duration');
|
||||
for (let columnCell of cells) {
|
||||
columnCell.style.display = event.target.checked ? 'table-cell' : 'none';
|
||||
}
|
||||
saveVisibility();
|
||||
});
|
||||
document.getElementById('summary-rate').addEventListener('click', function(event) {
|
||||
let cells = document.getElementsByClassName('summary-rate');
|
||||
for (let columnCell of cells) {
|
||||
columnCell.style.display = event.target.checked ? 'table-cell' : 'none';
|
||||
}
|
||||
saveVisibility();
|
||||
});
|
||||
document.getElementById('summary-internalRate').addEventListener('click', function(event) {
|
||||
let cells = document.getElementsByClassName('summary-internalRate');
|
||||
for (let columnCell of cells) {
|
||||
columnCell.style.display = event.target.checked ? 'table-cell' : 'none';
|
||||
}
|
||||
saveVisibility();
|
||||
});
|
||||
document.getElementById('summary-show').addEventListener('click', function(event) {
|
||||
document.getElementById('export-summary').style.display = event.target.checked ? 'block' : 'none';
|
||||
saveVisibility();
|
||||
});
|
||||
document.getElementById('records-show').addEventListener('click', function(event) {
|
||||
document.getElementById('export-records').style.display = event.target.checked ? 'block' : 'none';
|
||||
saveVisibility();
|
||||
});
|
||||
|
||||
for (let editable of editableTitles) {
|
||||
editable.addEventListener('input', function(event) {
|
||||
if (event.target.innerText === '') {
|
||||
return;
|
||||
let columnCheckboxes = document.getElementsByClassName('column-visibility-changer');
|
||||
|
||||
for (let checkbox of columnCheckboxes) {
|
||||
checkbox.addEventListener('click', function(event) {
|
||||
changeVisibility(event.target.name, event.target.checked);
|
||||
});
|
||||
}
|
||||
|
||||
let editableTitles = document.querySelectorAll('[contenteditable=true]');
|
||||
|
||||
for (let editable of editableTitles) {
|
||||
editable.addEventListener('input', function(event) {
|
||||
if (event.target.innerText === '') {
|
||||
return;
|
||||
}
|
||||
saveVisibility();
|
||||
});
|
||||
}
|
||||
|
||||
// needs to be executed as last action in the flow, after the listener were registered
|
||||
let config = localStorage.getItem('{{ storageItemName }}');
|
||||
if (config !== null) {
|
||||
try {
|
||||
config = JSON.parse(config);
|
||||
for (const elName in config) {
|
||||
if (!config.hasOwnProperty(elName)) {
|
||||
continue;
|
||||
}
|
||||
saveVisibility();
|
||||
});
|
||||
}
|
||||
|
||||
// needs to be executed as last action in the flow, after the listener were registered
|
||||
let config = localStorage.getItem('{{ storageItemName }}');
|
||||
if (config !== null) {
|
||||
try {
|
||||
config = JSON.parse(config);
|
||||
for (const elName in config) {
|
||||
if (!config.hasOwnProperty(elName)) {
|
||||
let elValue = config[elName];
|
||||
let el = document.getElementById(elName);
|
||||
if (el === undefined || el === null) {
|
||||
el = document.getElementById('records-column-' + elName);
|
||||
if (el === undefined || el === null) {
|
||||
continue;
|
||||
}
|
||||
let elValue = config[elName];
|
||||
let el = document.getElementById(elName);
|
||||
if (el === undefined || el === null) {
|
||||
el = document.getElementById('records-column-' + elName);
|
||||
if (el === undefined || el === null) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (el.type === 'checkbox') {
|
||||
if (elValue !== el.checked) {
|
||||
el.click();
|
||||
}
|
||||
} else if (el.type === 'select-one') {
|
||||
if (el.value !== elValue) {
|
||||
el.value = elValue;
|
||||
el.dispatchEvent(new Event('change'));
|
||||
}
|
||||
} else if (el.isContentEditable) {
|
||||
el.innerText = elValue;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore error in restoring
|
||||
console.log('Failed to restore settings, removing invalid settings', e);
|
||||
localStorage.removeItem('{{ storageItemName }}');
|
||||
if (el.type === 'checkbox') {
|
||||
if (elValue !== el.checked) {
|
||||
el.click();
|
||||
}
|
||||
} else if (el.type === 'select-one') {
|
||||
if (el.value !== elValue) {
|
||||
el.value = elValue;
|
||||
el.dispatchEvent(new Event('change'));
|
||||
}
|
||||
} else if (el.isContentEditable) {
|
||||
el.innerText = elValue;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore error in restoring
|
||||
console.log('Failed to restore settings, removing invalid settings', e);
|
||||
localStorage.removeItem('{{ storageItemName }}');
|
||||
}
|
||||
}
|
||||
|
||||
initialized = true;
|
||||
});
|
||||
initialized = true;
|
||||
|
||||
function changeVisibility(column, visible)
|
||||
{
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="application-name" content="{{ constant('App\\Constants::SOFTWARE') }}">
|
||||
<meta name="msapplication-config" content="{{ asset('browserconfig.xml') }}">
|
||||
<meta name="theme-color" content="#1d273b">
|
||||
<meta name="theme-color" content="#262626">
|
||||
{% if tabler_bundle.isThemeAuto() %}
|
||||
<script>if (window.matchMedia) { document.documentElement.setAttribute('data-bs-theme', (window.matchMedia("(prefers-color-scheme: dark)").matches) ? 'dark': 'light'); }</script>
|
||||
{% endif %}
|
||||
|
||||
@@ -327,7 +327,8 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
return [
|
||||
'id' => 'int',
|
||||
'name' => 'string',
|
||||
'color' => 'string',
|
||||
'color' => '@string',
|
||||
'color-safe' => 'string',
|
||||
'visible' => 'bool',
|
||||
];
|
||||
|
||||
@@ -358,7 +359,8 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
'enabled' => 'bool',
|
||||
'apiToken' => 'bool',
|
||||
'systemAccount' => 'bool',
|
||||
'color' => 'string',
|
||||
'color' => '@string',
|
||||
'color-safe' => 'string',
|
||||
'avatar' => '@string',
|
||||
'alias' => '@string',
|
||||
'accountNumber' => '@string',
|
||||
@@ -378,7 +380,8 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
'enabled' => 'bool',
|
||||
'apiToken' => 'bool',
|
||||
'systemAccount' => 'bool',
|
||||
'color' => 'string',
|
||||
'color' => '@string',
|
||||
'color-safe' => 'string',
|
||||
'avatar' => '@string',
|
||||
'alias' => '@string',
|
||||
'accountNumber' => '@string',
|
||||
@@ -402,7 +405,8 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
return [
|
||||
'id' => 'int',
|
||||
'name' => 'string',
|
||||
'color' => 'string',
|
||||
'color' => '@string',
|
||||
'color-safe' => 'string',
|
||||
];
|
||||
|
||||
// explicitly requested team
|
||||
@@ -410,7 +414,8 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
return [
|
||||
'id' => 'int',
|
||||
'name' => 'string',
|
||||
'color' => 'string',
|
||||
'color' => '@string',
|
||||
'color-safe' => 'string',
|
||||
'members' => ['result' => 'array', 'type' => 'TeamMember'],
|
||||
// TODO more info in entity than in collection
|
||||
'customers' => ['result' => 'array', 'type' => '@Customer'],
|
||||
@@ -439,7 +444,8 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
'name' => 'string',
|
||||
'visible' => 'bool',
|
||||
'billable' => 'bool',
|
||||
'color' => 'string',
|
||||
'color' => '@string',
|
||||
'color-safe' => 'string',
|
||||
'number' => '@string',
|
||||
'comment' => '@string',
|
||||
'currency' => 'string',
|
||||
@@ -460,7 +466,8 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
'name' => 'string',
|
||||
'visible' => 'boolean',
|
||||
'billable' => 'bool',
|
||||
'color' => 'string',
|
||||
'color' => '@string',
|
||||
'color-safe' => 'string',
|
||||
'number' => '@string',
|
||||
'comment' => '@string',
|
||||
'currency' => 'string',
|
||||
@@ -482,7 +489,8 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
'name' => 'string',
|
||||
'visible' => 'bool',
|
||||
'billable' => 'bool',
|
||||
'color' => 'string',
|
||||
'color' => '@string',
|
||||
'color-safe' => 'string',
|
||||
'number' => '@string',
|
||||
'comment' => '@string',
|
||||
'currency' => 'string',
|
||||
@@ -519,7 +527,8 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
'name' => 'string',
|
||||
'visible' => 'bool',
|
||||
'billable' => 'bool',
|
||||
'color' => 'string',
|
||||
'color' => '@string',
|
||||
'color-safe' => 'string',
|
||||
'customer' => 'int',
|
||||
'number' => '@string',
|
||||
'orderNumber' => '@string',
|
||||
@@ -538,7 +547,8 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
'name' => 'string',
|
||||
'visible' => 'bool',
|
||||
'billable' => 'bool',
|
||||
'color' => 'string',
|
||||
'color' => '@string',
|
||||
'color-safe' => 'string',
|
||||
'customer' => ['result' => 'object', 'type' => 'Customer'],
|
||||
'number' => '@string',
|
||||
'orderNumber' => '@string',
|
||||
@@ -557,7 +567,8 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
'name' => 'string',
|
||||
'visible' => 'bool',
|
||||
'billable' => 'bool',
|
||||
'color' => 'string',
|
||||
'color' => '@string',
|
||||
'color-safe' => 'string',
|
||||
'customer' => 'int',
|
||||
'number' => '@string',
|
||||
'orderNumber' => '@string',
|
||||
@@ -578,7 +589,8 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
'name' => 'string',
|
||||
'visible' => 'bool',
|
||||
'billable' => 'bool',
|
||||
'color' => 'string',
|
||||
'color' => '@string',
|
||||
'color-safe' => 'string',
|
||||
'customer' => 'int',
|
||||
'number' => '@string',
|
||||
'orderNumber' => '@string',
|
||||
@@ -605,7 +617,8 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
'billable' => 'bool',
|
||||
'project' => '@int',
|
||||
'number' => '@string',
|
||||
'color' => 'string',
|
||||
'color' => '@string',
|
||||
'color-safe' => 'string',
|
||||
'metaFields' => ['result' => 'array', 'type' => 'ProjectMeta'], // since 2.45
|
||||
'comment' => '@string',
|
||||
];
|
||||
@@ -618,7 +631,8 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
'billable' => 'bool',
|
||||
'project' => ['result' => 'object', 'type' => '@ProjectExpanded'],
|
||||
'number' => '@string',
|
||||
'color' => 'string',
|
||||
'color' => '@string',
|
||||
'color-safe' => 'string',
|
||||
'metaFields' => ['result' => 'array', 'type' => 'ProjectMeta'], // since 2.45
|
||||
'comment' => '@string',
|
||||
];
|
||||
@@ -632,7 +646,8 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
'billable' => 'bool',
|
||||
'project' => '@int',
|
||||
'number' => '@string',
|
||||
'color' => 'string',
|
||||
'color' => '@string',
|
||||
'color-safe' => 'string',
|
||||
'metaFields' => ['result' => 'array', 'type' => 'ProjectMeta'],
|
||||
'comment' => '@string',
|
||||
'parentTitle' => '@string',
|
||||
@@ -648,7 +663,8 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
'billable' => 'bool',
|
||||
'project' => '@int',
|
||||
'number' => '@string',
|
||||
'color' => 'string',
|
||||
'color' => '@string',
|
||||
'color-safe' => 'string',
|
||||
'metaFields' => ['result' => 'array', 'type' => 'ProjectMeta'],
|
||||
'comment' => '@string',
|
||||
'parentTitle' => '@string',
|
||||
|
||||
@@ -244,7 +244,6 @@ class ActivityControllerTest extends APIControllerBaseTestCase
|
||||
self::assertIsArray($result);
|
||||
self::assertApiResponseTypeStructure('ActivityEntity', $result);
|
||||
|
||||
self::assertCount(14, array_keys($result));
|
||||
self::assertNull($result['parentTitle']);
|
||||
self::assertNotEmpty($result['id']);
|
||||
self::assertIsArray($result['teams']);
|
||||
@@ -258,7 +257,8 @@ class ActivityControllerTest extends APIControllerBaseTestCase
|
||||
self::assertNull($result['budgetType']);
|
||||
self::assertNull($result['number']);
|
||||
self::assertEquals('Test comment', $result['comment']);
|
||||
self::assertEquals('#5319e7', $result['color']);
|
||||
self::assertNull($result['color']);
|
||||
self::assertEquals('#5319e7', $result['color-safe']);
|
||||
self::assertTrue($result['visible']);
|
||||
self::assertTrue($result['billable']);
|
||||
}
|
||||
@@ -297,7 +297,7 @@ class ActivityControllerTest extends APIControllerBaseTestCase
|
||||
self::assertEquals('Test', $result['parentTitle']);
|
||||
self::assertNotEmpty($result['id']);
|
||||
self::assertIsArray($result['teams']);
|
||||
self::assertEquals([['id' => 1, 'name' => 'Test team', 'color' => '#03A9F4']], $result['teams']);
|
||||
self::assertEquals([['id' => 1, 'name' => 'Test team', 'color' => null, 'color-safe' => '#03A9F4']], $result['teams']);
|
||||
self::assertIsArray($result['metaFields']);
|
||||
self::assertEquals([], $result['metaFields']);
|
||||
self::assertEquals('foo', $result['name']);
|
||||
|
||||
@@ -229,7 +229,6 @@ class CustomerControllerTest extends APIControllerBaseTestCase
|
||||
self::assertIsArray($result);
|
||||
self::assertApiResponseTypeStructure('CustomerEntity', $result);
|
||||
|
||||
self::assertCount(30, array_keys($result));
|
||||
self::assertNotEmpty($result['id']);
|
||||
self::assertIsArray($result['teams']);
|
||||
self::assertCount(1, $result['teams']);
|
||||
@@ -262,7 +261,8 @@ class CustomerControllerTest extends APIControllerBaseTestCase
|
||||
self::assertNull($result['homepage']);
|
||||
self::assertEquals('Europe/Berlin', $result['timezone']);
|
||||
self::assertNull($result['buyerReference']);
|
||||
self::assertEquals('#5319e7', $result['color']);
|
||||
self::assertNull($result['color']);
|
||||
self::assertEquals('#5319e7', $result['color-safe']);
|
||||
self::assertTrue($result['visible']);
|
||||
self::assertTrue($result['billable']);
|
||||
}
|
||||
@@ -316,7 +316,7 @@ class CustomerControllerTest extends APIControllerBaseTestCase
|
||||
self::assertApiResponseTypeStructure('CustomerEntity', $result);
|
||||
self::assertNotEmpty($result['id']);
|
||||
self::assertIsArray($result['teams']);
|
||||
self::assertEquals([['id' => 1, 'name' => 'Test team', 'color' => '#03A9F4']], $result['teams']);
|
||||
self::assertEquals([['id' => 1, 'name' => 'Test team', 'color' => null, 'color-safe' => '#03A9F4']], $result['teams']);
|
||||
self::assertIsArray($result['metaFields']);
|
||||
self::assertEquals([], $result['metaFields']);
|
||||
self::assertEquals('foo', $result['name']);
|
||||
|
||||
@@ -311,7 +311,6 @@ class ProjectControllerTest extends APIControllerBaseTestCase
|
||||
self::assertIsArray($result);
|
||||
self::assertApiResponseTypeStructure('ProjectEntity', $result);
|
||||
|
||||
self::assertCount(19, array_keys($result));
|
||||
self::assertEquals('first one', $result['parentTitle']);
|
||||
self::assertEquals($project->getId(), $result['id']);
|
||||
self::assertIsArray($result['teams']);
|
||||
@@ -329,7 +328,8 @@ class ProjectControllerTest extends APIControllerBaseTestCase
|
||||
self::assertNull($result['orderNumber']);
|
||||
self::assertNull($result['number']);
|
||||
self::assertNull($result['comment']);
|
||||
self::assertEquals('#2ECC40', $result['color']);
|
||||
self::assertNull($result['color']);
|
||||
self::assertEquals('#2ECC40', $result['color-safe']);
|
||||
self::assertTrue($result['globalActivities']);
|
||||
self::assertTrue($result['billable']);
|
||||
self::assertTrue($result['visible']);
|
||||
@@ -374,7 +374,7 @@ class ProjectControllerTest extends APIControllerBaseTestCase
|
||||
self::assertEquals('Test', $result['parentTitle']);
|
||||
self::assertNotEmpty($result['id']);
|
||||
self::assertIsArray($result['teams']);
|
||||
self::assertEquals([['id' => 1, 'name' => 'Test team', 'color' => '#03A9F4']], $result['teams']);
|
||||
self::assertEquals([['id' => 1, 'name' => 'Test team', 'color' => null, 'color-safe' => '#03A9F4']], $result['teams']);
|
||||
self::assertIsArray($result['metaFields']);
|
||||
self::assertEquals([], $result['metaFields']);
|
||||
self::assertEquals('foo', $result['name']);
|
||||
|
||||
128
tests/Export/Base/DefaultRendererTest.php
Normal file
128
tests/Export/Base/DefaultRendererTest.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?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\Export\Base;
|
||||
|
||||
use App\Export\Base\CsvRenderer;
|
||||
use App\Export\Base\HtmlRenderer;
|
||||
use App\Export\Base\PDFRenderer;
|
||||
use App\Export\Base\XlsxRenderer;
|
||||
use App\Export\ServiceExport;
|
||||
use App\Repository\ExportTemplateRepository;
|
||||
use App\Tests\Export\Renderer\AbstractRendererTestCase;
|
||||
use App\Tests\Mocks\Export\CsvRendererFactoryMock;
|
||||
use App\Tests\Mocks\Export\HtmlRendererFactoryMock;
|
||||
use App\Tests\Mocks\Export\PdfRendererFactoryMock;
|
||||
use App\Tests\Mocks\Export\XlsxRendererFactoryMock;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
#[CoversClass(ServiceExport::class)]
|
||||
#[CoversClass(CsvRenderer::class)]
|
||||
#[CoversClass(XlsxRenderer::class)]
|
||||
#[CoversClass(PDFRenderer::class)]
|
||||
#[CoversClass(HtmlRenderer::class)]
|
||||
#[Group('integration')]
|
||||
class DefaultRendererTest extends AbstractRendererTestCase
|
||||
{
|
||||
private function createServiceExport(): ServiceExport
|
||||
{
|
||||
$repository = $this->createMock(ExportTemplateRepository::class);
|
||||
$repository->expects($this->once())->method('findAll')->willReturn([]);
|
||||
$logger = $this->createMock(LoggerInterface::class);
|
||||
|
||||
return new ServiceExport(
|
||||
$this->createMock(EventDispatcherInterface::class),
|
||||
(new HtmlRendererFactoryMock($this))->create(),
|
||||
(new PdfRendererFactoryMock($this))->create(),
|
||||
(new CsvRendererFactoryMock($this))->create(),
|
||||
(new XlsxRendererFactoryMock($this))->create(),
|
||||
$repository,
|
||||
$logger,
|
||||
);
|
||||
}
|
||||
|
||||
public function testRenderDefaultTemplates(): void
|
||||
{
|
||||
$sut = $this->createServiceExport();
|
||||
|
||||
$renderer = $sut->getRenderer();
|
||||
self::assertCount(4, $renderer);
|
||||
self::assertInstanceOf(CsvRenderer::class, $renderer[0]);
|
||||
self::assertInstanceOf(XlsxRenderer::class, $renderer[1]);
|
||||
self::assertInstanceOf(PDFRenderer::class, $renderer[2]);
|
||||
self::assertInstanceOf(HtmlRenderer::class, $renderer[3]);
|
||||
|
||||
// make sure that the default templates do NOT violate the Twig SecurityPolicy
|
||||
$response = $this->render($renderer[0]);
|
||||
self::assertEquals('text/csv', $response->headers->get('Content-Type'));
|
||||
self::assertStringContainsString('attachment; filename', $response->headers->get('Content-Disposition') ?? '');
|
||||
|
||||
$response = $this->render($renderer[1]);
|
||||
self::assertEquals('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', $response->headers->get('Content-Type') ?? '');
|
||||
self::assertStringContainsString('attachment; filename', $response->headers->get('Content-Disposition') ?? '');
|
||||
|
||||
$response = $this->render($renderer[2]);
|
||||
self::assertEquals('application/pdf', $response->headers->get('Content-Type') ?? '');
|
||||
self::assertStringContainsString('attachment; filename', $response->headers->get('Content-Disposition') ?? '');
|
||||
|
||||
$response = $this->render($renderer[3]);
|
||||
self::assertEquals('text/html', $response->headers->get('Content-Type') ?? '');
|
||||
// HTML is attached to the body and twig is a mock in this setupo, so we just receive an empty string
|
||||
}
|
||||
|
||||
public function testRenderCustomTemplates(): void
|
||||
{
|
||||
$searchDir = __DIR__ . '/../../../var/templates';
|
||||
if (!is_dir($searchDir)) {
|
||||
$this->expectNotToPerformAssertions();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$finder = new Finder();
|
||||
$finder
|
||||
->in($searchDir)
|
||||
->name('*.twig')
|
||||
->path('export-tpl/')
|
||||
->files()
|
||||
;
|
||||
|
||||
$files = [];
|
||||
$dirs = [];
|
||||
foreach ($finder->getIterator() as $filename => $splFile) {
|
||||
$files[] = $splFile->getRealPath();
|
||||
$dir = \dirname($splFile->getRealPath());
|
||||
$dirs[$dir] = $dir;
|
||||
}
|
||||
$dirs = array_keys($dirs);
|
||||
|
||||
if (\count($dirs) === 0) {
|
||||
$this->expectNotToPerformAssertions();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$sut = $this->createServiceExport();
|
||||
foreach ($dirs as $dir) {
|
||||
$sut->addDirectory($dir);
|
||||
}
|
||||
|
||||
$renderers = $sut->getRenderer();
|
||||
self::assertCount(4 + \count($files), $renderers);
|
||||
foreach ($renderers as $renderer) {
|
||||
$response = $this->render($renderer);
|
||||
self::assertInstanceOf(Response::class, $response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,8 +129,8 @@ abstract class AbstractRendererTestCase extends KernelTestCase
|
||||
|
||||
$entries = [$timesheet, $timesheet2, $timesheet3, $timesheet4, $timesheet5, $timesheet6];
|
||||
|
||||
$currentUser = $this->createMock(User::class);
|
||||
$currentUser->expects($this->any())->method('isExportDecimal')->willReturn($exportDecimal);
|
||||
$currentUser = new User();
|
||||
$currentUser->setPreferenceValue('export_decimal', $exportDecimal);
|
||||
|
||||
$query = new TimesheetQuery();
|
||||
$query->setActivities([$activity]);
|
||||
|
||||
@@ -14,10 +14,12 @@ use App\Invoice\InvoiceItemHydrator;
|
||||
use App\Invoice\InvoiceModel;
|
||||
use App\Invoice\InvoiceModelHydrator;
|
||||
use App\Model\InvoiceDocument;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
#[CoversClass(DebugRenderer::class)]
|
||||
class DebugRendererTest extends TestCase
|
||||
{
|
||||
use RendererTestTrait;
|
||||
|
||||
@@ -18,6 +18,7 @@ use App\Tests\Mocks\FileHelperFactory;
|
||||
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;
|
||||
@@ -118,12 +119,26 @@ class PdfRendererTest extends KernelTestCase
|
||||
|
||||
$dirs = [
|
||||
__DIR__ . '/../../../templates/invoice/renderer/',
|
||||
//__DIR__ . '/../../../var/invoices/',
|
||||
//__DIR__ . '/../../../var/invoices_customer/',
|
||||
//__DIR__ . '/../../../var/invoices_old/',
|
||||
];
|
||||
|
||||
$files = [];
|
||||
|
||||
$additionalTemplatesDir = __DIR__ . '/../../../var/templates';
|
||||
if (is_dir($additionalTemplatesDir)) {
|
||||
$finder = new Finder();
|
||||
$finder
|
||||
->in($additionalTemplatesDir)
|
||||
->name('*.pdf.twig')
|
||||
->path('invoice-tpl/')
|
||||
->files();
|
||||
|
||||
foreach ($finder->getIterator() as $splFile) {
|
||||
$filename = $splFile->getRealPath();
|
||||
$files[] = $filename;
|
||||
$loader->addPath(\dirname($filename) . '/', 'invoice');
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($dirs as $dir) {
|
||||
if (!is_dir($dir)) {
|
||||
continue;
|
||||
@@ -145,7 +160,6 @@ class PdfRendererTest extends KernelTestCase
|
||||
$response = $sut->render($document, $model);
|
||||
self::assertEquals('application/pdf', $response->headers->get('Content-Type'));
|
||||
self::assertStringContainsString('attachment; filename', $response->headers->get('Content-Disposition'));
|
||||
self::assertNotEmpty($response->getContent());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ use App\Model\InvoiceDocument;
|
||||
use App\Repository\InvoiceRepository;
|
||||
use App\Repository\Query\InvoiceQuery;
|
||||
use App\Tests\Mocks\InvoiceModelFactoryFactory;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
|
||||
trait RendererTestTrait
|
||||
{
|
||||
@@ -85,6 +84,10 @@ trait RendererTestTrait
|
||||
|
||||
protected function getInvoiceModel(): InvoiceModel
|
||||
{
|
||||
$activityId = new \ReflectionProperty(Activity::class, 'id');
|
||||
$projectId = new \ReflectionProperty(Project::class, 'id');
|
||||
$userId = new \ReflectionProperty(User::class, 'id');
|
||||
|
||||
$user = new User();
|
||||
$user->setUserIdentifier('one-user');
|
||||
$user->setTitle('user title');
|
||||
@@ -107,59 +110,54 @@ trait RendererTestTrait
|
||||
|
||||
$pMeta = new ProjectMeta();
|
||||
$pMeta->setName('foo-project')->setValue('bar-project')->setIsVisible(true);
|
||||
$project = $this->createMock(Project::class);
|
||||
$project->method('getId')->willReturn(0);
|
||||
$project->method('getName')->willReturn('project name');
|
||||
$project->method('getCustomer')->willReturn($customer);
|
||||
$project->method('getMetaFields')->willReturn(new ArrayCollection([$pMeta]));
|
||||
$project->method('getVisibleMetaFields')->willReturn([$pMeta]);
|
||||
$project = new Project();
|
||||
$projectId->setValue($project, 0);
|
||||
$project->setName('project name');
|
||||
$project->setCustomer($customer);
|
||||
$project->setMetaField($pMeta);
|
||||
|
||||
$aMeta = new ActivityMeta();
|
||||
$aMeta->setName('foo-activity');
|
||||
$aMeta->setValue('bar-activity');
|
||||
$aMeta->setIsVisible(true);
|
||||
$activity = $this->createMock(Activity::class);
|
||||
$activity->method('getId')->willReturn(0);
|
||||
$activity->method('getName')->willReturn('activity description');
|
||||
$activity->method('getProject')->willReturn($project);
|
||||
$activity->method('getMetaFields')->willReturn(new ArrayCollection([$aMeta]));
|
||||
$activity->method('getVisibleMetaFields')->willReturn([$aMeta]);
|
||||
$activity = new Activity();
|
||||
$activityId->setValue($activity, 0);
|
||||
$activity->setName('activity description');
|
||||
$activity->setProject($project);
|
||||
$activity->setMetaField($aMeta);
|
||||
|
||||
$pMeta2 = new ProjectMeta();
|
||||
$pMeta2->setName('foo-project')->setValue('bar-project2')->setIsVisible(true);
|
||||
$project2 = $this->createMock(Project::class);
|
||||
$project2->method('getId')->willReturn(1);
|
||||
$project2->method('getName')->willReturn('project 2 name');
|
||||
$project2->method('getCustomer')->willReturn($customer);
|
||||
$project2->method('getMetaFields')->willReturn(new ArrayCollection([$pMeta2]));
|
||||
$project2->method('getVisibleMetaFields')->willReturn([$pMeta2]);
|
||||
$project2 = new Project();
|
||||
$projectId->setValue($project2, 1);
|
||||
$project2->setName('project 2 name');
|
||||
$project2->setCustomer($customer);
|
||||
$project2->setMetaField($pMeta2);
|
||||
|
||||
$aMeta2 = new ActivityMeta();
|
||||
$aMeta2->setName('foo-activity');
|
||||
$aMeta2->setValue('bar-activity2');
|
||||
$aMeta2->setIsVisible(true);
|
||||
$activity2 = $this->createMock(Activity::class);
|
||||
$activity2->method('getId')->willReturn(1);
|
||||
$activity2->method('getName')->willReturn('activity 1 description');
|
||||
$activity2->method('getProject')->willReturn($project2);
|
||||
$activity2->method('getMetaFields')->willReturn(new ArrayCollection([$aMeta2]));
|
||||
$activity2->method('getVisibleMetaFields')->willReturn([$aMeta2]);
|
||||
$activity2 = new Activity();
|
||||
$activityId->setValue($activity2, 1);
|
||||
$activity2->setName('activity 1 description');
|
||||
$activity2->setProject($project2);
|
||||
$activity2->setMetaField($aMeta2);
|
||||
|
||||
$pref1 = new UserPreference('foo', 'bar');
|
||||
$pref2 = new UserPreference('mad', 123.45);
|
||||
$userMethods = ['getId', 'getPreferenceValue', 'getVisiblePreferences', 'getUsername', 'getUserIdentifier'];
|
||||
$user1 = $this->getMockBuilder(User::class)->onlyMethods($userMethods)->disableOriginalConstructor()->getMock();
|
||||
$user1->method('getId')->willReturn(1);
|
||||
$user1->method('getPreferenceValue')->willReturn('50');
|
||||
$user1->method('getUsername')->willReturn('foo-bar');
|
||||
$user1->method('getUserIdentifier')->willReturn('foo-bar');
|
||||
$user1->method('getVisiblePreferences')->willReturn([$pref1, $pref2]);
|
||||
$user1 = new User();
|
||||
$user1->setUserIdentifier('foo-bar');
|
||||
$userId->setValue($user1, 1);
|
||||
//$user1->method('getPreferenceValue')->willReturn('50');
|
||||
$user1->addPreference($pref1);
|
||||
$user1->addPreference($pref2);
|
||||
|
||||
$user2 = $this->createMock(User::class);
|
||||
$user2->method('getId')->willReturn(2);
|
||||
$user2->method('getUsername')->willReturn('hello-world');
|
||||
$user2->method('getUserIdentifier')->willReturn('hello-world');
|
||||
$user2->method('getVisiblePreferences')->willReturn([$pref1, $pref2]);
|
||||
$user2 = new User();
|
||||
$userId->setValue($user2, 2);
|
||||
$user2->setUserIdentifier('hello-world');
|
||||
$user2->addPreference($pref1);
|
||||
$user2->addPreference($pref2);
|
||||
|
||||
$timesheet = new Timesheet();
|
||||
$timesheet->setDuration(3600);
|
||||
@@ -302,12 +300,14 @@ trait RendererTestTrait
|
||||
|
||||
$pref1 = new UserPreference('foo', 'bar');
|
||||
$pref2 = new UserPreference('mad', 123.45);
|
||||
$user1 = $this->createMock(User::class);
|
||||
$user1->method('getId')->willReturn(1);
|
||||
$user1->method('getPreferenceValue')->willReturn('50');
|
||||
$user1->method('getUsername')->willReturn('foo-bar');
|
||||
$user1->method('getUserIdentifier')->willReturn('foo-bar');
|
||||
$user1->method('getVisiblePreferences')->willReturn([$pref1, $pref2]);
|
||||
|
||||
$userId = new \ReflectionProperty(User::class, 'id');
|
||||
$user1 = new User();
|
||||
$user1->setUserIdentifier('foo-bar');
|
||||
$user1->addPreference($pref1);
|
||||
$user1->addPreference($pref2);
|
||||
$userId->setValue($user1, 1);
|
||||
//$user1->method('getPreferenceValue')->willReturn('50');
|
||||
|
||||
$timesheet = new Timesheet();
|
||||
$timesheet->setDuration(3600);
|
||||
|
||||
91
tests/Twig/SecurityPolicy/AbstractPolicyTestCase.php
Normal file
91
tests/Twig/SecurityPolicy/AbstractPolicyTestCase.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?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 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;
|
||||
|
||||
abstract class AbstractPolicyTestCase extends TestCase
|
||||
{
|
||||
abstract protected function createPolicy(): SecurityPolicyInterface;
|
||||
|
||||
public function testCheckSecurity(): void
|
||||
{
|
||||
$sut = $this->createPolicy();
|
||||
$sut->checkSecurity([], [], []);
|
||||
$this->expectNotToPerformAssertions();
|
||||
}
|
||||
|
||||
public function testCheckPropertyAllowed(): void
|
||||
{
|
||||
$sut = $this->createPolicy();
|
||||
$sut->checkPropertyAllowed(new \stdClass(), 'foo');
|
||||
$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'],
|
||||
];
|
||||
}
|
||||
}
|
||||
66
tests/Twig/SecurityPolicy/ChainPolicyTest.php
Normal file
66
tests/Twig/SecurityPolicy/ChainPolicyTest.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?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');
|
||||
}
|
||||
}
|
||||
23
tests/Twig/SecurityPolicy/DefaultPolicyTest.php
Normal file
23
tests/Twig/SecurityPolicy/DefaultPolicyTest.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
23
tests/Twig/SecurityPolicy/ExportPolicyTest.php
Normal file
23
tests/Twig/SecurityPolicy/ExportPolicyTest.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
90
tests/Twig/SecurityPolicy/InvoicePolicyTest.php
Normal file
90
tests/Twig/SecurityPolicy/InvoicePolicyTest.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?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'],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user