Release 2.60 (#5971)
This commit is contained in:
@@ -46,7 +46,7 @@ var/templates/*
|
||||
nbproject/
|
||||
|
||||
# Dev-only configs
|
||||
test/
|
||||
tests/
|
||||
.php-cs-fixer.dist.php
|
||||
php-cs-fixer.sh
|
||||
phpstan.sh
|
||||
@@ -57,6 +57,9 @@ eslint.config.mjs
|
||||
.codecov.yml
|
||||
phpunit.xml
|
||||
phpunit.xml.dist
|
||||
package.json
|
||||
pnpm-lock.yaml
|
||||
webpack.config.js
|
||||
|
||||
# Docker and compose (not needed inside the image; .docker/ is copied explicitly)
|
||||
docker-compose.yml
|
||||
|
||||
33
.github/release-drafter.yml
vendored
33
.github/release-drafter.yml
vendored
@@ -4,26 +4,37 @@ exclude-labels:
|
||||
- 'duplicate'
|
||||
- 'invalid'
|
||||
- 'wontfix'
|
||||
- 'release'
|
||||
exclude-contributors:
|
||||
- 'dependabot'
|
||||
- 'weblate'
|
||||
change-template: '- $TITLE (#$NUMBER)'
|
||||
change-title-escapes: '\<*_&`#@'
|
||||
categories:
|
||||
- title: "Features"
|
||||
semver-increment: minor
|
||||
when:
|
||||
labels:
|
||||
- "feature request"
|
||||
- "enhancement"
|
||||
- "technical debt"
|
||||
- "translation"
|
||||
- "release"
|
||||
- title: "Bug Fixes"
|
||||
when:
|
||||
labels:
|
||||
- "fix"
|
||||
- "bugfix"
|
||||
- "bug"
|
||||
- title: "🚨 Security"
|
||||
when:
|
||||
label: "security"
|
||||
version-resolver:
|
||||
minor:
|
||||
labels:
|
||||
- 'feature request'
|
||||
- 'release'
|
||||
patch:
|
||||
labels:
|
||||
- 'technical debt'
|
||||
- 'bug'
|
||||
- 'translation'
|
||||
default: patch
|
||||
default: minor
|
||||
template: |
|
||||
**Compatible with PHP 8.2 to 8.5**
|
||||
|
||||
$CHANGES
|
||||
|
||||
You can read more about all security reports [here](https://www.kimai.org/documentation/bughunter.html#published-vulnerabilities) or grab this [RSS feed](https://www.kimai.org/security.xml) to get notified about new published advisories.
|
||||
|
||||
Involved in this release: $CONTRIBUTORS
|
||||
|
||||
7
.github/renovate-config.js
vendored
Normal file
7
.github/renovate-config.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
branchPrefix: 'renovate/',
|
||||
username: 'renovate-release',
|
||||
gitAuthor: 'Renovate Bot <bot@renovateapp.com>',
|
||||
onboarding: false,
|
||||
platform: 'github'
|
||||
};
|
||||
3
.github/workflows/linting.yaml
vendored
3
.github/workflows/linting.yaml
vendored
@@ -72,5 +72,8 @@ jobs:
|
||||
- name: Lint codebase
|
||||
run: composer linting
|
||||
|
||||
- name: Check for published security advisories
|
||||
run: composer audit --abandoned=report
|
||||
|
||||
- name: Check for security issues in packages
|
||||
run: symfony security:check
|
||||
|
||||
23
.github/workflows/renovate.yaml
vendored
Normal file
23
.github/workflows/renovate.yaml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: Renovate
|
||||
|
||||
permissions: {}
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '17 3 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
renovate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone Kimai
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run Renovate
|
||||
uses: renovatebot/github-action@693b9ef15eec82123529a37c782242f091365961 # v46.1.14
|
||||
with:
|
||||
configurationFile: .github/renovate-config.js
|
||||
token: ${{ secrets.RENOVATE_TOKEN }}
|
||||
2
.github/workflows/testing.yaml
vendored
2
.github/workflows/testing.yaml
vendored
@@ -96,7 +96,7 @@ jobs:
|
||||
|
||||
- name: Upload code coverage
|
||||
if: matrix.php == '8.5'
|
||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./coverage.xml
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
"doctrine/doctrine-bundle": "^2.7",
|
||||
"doctrine/doctrine-migrations-bundle": "^3.3",
|
||||
"doctrine/orm": "^2.8",
|
||||
"easybill/zugferd-php": "^2.1",
|
||||
"endroid/qr-code": "^4.8",
|
||||
"erusev/parsedown": "^1.6",
|
||||
"friendsofsymfony/rest-bundle": "^3.0",
|
||||
|
||||
122
composer.lock
generated
122
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "fa13e2255a6ecd636351e12bf7b69792",
|
||||
"content-hash": "3a36f355f322598267719beb6a7c22e0",
|
||||
"packages": [
|
||||
{
|
||||
"name": "azuyalabs/yasumi",
|
||||
@@ -1661,70 +1661,6 @@
|
||||
},
|
||||
"time": "2026-02-08T16:21:46+00:00"
|
||||
},
|
||||
{
|
||||
"name": "easybill/zugferd-php",
|
||||
"version": "v2.3.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/easybill/zugferd-php.git",
|
||||
"reference": "fba2a504feb58ae2b3a19da5a0c051ce8fe13895"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/easybill/zugferd-php/zipball/fba2a504feb58ae2b3a19da5a0c051ce8fe13895",
|
||||
"reference": "fba2a504feb58ae2b3a19da5a0c051ce8fe13895",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-dom": "*",
|
||||
"jms/serializer": "^3.16",
|
||||
"php": "^8.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-libxml": "*",
|
||||
"friendsofphp/php-cs-fixer": "^3.16",
|
||||
"phpstan/phpstan": "^1.10",
|
||||
"phpstan/phpstan-strict-rules": "^1.5",
|
||||
"phpunit/phpunit": "^9.3",
|
||||
"rector/rector": "^1.0",
|
||||
"symfony/finder": "^5.4"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-libxml": "Needed for XSD validation"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Easybill\\ZUGFeRD\\": "src/zugferd10",
|
||||
"Easybill\\ZUGFeRD2\\": "src/zugferd2"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Patrick Romowicz",
|
||||
"email": "patrick@romowicz.de"
|
||||
},
|
||||
{
|
||||
"name": "Jan Nöhles",
|
||||
"email": "jan.noehles@easybill.de"
|
||||
}
|
||||
],
|
||||
"description": "ZUGFeRD PHP SDK (Factur-X, XRechnung) - Convert PHP Objects to XML and back.",
|
||||
"keywords": [
|
||||
"ZUGFeRD",
|
||||
"factur-x",
|
||||
"xrechnung"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/easybill/zugferd-php/issues",
|
||||
"source": "https://github.com/easybill/zugferd-php/tree/v2.3.1"
|
||||
},
|
||||
"time": "2025-09-13T13:57:46+00:00"
|
||||
},
|
||||
{
|
||||
"name": "egulias/email-validator",
|
||||
"version": "4.0.4",
|
||||
@@ -4318,16 +4254,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpoffice/phpspreadsheet",
|
||||
"version": "2.4.5",
|
||||
"version": "2.4.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
|
||||
"reference": "ec7815be350e03df90f3e2ace92653fa6cb4327c"
|
||||
"reference": "0bbef382b7d9c1dbda10c8113d564ff9159a7e79"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/ec7815be350e03df90f3e2ace92653fa6cb4327c",
|
||||
"reference": "ec7815be350e03df90f3e2ace92653fa6cb4327c",
|
||||
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/0bbef382b7d9c1dbda10c8113d564ff9159a7e79",
|
||||
"reference": "0bbef382b7d9c1dbda10c8113d564ff9159a7e79",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4418,9 +4354,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
|
||||
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/2.4.5"
|
||||
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/2.4.6"
|
||||
},
|
||||
"time": "2026-04-19T05:48:49+00:00"
|
||||
"time": "2026-06-07T02:31:12+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpoffice/phpword",
|
||||
@@ -5374,16 +5310,16 @@
|
||||
},
|
||||
{
|
||||
"name": "spomky-labs/otphp",
|
||||
"version": "11.4.3",
|
||||
"version": "11.5.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Spomky-Labs/otphp.git",
|
||||
"reference": "46663316285bf1f001a31584213b327494b00ae7"
|
||||
"reference": "877683d6352b80cdc7020fd43a725629c2524435"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Spomky-Labs/otphp/zipball/46663316285bf1f001a31584213b327494b00ae7",
|
||||
"reference": "46663316285bf1f001a31584213b327494b00ae7",
|
||||
"url": "https://api.github.com/repos/Spomky-Labs/otphp/zipball/877683d6352b80cdc7020fd43a725629c2524435",
|
||||
"reference": "877683d6352b80cdc7020fd43a725629c2524435",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5428,7 +5364,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/Spomky-Labs/otphp/issues",
|
||||
"source": "https://github.com/Spomky-Labs/otphp/tree/11.4.3"
|
||||
"source": "https://github.com/Spomky-Labs/otphp/tree/11.5.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -5440,7 +5376,7 @@
|
||||
"type": "patreon"
|
||||
}
|
||||
],
|
||||
"time": "2026-05-31T12:42:43+00:00"
|
||||
"time": "2026-06-06T23:41:24+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/asset",
|
||||
@@ -11681,16 +11617,16 @@
|
||||
},
|
||||
{
|
||||
"name": "friendsofphp/php-cs-fixer",
|
||||
"version": "v3.95.3",
|
||||
"version": "v3.95.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
|
||||
"reference": "3d681493acc0e93283481b1c63c263737df78687"
|
||||
"reference": "3f8f68856837a77e1f1d870354eca3c8747f2f72"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/3d681493acc0e93283481b1c63c263737df78687",
|
||||
"reference": "3d681493acc0e93283481b1c63c263737df78687",
|
||||
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/3f8f68856837a77e1f1d870354eca3c8747f2f72",
|
||||
"reference": "3f8f68856837a77e1f1d870354eca3c8747f2f72",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -11774,7 +11710,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.95.3"
|
||||
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.95.4"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -11782,7 +11718,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-05-29T20:35:26+00:00"
|
||||
"time": "2026-06-03T18:02:44+00:00"
|
||||
},
|
||||
{
|
||||
"name": "masterminds/html5",
|
||||
@@ -11971,11 +11907,11 @@
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "2.2.1",
|
||||
"version": "2.2.2",
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/dea9c8f2d25cc849391042b71e429c1a4bf82660",
|
||||
"reference": "dea9c8f2d25cc849391042b71e429c1a4bf82660",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/e5cc34d491a90e79c216d824f60fe21fd4d93bd6",
|
||||
"reference": "e5cc34d491a90e79c216d824f60fe21fd4d93bd6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -12031,7 +11967,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-05-28T14:44:12+00:00"
|
||||
"time": "2026-06-05T09:00:01+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan-deprecation-rules",
|
||||
@@ -12085,16 +12021,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan-doctrine",
|
||||
"version": "2.0.23",
|
||||
"version": "2.0.25",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpstan-doctrine.git",
|
||||
"reference": "4821d678585be36f585273de38c06b7e4a98bb91"
|
||||
"reference": "e20e8bf3223ae6eba9c4b5987c391d922e094b3c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/4821d678585be36f585273de38c06b7e4a98bb91",
|
||||
"reference": "4821d678585be36f585273de38c06b7e4a98bb91",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/e20e8bf3223ae6eba9c4b5987c391d922e094b3c",
|
||||
"reference": "e20e8bf3223ae6eba9c4b5987c391d922e094b3c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -12156,9 +12092,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/phpstan/phpstan-doctrine/issues",
|
||||
"source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.23"
|
||||
"source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.25"
|
||||
},
|
||||
"time": "2026-05-25T15:58:25+00:00"
|
||||
"time": "2026-06-02T20:27:36+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan-phpunit",
|
||||
|
||||
12
kimai.sh
12
kimai.sh
@@ -145,10 +145,10 @@ function install_plugins() {
|
||||
if [[ ${#packages[@]} -gt 0 ]]; then
|
||||
verbose "Installing Composer plugins: ${packages[*]}"
|
||||
run_composer require "${packages[@]} --update-no-dev --optimize-autoloader" || exit 1
|
||||
$KIMAI_PHP bin/console kimai:plugins --install || exit 1
|
||||
else
|
||||
verbose "No Composer plugins detected."
|
||||
fi
|
||||
|
||||
verbose "Installing plugins"
|
||||
$KIMAI_PHP bin/console kimai:plugins --install || exit 1
|
||||
}
|
||||
|
||||
function set_permission() {
|
||||
@@ -205,9 +205,9 @@ fi
|
||||
cd "$(dirname "$0")" || { echo "Cannot change working directory."; exit 1; }
|
||||
|
||||
# we need a few commands installed in order for this script to complete
|
||||
composer_exists || { echo >&2 "Update requires 'composer' but it's not installed or not executable."; exit 1; }
|
||||
command -v git >/dev/null 2>&1 || { echo >&2 "Update requires 'git' but it's not installed."; exit 1; }
|
||||
command -v "$KIMAI_PHP" >/dev/null 2>&1 || { echo >&2 "Update requires 'php' but it's not installed."; exit 1; }
|
||||
composer_exists || { echo >&2 "Kimai requires 'composer' but it's not installed or not executable."; exit 1; }
|
||||
command -v git >/dev/null 2>&1 || { echo >&2 "Kimai requires 'git' but it's not installed."; exit 1; }
|
||||
command -v "$KIMAI_PHP" >/dev/null 2>&1 || { echo >&2 "Kimai requires 'php' but it's not installed."; exit 1; }
|
||||
|
||||
verbose "Using PHP: $KIMAI_PHP"
|
||||
verbose "Using Composer: $KIMAI_COMPOSER"
|
||||
|
||||
@@ -24,18 +24,32 @@ final class Version20260530080724 extends AbstractMigration
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// a security related column
|
||||
if (!$schema->getTable('kimai2_users')->hasColumn('signature_date')) {
|
||||
$this->addSql('ALTER TABLE kimai2_users ADD signature_date DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\'');
|
||||
}
|
||||
|
||||
// improve session garbage collection
|
||||
if (!$schema->getTable('kimai2_sessions')->hasIndex('lifetime_idx')) {
|
||||
$this->addSql('CREATE INDEX lifetime_idx ON kimai2_sessions (lifetime)');
|
||||
}
|
||||
|
||||
$this->preventEmptyMigrationWarning(false);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
if ($schema->getTable('kimai2_users')->hasColumn('signature_date')) {
|
||||
$this->addSql('ALTER TABLE kimai2_users DROP signature_date');
|
||||
// improve session garbage collection
|
||||
}
|
||||
|
||||
if ($schema->getTable('kimai2_sessions')->hasIndex('lifetime_idx')) {
|
||||
$this->addSql('DROP INDEX lifetime_idx ON kimai2_sessions');
|
||||
}
|
||||
|
||||
$this->preventEmptyMigrationWarning(false);
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return false;
|
||||
|
||||
@@ -238,10 +238,10 @@ final class InvoiceCreateCommand extends Command
|
||||
$projects[] = $tmp;
|
||||
}
|
||||
$invoices = $this->createInvoicesForProjects($projects, $defaultQuery, $input, $output);
|
||||
} elseif ($byActiveCustomer) {
|
||||
} elseif ($byActiveCustomer) { // @phpstan-ignore elseif.alwaysFalse
|
||||
$customers = $this->getActiveCustomers($defaultQuery);
|
||||
$invoices = $this->createInvoicesForCustomer($customers, $defaultQuery, $input, $output);
|
||||
} elseif ($byActiveProject) {
|
||||
} elseif ($byActiveProject) { // @phpstan-ignore elseif.alwaysTrue
|
||||
$projects = $this->getActiveProjects($defaultQuery);
|
||||
$invoices = $this->createInvoicesForProjects($projects, $defaultQuery, $input, $output);
|
||||
} else {
|
||||
|
||||
@@ -17,11 +17,11 @@ final class Constants
|
||||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '2.59.0';
|
||||
public const VERSION = '2.60.0';
|
||||
/**
|
||||
* The current release: major * 10000 + minor * 100 + patch
|
||||
*/
|
||||
public const VERSION_ID = 25900;
|
||||
public const VERSION_ID = 26000;
|
||||
/**
|
||||
* The software name
|
||||
*/
|
||||
|
||||
@@ -58,10 +58,12 @@ abstract class AbstractMigration extends BaseAbstractMigration
|
||||
}
|
||||
}
|
||||
|
||||
protected function preventEmptyMigrationWarning(): void
|
||||
protected function preventEmptyMigrationWarning(bool $always = true): void
|
||||
{
|
||||
if ($always || \count($this->getSql()) === 0) {
|
||||
$this->addSql('#prevent empty warning - no SQL to execute');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* I don't know how often I accidentally dropped database tables,
|
||||
|
||||
@@ -37,13 +37,18 @@ final class InvoiceModelActivityHydrator implements InvoiceModelHydrator
|
||||
}
|
||||
}
|
||||
|
||||
if (\count($activities) === 0) {
|
||||
return [];
|
||||
$counter = \count($activities);
|
||||
|
||||
$values = [
|
||||
'activity.counter' => $counter,
|
||||
];
|
||||
|
||||
if ($counter === 0) {
|
||||
return $values;
|
||||
}
|
||||
|
||||
$activities = array_values($activities);
|
||||
|
||||
$values = [];
|
||||
$i = 0;
|
||||
|
||||
foreach ($activities as $activity) {
|
||||
|
||||
@@ -40,7 +40,7 @@ final class InvoiceModelProjectHydrator implements InvoiceModelHydrator
|
||||
$counter = \count($projects);
|
||||
|
||||
$values = [
|
||||
'project._counter' => $counter,
|
||||
'project.counter' => $counter,
|
||||
];
|
||||
|
||||
if ($counter === 0) {
|
||||
|
||||
@@ -26,6 +26,9 @@ final class PdfContext
|
||||
'filename', 'mode', 'format', 'orientation', 'default_font', 'default_font_size', 'fonts',
|
||||
'margin_left', 'margin_right', 'margin_top', 'margin_bottom', 'margin_header', 'margin_footer',
|
||||
'setAutoTopMargin', 'setAutoBottomMargin', 'PDFA', 'PDFAauto', 'useActiveForms',
|
||||
// Adding watermarks to PDFs
|
||||
'watermarkImgBehind', 'showWatermarkText', 'showWatermarkImage', 'watermarkText', 'watermarkAngle',
|
||||
'watermarkImage', 'watermark_font', 'watermarkTextAlpha', 'watermarkImageAlpha',
|
||||
];
|
||||
|
||||
private array $options = [];
|
||||
|
||||
@@ -45,6 +45,7 @@ class InvoiceArchiveQuery extends BaseQuery implements DateRangeInterface
|
||||
'dateRange' => new DateRange(),
|
||||
'customers' => [],
|
||||
'status' => [],
|
||||
'users' => [],
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ final class SearchHelper
|
||||
$i = 0;
|
||||
$c = 0;
|
||||
$j = 0;
|
||||
$k = 0;
|
||||
foreach ($searchTerm->getParts() as $part) {
|
||||
// we do NOT search for unspecific/global terms as of now, because it is not clear if the user wants that
|
||||
if (($metaName = $part->getField()) === null) {
|
||||
@@ -61,7 +62,7 @@ final class SearchHelper
|
||||
$metaValue = $part->getTerm();
|
||||
$paramName = 'metaName' . $i++;
|
||||
$paramValue = 'metaValue' . $c++;
|
||||
$subqueryName = 'metaNotExists' . $metaName;
|
||||
$subqueryName = 'metaNotExists' . $k++;
|
||||
$field = $alias . '.value';
|
||||
|
||||
$and = $qb->expr()->andX();
|
||||
|
||||
@@ -21,7 +21,10 @@ final class SearchTerm
|
||||
public function __construct(string $searchTerm)
|
||||
{
|
||||
$this->originalTerm = $searchTerm;
|
||||
$terms = explode(' ', $searchTerm);
|
||||
$terms = preg_split('/\s+/', trim($searchTerm), -1, PREG_SPLIT_NO_EMPTY);
|
||||
if (!\is_array($terms)) {
|
||||
$terms = [];
|
||||
}
|
||||
$finalTerm = [];
|
||||
|
||||
foreach ($terms as $term) {
|
||||
|
||||
@@ -92,15 +92,19 @@ mpdf-->
|
||||
{{ invoice['customer.number'] }}
|
||||
{% endif %}
|
||||
|
||||
{% if invoice['query.project.order_number'] is defined and invoice['query.project.order_number'] is not empty %}
|
||||
{% if invoice['project.counter'] == 1 and invoice['project.order_number'] is not empty %}
|
||||
<br>
|
||||
{{ 'orderNumber'|trans }}:
|
||||
{{ invoice['query.project.order_number'] }}
|
||||
{{ invoice['project.order_number'] }}
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
{% if invoice['customer.invoice_text'] is not empty%}
|
||||
<p>{{ invoice['customer.invoice_text']|md2html }}</p>
|
||||
{{ invoice['customer.invoice_text']|md2html }}
|
||||
{% endif %}
|
||||
|
||||
{% if invoice['project.counter'] == 1 and invoice['project.invoice_text'] is not empty %}
|
||||
{{ invoice['project.invoice_text']|md2html }}
|
||||
{% endif %}
|
||||
|
||||
<table class="items">
|
||||
|
||||
@@ -70,6 +70,9 @@ mpdf-->
|
||||
{% if country not in invoice['customer.address'] %}
|
||||
<br> {{ country }}
|
||||
{% endif %}
|
||||
{% if invoice['customer.vat_id'] is not empty %}
|
||||
<br>{{ 'vat_id'|trans }}: {{ invoice['customer.vat_id'] }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{% set classLeft = 'text-left' %}
|
||||
@@ -103,16 +106,10 @@ mpdf-->
|
||||
<td class="{{ classRight }}">{{ invoice['customer.number'] }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if invoice['query.project.order_number'] is defined and invoice['query.project.order_number'] is not empty %}
|
||||
{% if invoice['project.counter'] == 1 and invoice['project.order_number'] is not empty %}
|
||||
<tr>
|
||||
<td class="{{ classLeft }}">{{ 'orderNumber'|trans }}</td>
|
||||
<td class="{{ classRight }}">{{ invoice['query.project.order_number'] }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if invoice['customer.vat_id'] is not empty %}
|
||||
<tr>
|
||||
<td class="{{ classLeft }}">{{ 'vat_id'|trans }}</td>
|
||||
<td class="{{ classRight }}">{{ invoice['customer.vat_id'] }}</td>
|
||||
<td class="{{ classRight }}">{{ invoice['project.order_number'] }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
@@ -120,10 +117,14 @@ mpdf-->
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2 style="margin-top: 60px">{{ invoice['template.title'] }}</h2>
|
||||
<h2 style="margin-top: 60px; margin-bottom: 20px;">{{ invoice['template.title'] }}</h2>
|
||||
|
||||
{% if invoice['customer.invoice_text'] is not empty%}
|
||||
<p>{{ invoice['customer.invoice_text']|md2html }}</p>
|
||||
{{ invoice['customer.invoice_text']|md2html }}
|
||||
{% endif %}
|
||||
|
||||
{% if invoice['project.counter'] == 1 and invoice['project.invoice_text'] is not empty %}
|
||||
{{ invoice['project.invoice_text']|md2html }}
|
||||
{% endif %}
|
||||
|
||||
<table class="items">
|
||||
|
||||
@@ -35,6 +35,7 @@ class InvoiceModelActivityHydratorTest extends TestCase
|
||||
$keys = [
|
||||
'activity.id',
|
||||
'activity.name',
|
||||
'activity.counter',
|
||||
'activity.comment',
|
||||
'activity.number',
|
||||
'activity.invoice_text',
|
||||
|
||||
@@ -33,10 +33,10 @@ class InvoiceModelProjectHydratorTest extends TestCase
|
||||
public function assertModelStructure(array $model): void
|
||||
{
|
||||
$keys = [
|
||||
'project._counter',
|
||||
'project.id',
|
||||
'project.name',
|
||||
'project.comment',
|
||||
'project.counter',
|
||||
'project.number',
|
||||
'project.invoice_text',
|
||||
'project.order_date',
|
||||
|
||||
@@ -215,7 +215,8 @@ class DebugRendererTest extends TestCase
|
||||
'user.meta.hello',
|
||||
'user.meta.kitty',
|
||||
'testFromModelHydrator',
|
||||
'project._counter',
|
||||
'project.counter',
|
||||
'activity.counter',
|
||||
];
|
||||
|
||||
if ($activityCounter === 1) {
|
||||
|
||||
@@ -23,6 +23,7 @@ use Doctrine\ORM\Query\Expr\Orx;
|
||||
use Doctrine\ORM\Query\Parameter;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[CoversClass(SearchHelper::class)]
|
||||
@@ -198,4 +199,57 @@ class SearchHelperTest extends TestCase
|
||||
|
||||
$sut->addSearchTerm($qb, $query);
|
||||
}
|
||||
|
||||
public static function provideMaliciousMetaFieldPayloads(): array
|
||||
{
|
||||
$exploitPayload = "metaNotExists1\tFROM\tApp\\Entity\\User\tmetaNotExists1\tWHERE\tmetaNotExists1.id=1)--";
|
||||
|
||||
return [
|
||||
'dot and parenthesis characters' => ['x.y:~ x):""', ['x.y', 'x)']],
|
||||
'tab and comment injection payloads' => [$exploitPayload . ':~ ' . $exploitPayload . ':""', ['metaNotExists1.id=1)--', 'metaNotExists1.id=1)--']],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Regression test for GHSA-9cxw-hp3c-637x
|
||||
*/
|
||||
#[DataProvider('provideMaliciousMetaFieldPayloads')]
|
||||
public function testMaliciousMetaFieldNamesCannotInfluenceSubqueryDql(string $term, array $expectedMetaNames): void
|
||||
{
|
||||
$em = $this->createMock(EntityManagerInterface::class);
|
||||
$em->method('getExpressionBuilder')->willReturn(new Expr());
|
||||
$qb = new QueryBuilder($em);
|
||||
$qb->from(Timesheet::class, 'testFoo');
|
||||
|
||||
$query = new BaseQuery();
|
||||
$query->setSearchTerm(new SearchTerm($term));
|
||||
$configuration = new SearchConfiguration(['bar'], 'MetaFieldClass', 'metaFieldName');
|
||||
$configuration->setEntityFieldName('entityFieldName');
|
||||
|
||||
$sut = new SearchHelper($configuration);
|
||||
|
||||
$sut->addSearchTerm($qb, $query);
|
||||
|
||||
$wherePart = $qb->getDQLPart('where');
|
||||
self::assertInstanceOf(Andx::class, $wherePart);
|
||||
$where = (string) $wherePart;
|
||||
|
||||
self::assertStringContainsString('metaNotExists0', $where);
|
||||
self::assertStringContainsString('metaNotExists1', $where);
|
||||
self::assertStringNotContainsString('App\\Entity\\User', $where);
|
||||
self::assertStringNotContainsString('--', $where);
|
||||
self::assertStringNotContainsString("\tFROM\t", $where);
|
||||
|
||||
$metaNames = [];
|
||||
/** @var Parameter $parameter */
|
||||
foreach ($qb->getParameters() as $parameter) {
|
||||
if (str_starts_with($parameter->getName(), 'metaName')) {
|
||||
$metaNames[] = $parameter->getValue();
|
||||
}
|
||||
}
|
||||
|
||||
self::assertCount(2, $metaNames);
|
||||
self::assertEquals($expectedMetaNames[0], $metaNames[0]);
|
||||
self::assertEquals($expectedMetaNames[1], $metaNames[1]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,4 +170,39 @@ class SearchTermTest extends TestCase
|
||||
self::assertEquals($expected[2], $part->isExcluded());
|
||||
}
|
||||
}
|
||||
|
||||
public function testWhitespaceIsNormalizedWhenTokenizing(): void
|
||||
{
|
||||
$sut = new SearchTerm(" \tfoo\tbar:baz\nhello world\t ");
|
||||
|
||||
self::assertTrue($sut->hasSearchTerm());
|
||||
self::assertEquals('foo hello world', $sut->getSearchTerm());
|
||||
self::assertEquals(['bar' => 'baz'], $sut->getSearchFields());
|
||||
self::assertEquals(" \tfoo\tbar:baz\nhello world\t ", $sut->getOriginalSearch());
|
||||
self::assertCount(4, $sut->getParts());
|
||||
|
||||
$expectedParts = [
|
||||
['foo', null, false],
|
||||
['baz', 'bar', false],
|
||||
['hello', null, false],
|
||||
['world', null, false],
|
||||
];
|
||||
$i = 0;
|
||||
foreach ($sut->getParts() as $part) {
|
||||
$expected = $expectedParts[$i++];
|
||||
self::assertEquals($expected[0], $part->getTerm());
|
||||
self::assertEquals($expected[1], $part->getField());
|
||||
self::assertEquals($expected[2], $part->isExcluded());
|
||||
}
|
||||
}
|
||||
|
||||
public function testWhitespaceOnlySearchTermCreatesNoParts(): void
|
||||
{
|
||||
$sut = new SearchTerm(" \t \n ");
|
||||
|
||||
self::assertFalse($sut->hasSearchTerm());
|
||||
self::assertEquals('', $sut->getSearchTerm());
|
||||
self::assertEquals([], $sut->getSearchFields());
|
||||
self::assertCount(0, $sut->getParts());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user