diff --git a/AGENTS.md b/.agents/AGENTS.md
similarity index 82%
rename from AGENTS.md
rename to .agents/AGENTS.md
index d8d5f3cc6eb..a1d66837061 100644
--- a/AGENTS.md
+++ b/.agents/AGENTS.md
@@ -15,7 +15,7 @@ Every modification must respect:
- ❌ Do not break compatibility of PHP functions and methods
- ❌ Do not introduce external dependencies without validation
- ❌ Separate page actions in the `/* Actions */` section of the PHP code and the rendering part in the `/* Views */` section
-- ❌ Never commit directly to `main` or `master` branch
+- ❌ Never commit directly to `develop` or version branch matching regex `^\d+\.\d+$`
- ❌ Never use PHP native curl functions to call a GET or POST URL, but use instead the Dolibarr function getURLContent()
- ✅ Use Dolibarr hooks whenever possible
- ✅ Respect existing naming conventions
@@ -53,7 +53,6 @@ Before writing any code, the agent **must**:
- PHP >= 7.3 (minimum support); PHP 8.1+ recommended for new external modules
- ⚠️ When writing a **bug fix**, always target the lowest compatible PHP version
of the branch being patched — do not use PHP 8.x syntax on a fix targeting v19 or v20
-- Use `declare(strict_types=1)` when targeting PHP 8.1+
- Respect PSR-12, but **indentations must use Tabs, not Spaces**
- Write short, readable, and testable functions
- Avoid side effects
@@ -66,8 +65,7 @@ Before writing any code, the agent **must**:
- Use Dolibarr database functions exclusively — never use PDO or MySQLi directly
- In pages: use global `$db`
- In classes: use `$this->db`
-- ✅ Always escape user inputs
-- ✅ SQL forged by PHP must escape fields with `db->escape()`, `db->sanitize()`, or by casting values to `(int)` or `(float)`
+- ✅ SQL forged by PHP must escaped fields with `db->escape()`, `db->sanitize()`, or by casting values to `(int)` or `(float)`
- ✅ Always use `$db->query()` followed by `$db->fetch_object()` or `$db->fetch_array()` to retrieve results
- ✅ SQL scripts for table and index creation must be placed in `htdocs/install/mysql/tables/` (see existing files for examples)
- ❌ Never run SQL queries inside loops (avoid N+1 problem — use JOINs or batch queries instead)
@@ -94,7 +92,8 @@ Before writing any code, the agent **must**:
- Never hardcode user-facing strings — always use `$langs->trans('Key')`
- Language files must be placed in `mymodule/langs/en_US/` (and other locales as needed)
-- Language key names must be in English and PascalCase (e.g., `MyModuleLabel`, not `MonLibellé`)
+- All code comments and variables or functions names must be in English.
+- Language key names must use PascalCase (e.g., `MyModuleLabel`, not `monLibelléModule`)
- Load the language file at the top of the page: `$langs->load('mymodule@mymodule')`
---
@@ -103,12 +102,13 @@ Before writing any code, the agent **must**:
Before any modification, verify:
- Creation / edition / deletion workflows
-- User rights enforcement (`$user->rights->module->action`)
-- Multi-entity compatibility (`$conf->entity`)
+- User rights enforcement (`$user->hasRights("module", "permission")` or `$user->hasRights("module", "objectname", "permission")`)
+- Multi-entity compatibility (add ` AND entity IN ('.getDolEntity("tablename").')`)
If possible:
-- Add a PHPUnit test file in `htdocs/modulebuilder/template/test/phpunit/`
-- Reference the `phpunit.xml` file at the root of the project to run the test suite
+- If doing an external module, add a PHPUnit test file in `yourmoduledir/test/phpunit/`
+- If modifying the Dolibarr code project, add a PHPUnit test file into `test/phpunit/` and add the entry into file `test/phpunit/AllTests.php`.
+
---
@@ -123,8 +123,9 @@ If possible:
## 🔒 Security
-- Always validate inputs (`GET`, `POST`) via `GETPOST()` with a type parameter
-- Prevent SQL injection (use `db->escape()`) and XSS (use `dol_escape_htmltag()`)
+- Always validate user inputs (`GET`, `POST`) via `GETPOST()` with a type parameter
+- Prevent SQL injection (use `db->escape()` or cast into `(int)` or `(float)`)
+- Prevent XSS injection by escaping HTML output (use `dolPrintHTML()`, `dolPrintHTMLForAttribute()`)
- Always include Dolibarr CSRF tokens in POST forms: ``
---
@@ -153,7 +154,7 @@ If possible:
- `develop` branch for both fixes and new features
- ❌ Never commit directly to `main` or `develop` without a reviewed PR
- Commit message format: `TYPE: #issueNumber Short description`
- - Types: `NEW`, `FIX`, `CLOSE`
+ - Types: `NEW`, `FIX` or `CLOSE`
- Example: `FIX: #1234 Correct VAT calculation on credit notes`
- Update the `ChangeLog` file with a summary of significant changes
- When fixing a bug, apply the patch on the **oldest affected branch first**,
@@ -177,6 +178,7 @@ If possible:
- Change the global architecture of existing modules
- Delete code without justification and a comment explaining why
- Add external dependencies (Composer packages, JS libraries) without prior validation
+- Modify the `ChangeLog` file (this file will be updated by the maintainer during the release process)
---
diff --git a/.agents/skills/add-unit-test/SKILL.md b/.agents/skills/add-unit-test/SKILL.md
new file mode 100644
index 00000000000..ffb14fb96ed
--- /dev/null
+++ b/.agents/skills/add-unit-test/SKILL.md
@@ -0,0 +1,95 @@
+---
+name: add-unit-test
+description: Add a unit test to test functions or methods.
+license: MIT
+user-invocable: true
+allowed-tools:
+ - read_file
+ - write_file
+ - grep
+---
+
+
+# Skill: Add a Unit Test for Dolibarr
+
+## When to use this skill
+
+Use this skill whenever the user asks to create, modify, or complete a **PHP unit test** for the Dolibarr ERP/CRM project.
+
+The goal is to produce a unit test that follows Dolibarr conventions and integrates cleanly into the existing PHPUnit test suite.
+
+
+## Inputs
+
+The user request should contain, when available:
+
+- a name of the function to make the unit test for
+- or the method of the class name to make the unit test for
+
+
+## General Rules
+
+- Follow the coding style already used in files into directory test/phpunit/
+- Modify the minimum amount of existing code.
+- Prefer adding new test methods instead of modifying existing ones.
+- Tests must be deterministic and independent.
+- Avoid dependencies on external services.
+- Clean up every object created during the test.
+
+## Test Location
+
+Locate the most appropriate existing PHPUnit test file in test/phpunit.
+
+If no suitable test exists, create one in:
+
+```
+test/phpunit/
+```
+
+using the naming convention:
+
+```
+FeatureTest.php
+```
+
+
+## Naming
+
+A test method should describe the expected behavior.
+
+Examples:
+
+```php
+public function testCreateObject()
+public function testDeleteObject()
+public function testFetchReturnsExpectedValues()
+public function testInvalidInputThrowsException()
+```
+
+## Assertions
+
+Prefer specific assertions.
+
+Good:
+
+```php
+$this->assertTrue($result);
+$this->assertFalse($result);
+$this->assertEquals($expected, $actual);
+$this->assertCount(3, $array);
+$this->assertNull($value);
+$this->assertNotNull($value);
+```
+
+Avoid generic assertions when a more precise one exists.
+
+
+## Output
+
+When generating code:
+
+- provide only the relevant PHP code
+- preserve the existing file formatting
+- do not rewrite unrelated methods
+- explain briefly what is being tested
+
diff --git a/.agents/skills/code-review/SKILL.md b/.agents/skills/code-review/SKILL.md
new file mode 100644
index 00000000000..b82913625c4
--- /dev/null
+++ b/.agents/skills/code-review/SKILL.md
@@ -0,0 +1,51 @@
+---
+name: code-review
+description: Review code and fix bad practices
+license: MIT
+user-invocable: true
+allowed-tools:
+ - read_file
+ - write_file
+ - grep
+---
+
+
+# Skill: Review code for Dolibarr practices and fix it
+
+## When to use this skill
+
+Use this skill whenever the user asks to review code to fix bad practices or to update code to match good practices.
+
+
+## Inputs
+
+The user request should contain, when available:
+
+- a module name
+- or a directory name
+- or a file name
+
+
+## General Rules
+
+- Follow the coding style already used in files in module builder template in htdocs/modulebuilder/templates
+- Modify the minimum amount of existing code.
+
+
+## Rules
+
+- Use PSR-12 coding style except tabulation that must use TAB characters and not SPACES.
+- Remove all spaces at end of lines
+- Rewrite in english all code comments that are not in english
+- Scan files for security vulnerabilities
+
+
+## Output
+
+When generating code:
+
+- provide only the relevant PHP code
+- preserve the existing file formatting
+- do not rewrite unrelated methods
+- explain briefly what is being tested
+
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index 57628ed8ac5..6c8ac975245 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -22,6 +22,8 @@ body:
label: Dolibarr Version
description: Affected Dolibarr version(s)
placeholder: 19.0, develop, ...
+ validations:
+ required: true
- type: input
id: environment-php
@@ -40,6 +42,8 @@ body:
attributes:
label: Steps to reproduce the behavior and expected behavior
description: Verbose description
+ validations:
+ required: true
- type: textarea
id: files
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
index 5d97ee6a5be..5bc0de505fa 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -28,8 +28,3 @@ body:
label: Suggested implementation
description: Verbose description
- - type: textarea
- id: suggested-steps
- attributes:
- label: Suggested steps
- description: List of tasks to achieve goal
diff --git a/.github/scripts/get_changed_php.sh b/.github/scripts/get_changed_php.sh
index 192de878900..43d64101c74 100755
--- a/.github/scripts/get_changed_php.sh
+++ b/.github/scripts/get_changed_php.sh
@@ -1,5 +1,7 @@
#!/bin/bash
-# Copyright (C) 2025 MDW
+# Copyright (C) 2025-2026 MDW
+
+# shellcheck disable=2129,2128,2034,2016
set -euo pipefail
@@ -41,22 +43,33 @@ repo="${GITHUB_REPOSITORY##*/}" # Extract text after the last '/'
page=1
per_page=100
changed_php_files=()
+changed_phan_files=()
changed_lang_files=()
+# Get phan path configuration
+phan_directory_list=$(php -r '$config = require("dev/tools/phan/config.php"); echo "^".implode("|",$config["directory_list"]);')
+phan_exclude_directory=$(php -r '$config = require("dev/tools/phan/config.php"); echo "^".implode("|",$config["exclude_analysis_directory_list"]);')
+
+phan_exclude_file_regex=$(php -r '$config = require("dev/tools/phan/config.php"); echo $config["exclude_file_regex"];')
+phan_exclude_file_regex=${phan_exclude_file_regex#@}
+phan_exclude_file_regex=${phan_exclude_file_regex%@}
+
# Loop through all pages to gather changed files
while true; do
response=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" \
"https://api.github.com/repos/${owner}/${repo}/pulls/${pr_number}/files?per_page=${per_page}&page=${page}")
- phan_exclude_file_regex=$(php -r '$config = require("dev/tools/phan/config.php"); echo $config["exclude_file_regex"];')
# Filter for files ending with .php and add them to the list
- mapfile -t files < <(echo "$response" | jq -r '.[] | select((.filename | test("\\.php$")) and (.filename | test("^dev/") | not)) | .filename' | grep -vP "$phan_exclude_file_regex")
+ mapfile -t files < <(echo "$response" | jq -r '.[] | select((.filename | test("\\.php$")) and (.filename | test("^dev/") | not)) | .filename')
changed_php_files+=("${files[@]}")
+ mapfile -t files < <(echo "$response" | jq -r '.[] | select((.filename | test("\\.php$")) and (.filename | test("'"$phan_directory_list"'"))) | .filename' | grep -vP "$phan_exclude_file_regex")
+ changed_phan_files+=("${files[@]}")
+
mapfile -t files < <(echo "$response" | jq -r '.[] | select(.filename | test("\\.lang$")) | .filename')
- changed_lang_files+=("${files[@]}")
+ changed_lang_files+=("${files[@]}")
# Check if we have reached the last page (less than per_page results)
count=$(echo "$response" | jq 'length')
@@ -69,10 +82,12 @@ done
# Build a space-separated string of changed PHP and lang files
# This does not cope with files that have spaces.
+
# But such files do not exist in the project (at least not for the
# files we are filtering).
all_changed_files=$(IFS=" " ; echo "${changed_php_files[*]}")
all_changed_lang=$(IFS=" " ; echo "${changed_lang_files[*]}")
+phan_changed_files=$(IFS=" " ; echo "${changed_phan_files[*]}")
forbidden_files=""
@@ -85,21 +100,31 @@ forbidden_files=""
#fi
-# Determine changed files flag
-if [ -z "$all_changed_files" ]; then
+# Determine changed files flags
+if [ -z "${all_changed_files}" ]; then
any_changed="false"
else
any_changed="true"
fi
+if [ -z "${phan_changed_files}" ]; then
+ phan_changed="false"
+else
+ phan_changed="true"
+fi
+
# Set outputs for GitHub Actions if GITHUB_OUTPUT is available
if [ -n "${GITHUB_OUTPUT:-}" ]; then
echo "any_changed=${any_changed}" >> "$GITHUB_OUTPUT"
+ echo "phan_changed=${phan_changed}" >> "$GITHUB_OUTPUT"
echo "all_changed_files=${all_changed_files}" >> "$GITHUB_OUTPUT"
+ echo "phan_changed_files=${phan_changed_files[*]}" >> "$GITHUB_OUTPUT"
echo "forbidden_files=${forbidden_files}" >> "$GITHUB_OUTPUT"
else
# Otherwise, print the outputs
echo "any_changed=${any_changed}"
+ echo "phan_changed=${phan_changed}"
+ echo "phan_changed_files=${phan_changed_files[*]}"
echo "all_changed_files=${all_changed_files}"
echo "forbidden_files=${forbidden_files}"
fi
diff --git a/.github/workflows/phan.yml b/.github/workflows/phan.yml
index 3547d3e1df5..9acdbfa17b7 100644
--- a/.github/workflows/phan.yml
+++ b/.github/workflows/phan.yml
@@ -61,9 +61,10 @@ jobs:
# - the merge from branch contains 'phan_full', or,
# - there are changes in PHP files.
# Note: --output-mode=github does not provide file:line, so using checkstyle and cs2pr
- if: ${{ ! cancelled() && (github.ref_name == 'develop' || github.ref_name == 'refs/heads/develop' || endsWith(github.ref_name, '.0') || contains(github.head_ref, 'phan_full') || steps.changed-php.outputs.any_changed == 'true') }}
+ if: ${{ ! cancelled() && (github.ref_name == 'develop' || github.ref_name == 'refs/heads/develop' || endsWith(github.ref_name, '.0') || contains(github.head_ref, 'phan_full') || steps.changed-php.outputs.phan_changed == 'true') }}
env:
ALL_CHANGED_FILES: ${{ steps.changed-php.outputs.all_changed_files }}
+ PHAN_CHANGED_FILES: ${{ steps.changed-php.outputs.phan_changed_files }}
# shellcheck disable=2086
FILE_CHANGED_LIST: /tmp/phan-changed.lst
run: |
@@ -73,7 +74,7 @@ jobs:
phan $PHAN_QUICK -k "$PHAN_CONFIG" -B "$PHAN_BASELINE" --analyze-twice --minimum-target-php-version "$PHAN_MIN_PHP" --output-mode=checkstyle -o _phan.xml
else
echo -n "" > "$FILE_CHANGED_LIST"
- for f in $ALL_CHANGED_FILES; do echo "$f" >> "$FILE_CHANGED_LIST"; done
+ for f in $PHAN_CHANGED_FILES; do echo "$f" >> "$FILE_CHANGED_LIST"; done
# Must exclude same files as in phan configuration
grep -v -E '^htdocs/(custom/|.*/canvas/.*/tpl/.*.tpl.php|admin/tools/ui/|includes/(nusoap/|restler/|stripe/)|conf/conf.php)$' "$FILE_CHANGED_LIST"> "$FILE_CHANGED_LIST".tmp || true
mv "$FILE_CHANGED_LIST".tmp "$FILE_CHANGED_LIST"
diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml
index 6b788114546..b597b678c68 100644
--- a/.github/workflows/phpstan.yml
+++ b/.github/workflows/phpstan.yml
@@ -21,6 +21,7 @@ env:
gh_event: ${{ inputs.gh_event || github.event_name }}
CACHE_KEY_PART: ${{ ( inputs.gh_event == 'pull_request' || github.event_name == 'pull_request' ) && format('{0}-{1}', github.base_ref, github.head_ref) || github.ref_name }}
GITHUB_JSON: ${{ toJSON(github) }} # Helps in debugging Github Action
+ DUMMY_FILE: htdocs/theme/md/progress.inc.php
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job
@@ -113,7 +114,7 @@ jobs:
fi
echo phpstan analyse --no-progress --error-format=checkstyle --memory-limit 7G -a dev/build/phpstan/bootstrap_action.php ${ALL_CHANGED_FILES}
- phpstan analyse --no-progress --error-format=checkstyle --memory-limit 7G -a dev/build/phpstan/bootstrap_action.php ${ALL_CHANGED_FILES} | tee _stan.xml | cs2pr --graceful-warnings
+ phpstan analyse --no-progress --error-format=checkstyle --memory-limit 7G -a dev/build/phpstan/bootstrap_action.php "${{ env.DUMMY_FILE }}" ${ALL_CHANGED_FILES} | tee _stan.xml | cs2pr --graceful-warnings
fi
# continue-on-error: true
diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml
index 8098dc970c9..3ab7757a732 100644
--- a/.github/workflows/pre-commit.yml
+++ b/.github/workflows/pre-commit.yml
@@ -157,7 +157,7 @@ jobs:
ls -l ~/.cache/pre-commit/
- name: Convert Raw Log to Annotations
- uses: mdeweerd/logToCheckStyle@v2025.11.2
+ uses: mdeweerd/logToCheckStyle@v2026.7.1
if: ${{ failure() }}
with:
in: ${{ env.RAW_LOG }}
diff --git a/.github/workflows/windows-ci.yml b/.github/workflows/windows-ci.yml
index 5215886e9ab..a1a42ceecaa 100644
--- a/.github/workflows/windows-ci.yml
+++ b/.github/workflows/windows-ci.yml
@@ -192,7 +192,7 @@ jobs:
for /f "tokens=2 delims==" %%A in ('doskey /m:err') do EXIT /B %%A
- name: Convert Raw Log to Annotations
- uses: mdeweerd/logToCheckStyle@v2025.11.2
+ uses: mdeweerd/logToCheckStyle@v2026.7.1
if: ${{ failure() }}
with:
in: ${{ env.PHPUNIT_LOG }}
diff --git a/.mailmap b/.mailmap
index 7fb0a8c7f30..32d2219ba0d 100644
--- a/.mailmap
+++ b/.mailmap
@@ -4,11 +4,13 @@
#
# This files allows to have several profiles (the second part) matched to the same name/email (the first part).
#
-Laurent Destailleur Laurent Destailleur
-Laurent Destailleur eldy
-Laurent Destailleur Laurent Destailleur
-Laurent Destailleur eldy10
-Laurent Destailleur Laurent Destailleur
+Laurent Destailleur Laurent Destailleur
+Laurent Destailleur eldy
+Laurent Destailleur Laurent Destailleur
+Laurent Destailleur eldy10
+Laurent Destailleur Laurent Destailleur
+Laurent Destailleur Laurent Destailleur
+Laurent Destailleur Laurent Destailleur
Regis Houssin Regis Houssin
Regis Houssin Régis Houssin
Juanjo Menent simnandez
@@ -18,12 +20,15 @@ Juanjo Menent juanjo
Juanjo Menent juanjo
Marcos García de La Fuente Marcos García
Marcos García de La Fuente Marcos García
+Mohamed DAOUD Mohamed DAOUD
+Mohamed DAOUD Mohamed DAOUD
Florian Henry fhenry
Florian Henry Florian HENRY
Florian Henry FHenry
Florian Henry Florian HENRY
Florian Henry FHenry
Florian Henry HENRY Florian
+Lucas Marcouiller Hystepik
Alexandre Spangaro aspangaro
Alexandre Spangaro Spangaro Alexandre
Alexandre Spangaro Alexandre SPANGARO
diff --git a/ChangeLog b/ChangeLog
index 33c35923532..0ba4d32234f 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -159,7 +159,6 @@ NEW: Add mass action to cancel invoices using credit notes (#37852)
NEW: Add personal_data flag in extrafields admin UI - next step of #8612 (#38133)
NEW: Direct installation for community and free modules on DoliStore (#38357)
NEW: Display thirdparty name with ref in supplier orders linked objects (#36952)
-NEW: Show bom net needs treeview by line position. (#37459)
NEW: Show name_alias in parentheses in thirdparty dropdown lists (#37997)
NEW: Thirdparty card +tab with events where thirdparty is attendee/booth (#37743)
NEW: Update ticket read date when status changes (#37470)
@@ -178,7 +177,6 @@ For developers:
NEW: Use another hash algorithm v2 based on sha256/hmac for immutable logs (#37725)
NEW: Replace MyObject MyModule occurrences (#38370)
NEW: ModuleBuilder - Allow selecting which optional tabs (contact, note, document, agenda) are generated for an object (#38570)
-FIX: ModuleBuilder - Generated list page derives the email trackid from the object name instead of the leftover 'xxxx' placeholder (#32460)
NEW: add hooks in reception card (#37214)
NEW: add new hook in BonPrelevement::EnregDestinataireSEPA() function (#37419)
NEW: Add hook selectForFormsListUrl in Form::selectForForms (#37447)
@@ -243,6 +241,7 @@ FIX: missing "blob:" in the assistant for CSP editor.
FIX: option MEMBER_SEARCH_MEMBER_PUBLIC_FORM_CREATE
FIX: product price.php: preserve default_vat_code and tva_npr when auto-creating initial product_price row (#38034)
FIX: propagate fk_warehouse from BOM/MO to production lines in createProduction() and processBOM() (#38147)
+FIX: #39068 Product search combo: ignore supplier price warning text when filtering
FIX: selected default value ko on select_produits_fournisseurs_list()
FIX: Set default warehouse on order create. (#37815)
FIX: Site root missing in $backtopage. (#37804)
diff --git a/dev/build/generate_filelist_xml.php b/dev/build/generate_filelist_xml.php
index 7c1fb1d5a30..f73bdadd47b 100755
--- a/dev/build/generate_filelist_xml.php
+++ b/dev/build/generate_filelist_xml.php
@@ -115,7 +115,7 @@ while ($i < $argc) {
$includeconstants[$tmp[0]][$tmp[1]] = $tmp[2];
}
if (!empty($result["buildzip"])) {
- $buildzip = 1;
+ $buildzip = $result["buildzip"];
}
$i++;
}
@@ -134,7 +134,7 @@ $savrelease = $release;
$tmpver = explode('-', $release, 2);
if ($tmpver[0] == 'auto') {
$release = DOL_VERSION;
- if (!empty($tmpver[1]) && $tmpver[0] == 'auto') {
+ if (!empty($tmpver[1])) {
$release .= '-'.$tmpver[1];
}
}
@@ -419,12 +419,12 @@ foreach ($arrayofunalterablefiles as $entry) {
exit(1);
}
if ($newdir != $dir) {
- if ($needtoclose) {
- if ($release) {
- fputs($fp, ' '."\n");
- }
- $needtoclose = 0;
+ //if ($needtoclose) {
+ if ($release) {
+ fputs($fp, ' '."\n");
}
+ $needtoclose = 0;
+ //}
if ($release) {
fputs($fp, ' '."\n");
}
diff --git a/dev/build/makepack-dolibarr.php b/dev/build/makepack-dolibarr.php
index 026b0235140..7ebbd98fbe0 100755
--- a/dev/build/makepack-dolibarr.php
+++ b/dev/build/makepack-dolibarr.php
@@ -230,6 +230,8 @@ if (preg_match("/define\('DOL_VERSION',\s*'([\d\.a-z\-]+)'\)/i", $filefuncConten
$PROJVERSION = $matches[1];
}
if (empty($PROJVERSION)) {
+ $DOL_MAJOR_VERSION = 'notfound';
+ $DOL_MINOR_VERSION = 'notfound';
if (preg_match("/define\('DOL_MAJOR_VERSION',\s*'([\d\.a-z\-]+)'\)/i", $filefuncContent, $matches)) {
$DOL_MAJOR_VERSION = $matches[1];
}
@@ -269,7 +271,7 @@ if (strpos($newbuild, '-') === false) {
$newbuild .= '-0.4'; // finale (fedora)
}
$REL1 = preg_replace('/-.*$/', '', $newbuild);
-if ($RPMSUBVERSION === 'auto') {
+if ($RPMSUBVERSION === 'auto') { // @phpstan-ignore-line
$RPMSUBVERSION = preg_replace('/^.*-/', '', $newbuild);
}
$FILENAMETGZ2 = "$PROJECT-$MAJOR.$MINOR.$REL1";
@@ -314,10 +316,10 @@ for ($i = 1; $i < $argc; $i++) {
}
// Force output dir if env vars are defined
-if ($ENVDESTIBETARC && preg_match('/[a-z]/i', $BUILD)) {
+if ($ENVDESTIBETARC && preg_match('/[a-z]/i', $BUILD)) { // @phpstan-ignore-line
$DESTI = $ENVDESTIBETARC;
}
-if ($ENVDESTISTABLE && preg_match('/^[0-9]+$/', $BUILD)) {
+if ($ENVDESTISTABLE && preg_match('/^[0-9]+$/', $BUILD)) { // @phpstan-ignore-line
$DESTI = $ENVDESTISTABLE;
}
@@ -349,12 +351,12 @@ if ($target) {
$targetUpper = strtoupper($target);
if ($targetUpper === 'ALL') {
foreach ($LISTETARGET as $key) {
- if ($key !== 'SNAPSHOT' && $key !== 'SF' && $key !== 'ASSO') {
+ if ($key !== 'SNAPSHOT' && $key !== 'SF' && $key !== 'ASSO') { // @phpstan-ignore-line
$CHOOSEDTARGET[$key] = 1;
}
}
}
- if ($targetUpper !== 'ALL' && $targetUpper !== 'SF' && $targetUpper !== 'ASSO') {
+ if ($targetUpper !== 'ALL' && $targetUpper !== 'SF' && $targetUpper !== 'ASSO') { // @phpstan-ignore-line
$CHOOSEDTARGET[$targetUpper] = 1;
}
if ($targetUpper === 'SF') {
@@ -399,7 +401,7 @@ if ($target) {
} elseif ($NUM_SCRIPT === '0') {
$CHOOSEDTARGET['-CHKSUM'] = 1;
foreach ($LISTETARGET as $key) {
- if ($key !== 'SNAPSHOT' && $key !== 'ASSO' && $key !== 'SF') {
+ if ($key !== 'SNAPSHOT' && $key !== 'ASSO' && $key !== 'SF') { // @phpstan-ignore-line
$CHOOSEDTARGET[$key] = 1;
}
}
@@ -495,7 +497,7 @@ foreach ($CHOOSEDTARGET as $tgt => $val) {
ksort($CHOOSEDPUBLISH);
foreach ($CHOOSEDPUBLISH as $tgt => $val) {
- if ($val < 0) { continue; }
+ if ($val < 0) { continue; } // @phpstan-ignore-line
if ($tgt === 'ASSO') { $nbofpublishneedchangelog++; }
if ($tgt === 'SF') { $nbofpublishneedchangelog++; $nbofpublishneedtag++; }
$nboftargetok++;
@@ -621,7 +623,7 @@ if ($nboftargetok) {
// ========================================================================
if ($nboftargetneedbuildroot) {
- if (!$copyalreadydone) {
+ if (!$copyalreadydone) { // @phpstan-ignore-line
echo "Creation of a buildroot used for all packages\n";
echo "Delete directory $BUILDROOT\n";
@@ -1297,7 +1299,7 @@ if ($nboftargetok) {
ksort($CHOOSEDPUBLISH);
foreach ($CHOOSEDPUBLISH as $tgt => $val) {
- if ($val < 0) { continue; }
+ if ($val < 0) { continue; } // @phpstan-ignore-line
echo "\nList of files to publish (BUILD=$BUILD)\n";
diff --git a/dev/build/makepack-howto.md b/dev/build/makepack-howto.md
index 8eca0e0807d..1cee5b9190d 100644
--- a/dev/build/makepack-howto.md
+++ b/dev/build/makepack-howto.md
@@ -53,7 +53,7 @@ Prerequisites to build autoexe DoliWamp package from Windows:
```
-## Actions to do a BETA
+## Actions to do a FREEZE / BETA
This section describes steps made by Dolibarr packaging team to make a beta version of Dolibarr, step by step.
@@ -84,7 +84,7 @@ git log x.y.z-1.. --no-merges --pretty=short --oneline | sed -e "s/^[0-9a-z]* //
- Recopy the content of the output file into the file ChangeLog.
- Note: To know number of lines changes: git diff --shortstat A B
+ Note: To know number of changed lines: git diff --shortstat A B
- Update version number with x.y.z-w in file htdocs/filefunc.inc.php
diff --git a/dev/build/phpstan/phpstan-baseline.neon b/dev/build/phpstan/phpstan-baseline.neon
index 0968e6da4ca..7ab66d19e39 100644
--- a/dev/build/phpstan/phpstan-baseline.neon
+++ b/dev/build/phpstan/phpstan-baseline.neon
@@ -1230,12 +1230,6 @@ parameters:
count: 1
path: ../../../htdocs/comm/action/index.php
- -
- message: '#^Parameter \#16 \$usergroupid of function print_actions_filter expects array\\|int, string given\.$#'
- identifier: argument.type
- count: 1
- path: ../../../htdocs/comm/action/index.php
-
-
message: '#^Ternary operator condition is always true\.$#'
identifier: ternary.alwaysTrue
@@ -3216,12 +3210,6 @@ parameters:
count: 1
path: ../../../htdocs/core/class/openid.class.php
- -
- message: '#^Parameter \#1 \$content of method SimpleOpenID\:\:HTML2OpenIDServer\(\) expects string, array\ given\.$#'
- identifier: argument.type
- count: 1
- path: ../../../htdocs/core/class/openid.class.php
-
-
message: '#^Call to function is_object\(\) with XMLParser will always evaluate to true\.$#'
identifier: function.alreadyNarrowedType
@@ -5088,12 +5076,6 @@ parameters:
count: 1
path: ../../../htdocs/fourn/paiement/card.php
- -
- message: '#^Variable \$object might not be defined\.$#'
- identifier: variable.undefined
- count: 2
- path: ../../../htdocs/fourn/paiement/document.php
-
-
message: '#^Ternary operator condition is always false\.$#'
identifier: ternary.alwaysFalse
@@ -5640,12 +5622,6 @@ parameters:
count: 1
path: ../../../htdocs/loan/class/loanschedule.class.php
- -
- message: '#^Variable \$object might not be defined\.$#'
- identifier: variable.undefined
- count: 1
- path: ../../../htdocs/loan/payment/card.php
-
-
message: '#^Negated boolean expression is always true\.$#'
identifier: booleanNot.alwaysTrue
@@ -6222,12 +6198,6 @@ parameters:
count: 4
path: ../../../htdocs/multicurrency/multicurrency_rate.php
- -
- message: '#^Variable \$id might not be defined\.$#'
- identifier: variable.undefined
- count: 1
- path: ../../../htdocs/opensurvey/card.php
-
-
message: '#^Left side of && is always true\.$#'
identifier: booleanAnd.leftAlwaysTrue
@@ -6252,12 +6222,6 @@ parameters:
count: 1
path: ../../../htdocs/opensurvey/results.php
- -
- message: '#^Variable \$expiredate might not be defined\.$#'
- identifier: variable.undefined
- count: 1
- path: ../../../htdocs/opensurvey/results.php
-
-
message: '#^Left side of && is always false\.$#'
identifier: booleanAnd.leftAlwaysFalse
@@ -6378,12 +6342,6 @@ parameters:
count: 1
path: ../../../htdocs/paypal/lib/paypal.lib.php
- -
- message: '#^Parameter \#3 \$value of function curl_setopt expects 0\|2, bool given\.$#'
- identifier: argument.type
- count: 1
- path: ../../../htdocs/paypal/lib/paypal.lib.php
-
-
message: '#^Parameter \#3 \$value of function curl_setopt expects bool, int given\.$#'
identifier: argument.type
@@ -6798,12 +6756,6 @@ parameters:
count: 1
path: ../../../htdocs/product/stock/stocktransfer/stocktransfer_card.php
- -
- message: '#^Variable \$result might not be defined\.$#'
- identifier: variable.undefined
- count: 1
- path: ../../../htdocs/product/stock/stocktransfer/stocktransfer_contact.php
-
-
message: '#^Right side of && is always true\.$#'
identifier: booleanAnd.rightAlwaysTrue
@@ -6894,12 +6846,6 @@ parameters:
count: 1
path: ../../../htdocs/projet/card.php
- -
- message: '#^Variable \$morecss might not be defined\.$#'
- identifier: variable.undefined
- count: 1
- path: ../../../htdocs/projet/card.php
-
-
message: '#^Call to function method_exists\(\) with \$this\(Project\) and ''getLibStatut'' will always evaluate to true\.$#'
identifier: function.alreadyNarrowedType
@@ -7476,18 +7422,6 @@ parameters:
count: 1
path: ../../../htdocs/public/onlinesign/newonlinesign.php
- -
- message: '#^Result of && is always false\.$#'
- identifier: booleanAnd.alwaysFalse
- count: 1
- path: ../../../htdocs/public/onlinesign/newonlinesign.php
-
- -
- message: '#^Variable \$mesg might not be defined\.$#'
- identifier: variable.undefined
- count: 2
- path: ../../../htdocs/public/onlinesign/newonlinesign.php
-
-
message: '#^Left side of && is always true\.$#'
identifier: booleanAnd.leftAlwaysTrue
@@ -7944,12 +7878,6 @@ parameters:
count: 1
path: ../../../htdocs/reception/card.php
- -
- message: '#^Variable \$reception might not be defined\.$#'
- identifier: variable.undefined
- count: 1
- path: ../../../htdocs/reception/card.php
-
-
message: '#^Call to function method_exists\(\) with \$this\(Reception\) and ''getLibStatut'' will always evaluate to true\.$#'
identifier: function.alreadyNarrowedType
@@ -8238,24 +8166,6 @@ parameters:
count: 1
path: ../../../htdocs/societe/canvas/company/tpl/card_view.tpl.php
- -
- message: '#^Variable \$objcanvas might not be defined\.$#'
- identifier: variable.undefined
- count: 1
- path: ../../../htdocs/societe/canvas/company/tpl/card_view.tpl.php
-
- -
- message: '#^Variable \$objcanvas might not be defined\.$#'
- identifier: variable.undefined
- count: 1
- path: ../../../htdocs/societe/canvas/individual/tpl/card_view.tpl.php
-
- -
- message: '#^Variable \$socid might not be defined\.$#'
- identifier: variable.undefined
- count: 3
- path: ../../../htdocs/societe/canvas/individual/tpl/card_view.tpl.php
-
-
message: '#^Call to function is_numeric\(\) with 0\|1\|2\|3\|4\|5\|6\|7\|8\|9 will always evaluate to true\.$#'
identifier: function.alreadyNarrowedType
diff --git a/dev/dolibarr_changes.txt b/dev/dolibarr_changes.txt
index 4aec331a2e7..6d590dd7563 100644
--- a/dev/dolibarr_changes.txt
+++ b/dev/dolibarr_changes.txt
@@ -467,3 +467,13 @@ Edit CSS to restore line removed between 4.0.5 and 4.0.6. It generates this bug:
JS JCROP:
----------
* Remove analytics tag into file index.html
+
+
+PHP MIKE42 ESCPOS:
+------------------
+* In htdocs/includes/mike42/escpos-php/src/Mike42/Escpos/GdEscposImage.php
+Replace:
+ if (!is_resource($im)) {
+With:
+ if (!($im instanceof \GdImage) && !is_resource($im)) {
+PHP 8.0+ returns GdImage objects from imagecreate*; the upstream guard with is_resource() alone fails on PHP 8 and throws 'Failed to load image' on receipts that contain an image.
diff --git a/dev/setup/codesniffer/Sniffs/Dolibarr/CheckIsModEnabledArgumentSniff.php b/dev/setup/codesniffer/Sniffs/Dolibarr/CheckIsModEnabledArgumentSniff.php
index 0f5c435a9a8..28d260f60e2 100644
--- a/dev/setup/codesniffer/Sniffs/Dolibarr/CheckIsModEnabledArgumentSniff.php
+++ b/dev/setup/codesniffer/Sniffs/Dolibarr/CheckIsModEnabledArgumentSniff.php
@@ -1,5 +1,6 @@
+ * Copyright (C) 2026 MDW
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -25,7 +26,7 @@ use PHP_CodeSniffer\Files\File;
*/
class CheckIsModEnabledArgumentSniff implements Sniff
{
- // Nom de la fonction cible
+ // Target function name
protected $targetFunction = 'ismodenabled';
protected $deprecatedModulesNames = [
@@ -87,7 +88,7 @@ class CheckIsModEnabledArgumentSniff implements Sniff
return;
}
- // check value of argument
+ // Check argument value
$argContent = str_replace(["'", '"'], '', $tokens[$firstArgTokenPtr]['content']);
$argCode = $tokens[$firstArgTokenPtr]['code'];
diff --git a/dev/setup/codesniffer/Sniffs/Dolibarr/LanguageOfCommentsSniff.php b/dev/setup/codesniffer/Sniffs/Dolibarr/LanguageOfCommentsSniff.php
index 2b656adad0b..1fedd27091d 100644
--- a/dev/setup/codesniffer/Sniffs/Dolibarr/LanguageOfCommentsSniff.php
+++ b/dev/setup/codesniffer/Sniffs/Dolibarr/LanguageOfCommentsSniff.php
@@ -1,5 +1,6 @@
+ * Copyright (C) 2026 MDW
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -14,6 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
+
namespace codesniffer\Sniffs\Dolibarr;
use PHP_CodeSniffer\Sniffs\Sniff;
@@ -108,7 +110,14 @@ class LanguageOfCommentsSniff implements Sniff
return;
}
- if (strpos($content, 'Copyright') === false && preg_match('/[àâäéèêëîïôùûüçœÀÂÄÉÈÊËÎÏÔÙÛÜÇŒ]/u', $content)) {
+ // Avoid matching special characters directly preceded or followed with []'"
+ // Avoid matching special characters in a "word/identifier" like string with '_'.
+ // Implementation:
+ // - Start with word (\b);
+ // - Require that this word does not contain '_'
+ // - Match all word characters up to the accented character.
+ // - Require that the character is not preceded or following by specific delimieters
+ if (strpos($content, 'Copyright') === false && preg_match('/\b(?![\p{L}_]*_[\p{L}_]*\b)([\p{L}_]*)(?\/\'"\[])[àâäéèêëîïôùûüçœÀÂÄÉÈÊËÎÏÔÙÛÜÇŒ]+(?![\'"\[]\/)(?![<>\/\'"\[])/u', $content)) {
$error = "The comment appears to be in French (accent detected in: '%s'). Please write in English.";
$data = [trim($content)];
$phpcsFile->addWarning($error, $stackPtr, 'FrenchDetected', $data);
diff --git a/dev/tools/codespell/codespell-lines-ignore.txt b/dev/tools/codespell/codespell-lines-ignore.txt
index 79f375d950a..3c99a539b3b 100644
--- a/dev/tools/codespell/codespell-lines-ignore.txt
+++ b/dev/tools/codespell/codespell-lines-ignore.txt
@@ -1,16 +1,16 @@
$objMod->dictionaries = $objMod->{"dictionnaries"}; // For backward compatibility
if (empty($objMod->dictionaries) && !empty($objMod->{"dictionnaries"})) {
print 'id.'&id_entrepot='.$entrepotstatic->id.'&action=transfert&pdluoid='.$pdluo->id.'&token='.newToken().'">';
+ $dateemail = dol_stringtotime((string) $overview[0]->udate, 'gmt');
$reponsesadd = str_split($obj->reponses);
$sql .= " SET reponses = '".$db->escape($reponsesadd)."'";
$sql .= " SET reponses = '0".$db->escape($obj->reponses)."'";
$tmpfiles = dol_dir_list($tmpdir, 'files', 0, '\.od(s|t)$', '', 'name', SORT_ASC, 0);
+ dol_syslog("msgid=".$overview[0]->message_id." date=".dol_print_date($overview[0]->udate, 'dayrfc', 'gmt')." from=".$overview[0]->from." to=".$overview[0]->to." subject=".$overview[0]->subject);
print '
';
@@ -583,7 +583,7 @@ if (getDolGlobalString('SHOW_TOTAL_OF_PREVIOUS_LISTS_IN_LIN_PAGE')) { // This pa
if ($j > 12) {
$j -= 12;
}
- $sql .= " SUM(".$db->ifsql("MONTH(ff.datef) = ".((int) $j), "ffd.total_ht", "0").") AS month".str_pad((string) $j, 2, "0", STR_PAD_LEFT).",";
+ $sql .= " SUM(".$db->ifsql("MONTH(ff.datef) = ".((int) $j), "ffd.total_ht", "0").") AS month".str_pad((string) ((int) $j), 2, "0", STR_PAD_LEFT).",";
}
$sql .= " SUM(ffd.total_ht) as total";
$sql .= " FROM ".$db->prefix()."facture_fourn_det as ffd";
diff --git a/htdocs/accountancy/supplier/list.php b/htdocs/accountancy/supplier/list.php
index 62c1fdb167a..1fc9d489260 100644
--- a/htdocs/accountancy/supplier/list.php
+++ b/htdocs/accountancy/supplier/list.php
@@ -7,7 +7,7 @@
* Copyright (C) 2014 Juanjo Menent s
* Copyright (C) 2016 Laurent Destailleur
* Copyright (C) 2024 Frédéric France
- * Copyright (C) 2024-2025 MDW
+ * Copyright (C) 2024-2026 MDW
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -313,10 +313,10 @@ if (getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
}
$alias_societe_perentity = !getDolGlobalString('MAIN_COMPANY_PERENTITY_SHARED') ? "s" : "spe";
$alias_product_perentity = !getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED') ? "p" : "ppe";
-$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."accounting_account as aa ON " . $alias_product_perentity . ".accountancy_code_buy = aa.account_number AND aa.active = 1 AND aa.fk_pcg_version = '".$db->escape($chartaccountcode)."' AND aa.entity = ".$conf->entity;
-$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."accounting_account as aa2 ON " . $alias_product_perentity . ".accountancy_code_buy_intra = aa2.account_number AND aa2.active = 1 AND aa2.fk_pcg_version = '".$db->escape($chartaccountcode)."' AND aa2.entity = ".$conf->entity;
-$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."accounting_account as aa3 ON " . $alias_product_perentity . ".accountancy_code_buy_export = aa3.account_number AND aa3.active = 1 AND aa3.fk_pcg_version = '".$db->escape($chartaccountcode)."' AND aa3.entity = ".$conf->entity;
-$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."accounting_account as aa4 ON " . $alias_societe_perentity . ".accountancy_code_buy = aa4.account_number AND aa4.active = 1 AND aa4.fk_pcg_version = '".$db->escape($chartaccountcode)."' AND aa4.entity = ".$conf->entity;
+$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."accounting_account as aa ON " . $alias_product_perentity . ".accountancy_code_buy = aa.account_number AND aa.active = 1 AND aa.fk_pcg_version = '".$db->escape($chartaccountcode)."' AND aa.entity = ".((int) $conf->entity);
+$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."accounting_account as aa2 ON " . $alias_product_perentity . ".accountancy_code_buy_intra = aa2.account_number AND aa2.active = 1 AND aa2.fk_pcg_version = '".$db->escape($chartaccountcode)."' AND aa2.entity = ".((int) $conf->entity);
+$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."accounting_account as aa3 ON " . $alias_product_perentity . ".accountancy_code_buy_export = aa3.account_number AND aa3.active = 1 AND aa3.fk_pcg_version = '".$db->escape($chartaccountcode)."' AND aa3.entity = ".((int) $conf->entity);
+$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."accounting_account as aa4 ON " . $alias_societe_perentity . ".accountancy_code_buy = aa4.account_number AND aa4.active = 1 AND aa4.fk_pcg_version = '".$db->escape($chartaccountcode)."' AND aa4.entity = ".((int) $conf->entity);
// Add table from hooks
$parameters = array();
$reshook = $hookmanager->executeHooks('printFieldListFrom', $parameters, $object, $action); // Note that $action and $object may have been modified by hook
@@ -836,7 +836,8 @@ if ($result) {
if ($conf->main_checkbox_left_column) {
print '
\n";
$out .= "\n";
if ($genallowed) {
@@ -1403,7 +1407,7 @@ class FormFile
// Show title of list of existing files
$morehtmlright = '';
if (!empty($moreoptions['showhideaddbutton']) && $conf->use_javascript_ajax) {
- $tmpurlforbutton = 'javascript:console.log("open add file form");jQuery(".divattachnewfile").removeClass("hidden").toggle(); if (!jQuery(".divattachnewfile").is(":hidden")) { jQuery(".divattachnewfile input[type=\'file\']").first().click();} void(0);'; // scope file picker click to the just-shown form to avoid triggering native file dialog twice when other input[type=file] exist on the page; removeClass keeps the toggle effective when a theme stylesheet ships a `.hidden { display: none !important }` rule (see #34780, #36782)
+ $tmpurlforbutton = 'javascript:console.log("open add file form"); if (jQuery(".divattachnewfile").is(":hidden")) { jQuery(".divattachnewfile").removeClass("hidden"); jQuery(".divattachnewfile input[type=\'file\']").first().click(); } else { jQuery(".divattachnewfile").addClass("hidden"); } void(0);';
$morehtmlright .= dolGetButtonTitle($langs->trans('New'), '', 'fa fa-plus-circle', $tmpurlforbutton, '', $permtoeditline);
}
@@ -2282,7 +2286,7 @@ class FormFile
$morehtmlright = '';
if (!empty($moreoptions['showhideaddbutton']) && $conf->use_javascript_ajax) {
- $morehtmlright .= dolGetButtonTitle($langs->trans('New'), '', 'fa fa-plus-circle', 'javascript:console.log("open addlink form"); jQuery(".divlinkfile").toggle(); void(0);', '', $permissiontoedit);
+ $morehtmlright .= dolGetButtonTitle($langs->trans('New'), '', 'fa fa-plus-circle', 'javascript:console.log("open addlink form"); if (jQuery(".divlinkfile").is(":hidden")) { jQuery(".divlinkfile").removeClass("hidden"); } else { jQuery(".divlinkfile").addClass("hidden"); } void(0);', '', $permissiontoedit);
}
// Show list of associated links
diff --git a/htdocs/core/class/html.formfiscalyear.class.php b/htdocs/core/class/html.formfiscalyear.class.php
index 9f49aa16f41..9092450d521 100644
--- a/htdocs/core/class/html.formfiscalyear.class.php
+++ b/htdocs/core/class/html.formfiscalyear.class.php
@@ -1,5 +1,6 @@
+ * Copyright (C) 2026 MDW
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -65,7 +66,7 @@ class FormFiscalYear extends Form
$sql = "SELECT f.rowid, f.label, f.date_start, f.date_end, f.statut as status";
$sql .= " FROM ".$this->db->prefix()."accounting_fiscalyear as f";
- $sql .= " WHERE f.entity = ".$conf->entity;
+ $sql .= " WHERE f.entity = ".((int) $conf->entity);
$sql .= " ORDER BY f.date_start ASC";
dol_syslog(get_class($this).'::'.__METHOD__, LOG_DEBUG);
diff --git a/htdocs/core/class/html.formintervention.class.php b/htdocs/core/class/html.formintervention.class.php
index cd01dc3fbce..bebd44df22f 100644
--- a/htdocs/core/class/html.formintervention.class.php
+++ b/htdocs/core/class/html.formintervention.class.php
@@ -1,6 +1,6 @@
- * Copyright (C) 2025 MDW
+ * Copyright (C) 2025-2026 MDW
* Copyright (C) 2025 Frédéric France
*
* This program is free software; you can redistribute it and/or modify
@@ -75,7 +75,7 @@ class FormIntervention
// Search all contacts
$sql = "SELECT f.rowid, f.ref, f.fk_soc, f.fk_statut as status";
$sql .= " FROM ".$this->db->prefix()."fichinter as f";
- $sql .= " WHERE f.entity = ".$conf->entity;
+ $sql .= " WHERE f.entity = ".((int) $conf->entity);
if ($socid >= 0) {
if ($socid == '0') {
$sql .= " AND (f.fk_soc = 0 OR f.fk_soc IS NULL)";
diff --git a/htdocs/core/class/html.formmargin.class.php b/htdocs/core/class/html.formmargin.class.php
index b4530c893de..ab05efe02a1 100644
--- a/htdocs/core/class/html.formmargin.class.php
+++ b/htdocs/core/class/html.formmargin.class.php
@@ -112,8 +112,14 @@ class FormMargin
/** @var Facture $object */
if (($object->element == 'facture' && $object->type == $object::TYPE_SITUATION)
|| ($object->element == 'facture' && $object->type == $object::TYPE_CREDIT_NOTE && getDolGlobalInt('INVOICE_USE_SITUATION_CREDIT_NOTE') && $object->situation_counter > 0)) {
- // We need a compensation relative to $line->situation_percent
- $pa = $line->qty * $pa_ht * ($line->situation_percent / 100);
+ // We need only the delta between this situation and the previous one to avoid cumulating margins across situation invoices
+ // total_ht is stored cumulatively (e.g. 60% of full price), so we extract only this invoice's share using the delta percent
+ $prevPercent = $line->get_prev_progress($object->id);
+ $deltaPercent = $line->situation_percent - $prevPercent;
+ if ($line->situation_percent > 0) {
+ $pv *= ($deltaPercent / $line->situation_percent);
+ }
+ $pa = $line->qty * $pa_ht * ($deltaPercent / 100);
} else {
$pa = $line->qty * $pa_ht;
}
diff --git a/htdocs/core/class/html.formsetup.class.php b/htdocs/core/class/html.formsetup.class.php
index e06f6490821..2568363d449 100644
--- a/htdocs/core/class/html.formsetup.class.php
+++ b/htdocs/core/class/html.formsetup.class.php
@@ -1,6 +1,6 @@
- * Copyright (C) 2024-2025 MDW
+ * Copyright (C) 2024-2026 MDW
* Copyright (C) 2026 Frédéric France
* Copyright (C) 2026 Alexandre Spangaro
*
@@ -1745,7 +1745,7 @@ class FormSetupItem
/**
* Set type of input as a select list.
*
- * @param ?array $fieldOptions A table of field options
+ * @param ?array $fieldOptions A table of field options
* @return self
*/
public function setAsSelect($fieldOptions)
diff --git a/htdocs/core/class/html.formsms.class.php b/htdocs/core/class/html.formsms.class.php
index 15336ce4f70..7d6953902d3 100644
--- a/htdocs/core/class/html.formsms.class.php
+++ b/htdocs/core/class/html.formsms.class.php
@@ -27,10 +27,10 @@ require_once DOL_DOCUMENT_ROOT.'/core/class/html.form.class.php';
/**
- * Class permettant la generation du formulaire d'envoi de Sms
+ * Class for generating the SMS sending form
* Usage: $formsms = new FormSms($db)
* $formsms->proprietes=1 or string or table of values
- * $formsms->show_form() affiche le formulaire
+ * $formsms->show_form() displays the form
*/
class FormSms
{
diff --git a/htdocs/core/class/html.formticket.class.php b/htdocs/core/class/html.formticket.class.php
index f9cd0a81d21..a80e0d94e6e 100644
--- a/htdocs/core/class/html.formticket.class.php
+++ b/htdocs/core/class/html.formticket.class.php
@@ -1213,7 +1213,7 @@ class FormTicket
if (!empty($arrayidused)) {
$sql .= " AND ctc.fk_parent IN ( ";
foreach ($arrayidused as $idused) {
- $sql .= $idused.", ";
+ $sql .= ((int) $idused).", ";
}
$sql = substr($sql, 0, -2);
$sql .= ")";
diff --git a/htdocs/core/class/infobox.class.php b/htdocs/core/class/infobox.class.php
index 9f1ff93c0e0..6b08c756c38 100644
--- a/htdocs/core/class/infobox.class.php
+++ b/htdocs/core/class/infobox.class.php
@@ -3,7 +3,7 @@
* Copyright (C) 2004-2012 Laurent Destailleur
* Copyright (C) 2005-2012 Regis Houssin
* Copyright (C) 2019 Nicolas ZABOURI
- * Copyright (C) 2024 MDW
+ * Copyright (C) 2024-2026 MDW
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -105,12 +105,12 @@ class InfoBox
$sql .= " d.rowid as box_id, d.file, d.note, d.tms";
$sql .= " FROM ".$dbs->prefix()."boxes as b, ".$dbs->prefix()."boxes_def as d";
$sql .= " WHERE b.box_id = d.rowid";
- $sql .= " AND b.entity IN (0,".$conf->entity.")";
+ $sql .= " AND b.entity IN (0,".((int) $conf->entity).")";
if ($zone >= 0) {
$sql .= " AND b.position = ".((int) $zone);
}
if (is_object($user)) {
- $sql .= " AND b.fk_user IN (0,".$user->id.")";
+ $sql .= " AND b.fk_user IN (0,".((int) $user->id).")";
} else {
$sql .= " AND b.fk_user = 0";
}
@@ -118,7 +118,7 @@ class InfoBox
} else { // available
$sql = "SELECT d.rowid as box_id, d.file, d.note, d.tms";
$sql .= " FROM ".$dbs->prefix()."boxes_def as d";
- $sql .= " WHERE d.entity IN (0, ".$conf->entity.")";
+ $sql .= " WHERE d.entity IN (0, ".((int) $conf->entity).")";
}
dol_syslog(self::class."::listBoxes get default box list for mode=".$mode." userid=".(is_object($user) ? $user->id : ''), LOG_DEBUG);
@@ -260,7 +260,7 @@ class InfoBox
// Delete all lines
$sql = "DELETE FROM ".$dbs->prefix()."boxes";
- $sql .= " WHERE entity = ".$conf->entity;
+ $sql .= " WHERE entity = ".((int) $conf->entity);
$sql .= " AND fk_user = ".((int) $userid);
$sql .= " AND position = ".((int) $zone);
diff --git a/htdocs/core/class/ldap.class.php b/htdocs/core/class/ldap.class.php
index f587a905600..11774f158b7 100644
--- a/htdocs/core/class/ldap.class.php
+++ b/htdocs/core/class/ldap.class.php
@@ -359,7 +359,10 @@ class Ldap
dol_syslog(get_class($this)."::connectBind serverPing true, we try ldap_connect to ".$host, LOG_DEBUG);
}
if (version_compare(PHP_VERSION, '8.3.0', '>=')) {
- $uri = $host.':'.$this->serverPort;
+ // Since PHP 8.3, ldap_connect() expects a single URI argument. A scheme-less
+ // host (ex: localhost, 192.168.0.2) must be turned into a valid ldap:// URI,
+ // otherwise the host is parsed as the URI scheme and the later bind fails.
+ $uri = preg_match('/^ldaps?:\/\//i', $host) ? $host : 'ldap://'.$host.':'.$this->serverPort;
$this->connection = ldap_connect($uri);
} else {
$this->connection = ldap_connect($host, $this->serverPort);
@@ -372,7 +375,7 @@ class Ldap
dol_syslog(get_class($this)."::connectBind serverPing false, we try ldap_connect to ".$host, LOG_DEBUG);
}
if (version_compare(PHP_VERSION, '8.3.0', '>=')) {
- $uri = $host.':'.$this->serverPort;
+ $uri = preg_match('/^ldaps?:\/\//i', $host) ? $host : 'ldap://'.$host.':'.$this->serverPort;
$this->connection = ldap_connect($uri);
} else {
$this->connection = ldap_connect($host, $this->serverPort);
diff --git a/htdocs/core/class/link.class.php b/htdocs/core/class/link.class.php
index 73a2adda0f2..37b10cf41a2 100644
--- a/htdocs/core/class/link.class.php
+++ b/htdocs/core/class/link.class.php
@@ -1,6 +1,6 @@
- * Copyright (C) 2024-2025 MDW
+ * Copyright (C) 2024-2026 MDW
* Copyright (C) 2024 Frédéric France
*
* This program is free software; you can redistribute it and/or modify
@@ -123,7 +123,7 @@ class Link extends CommonObject
$this->db->begin();
$sql = "INSERT INTO ".$this->db->prefix()."links (entity, datea, url, label, objecttype, objectid, share,share_pass)";
- $sql .= " VALUES (".$conf->entity.", '".$this->db->idate($this->datea)."'";
+ $sql .= " VALUES (".((int) $conf->entity).", '".$this->db->idate($this->datea)."'";
$sql .= ", '".$this->db->escape($this->url)."'";
$sql .= ", '".$this->db->escape($this->label)."'";
$sql .= ", '".$this->db->escape($this->objecttype)."'";
@@ -345,7 +345,7 @@ class Link extends CommonObject
$rowid = $this->id;
}
- $sqlwhere=[];
+ $sqlwhere = [];
$sql = "SELECT rowid, entity, datea, url, label, objecttype, objectid, share, share_pass FROM ".$this->db->prefix()."links";
if (!empty((int) $rowid)) {
@@ -356,10 +356,10 @@ class Link extends CommonObject
}
if ($conf->entity != 0) {
- $sqlwhere[] = " entity = ".$conf->entity;
+ $sqlwhere[] = " entity = ".((int) $conf->entity);
}
- if (count($sqlwhere)>0) {
- $sql .=' WHERE '.implode(' AND ', $sqlwhere);
+ if (count($sqlwhere) > 0) {
+ $sql .= ' WHERE '.implode(' AND ', $sqlwhere);
}
dol_syslog(get_class($this)."::fetch", LOG_DEBUG);
diff --git a/htdocs/core/class/menubase.class.php b/htdocs/core/class/menubase.class.php
index 2b9d18fa4fe..065080d289e 100644
--- a/htdocs/core/class/menubase.class.php
+++ b/htdocs/core/class/menubase.class.php
@@ -230,7 +230,7 @@ class Menubase
$maxrowid = 1;
}
- $sql = "SELECT setval('".$this->db->prefix()."menu_rowid_seq', ".($maxrowid).")";
+ $sql = "SELECT setval('".$this->db->prefix()."menu_rowid_seq', ".((int) $maxrowid).")";
//print $sql; exit;
$resqlrowidset = $this->db->query($sql);
if (!$resqlrowidset) {
@@ -248,7 +248,7 @@ class Menubase
$sql .= " AND fk_menu = ".((int) $this->fk_menu);
$sql .= " AND position = ".((int) $this->position);
$sql .= " AND url = '".$this->db->escape($this->url)."'";
- $sql .= " AND entity IN (0, ".$conf->entity.")";
+ $sql .= " AND entity IN (0, ".((int) $conf->entity).")";
$result = $this->db->query($sql);
if ($result) {
@@ -649,7 +649,7 @@ class Menubase
$sql = "SELECT m.rowid, m.type, m.module, m.fk_menu, m.fk_mainmenu, m.fk_leftmenu, m.url, m.titre,";
$sql .= " m.prefix, m.langs, m.perms, m.enabled, m.target, m.mainmenu, m.leftmenu, m.position, m.showtopmenuinframe";
$sql .= " FROM ".$this->db->prefix()."menu as m";
- $sql .= " WHERE m.entity IN (0,".$conf->entity.")";
+ $sql .= " WHERE m.entity IN (0,".((int) $conf->entity).")";
$sql .= " AND m.menu_handler IN ('".$this->db->escape($menu_handler)."','all')";
if ($type_user == 0) {
$sql .= " AND m.usertype IN (0,2)";
@@ -679,7 +679,7 @@ class Menubase
$tmpcond = preg_replace('/\$leftmenu\s*==\s*["\'a-zA-Z_]+/', '1==1', $tmpcond); // Force the part of condition on leftmenu to true
}
$perms = verifCond($tmpcond);
- //print "verifCond rowid=".$menu['rowid']." ".$tmpcond.":".$perms." \n";
+ //var_dump($menu['rowid'].' - '.$menu['titre'].' - '.$menu['perms'].' => '.$tmpcond.":".$perms);
}
// Define $enabled
diff --git a/htdocs/core/class/openid.class.php b/htdocs/core/class/openid.class.php
index 9a0e91c48a7..5f04f6a9a19 100644
--- a/htdocs/core/class/openid.class.php
+++ b/htdocs/core/class/openid.class.php
@@ -388,7 +388,7 @@ class SimpleOpenID
$response = getURLContent($url, 'GET', '', 1, array(), array('http', 'https'));
- list($servers, $delegates) = $this->HTML2OpenIDServer($response);
+ list($servers, $delegates) = $this->HTML2OpenIDServer($response['content']);
if (count($servers) == 0) {
$this->ErrorStore('OPENID_NOSERVERSFOUND');
return false;
diff --git a/htdocs/core/class/timespent.class.php b/htdocs/core/class/timespent.class.php
index 3e9464d6c14..0709c296533 100644
--- a/htdocs/core/class/timespent.class.php
+++ b/htdocs/core/class/timespent.class.php
@@ -561,14 +561,14 @@ class TimeSpent extends CommonObject
if (preg_match('/^[\(]?PROV/i', $this->ref)) {
// Now we rename also files into index
$sql = 'UPDATE '.MAIN_DB_PREFIX."ecm_files set filename = CONCAT('".$this->db->escape($this->newref)."', SUBSTR(filename, ".(strlen($this->ref) + 1).")), filepath = 'timespent/".$this->db->escape($this->newref)."'";
- $sql .= " WHERE filename LIKE '".$this->db->escape($this->ref)."%' AND filepath = 'timespent/".$this->db->escape($this->ref)."' and entity = ".$conf->entity;
+ $sql .= " WHERE filename LIKE '".$this->db->escape($this->ref)."%' AND filepath = 'timespent/".$this->db->escape($this->ref)."' and entity = ".((int) $conf->entity);
$resql = $this->db->query($sql);
if (!$resql) {
$error++;
$this->error = $this->db->lasterror();
}
$sql = 'UPDATE '.MAIN_DB_PREFIX."ecm_files set filepath = 'timespent/".$this->db->escape($this->newref)."'";
- $sql .= " WHERE filepath = 'timespent/".$this->db->escape($this->ref)."' and entity = ".$conf->entity;
+ $sql .= " WHERE filepath = 'timespent/".$this->db->escape($this->ref)."' and entity = ".((int) $conf->entity);
$resql = $this->db->query($sql);
if (!$resql) {
$error++;
diff --git a/htdocs/core/class/utils.class.php b/htdocs/core/class/utils.class.php
index d9fafe911ed..ca041a98ad9 100644
--- a/htdocs/core/class/utils.class.php
+++ b/htdocs/core/class/utils.class.php
@@ -246,9 +246,10 @@ class Utils
*/
public function dumpDatabase($compression = 'none', $type = 'auto', $usedefault = 1, $file = 'auto', $keeplastnfiles = 0, $execmethod = 0, $lowmemorydump = 0)
{
- global $db, $conf, $langs, $dolibarr_main_data_root;
+ global $db, $conf, $langs;
global $dolibarr_main_db_name, $dolibarr_main_db_host, $dolibarr_main_db_user, $dolibarr_main_db_port, $dolibarr_main_db_pass;
global $dolibarr_main_db_character_set;
+ global $dolibarr_main_restrict_os_commands;
$langs->load("admin");
@@ -318,6 +319,23 @@ class Utils
$outputerror = $outputfile.'.err';
dol_mkdir($conf->admin->dir_output.'/backup');
+ $cmddump = dol_sanitizePathName($cmddump); // Sanitize path
+ $cmddump = dol_string_nospecial($cmddump, '', array("|", ";", "<", ">", "&", "+")); // Sanitize command
+ $basenamecmddump = basename(str_replace('\\', '/', $cmddump));
+
+ // Sanitize and validate $cmddump
+ if (!empty($dolibarr_main_restrict_os_commands)) {
+ $arrayofallowedcommand = explode(',', $dolibarr_main_restrict_os_commands);
+ $arrayofallowedcommand = array_map('trim', $arrayofallowedcommand);
+ dol_syslog("Command are restricted to ".$dolibarr_main_restrict_os_commands.". We check that one of this command is inside ".$cmddump);
+ if (!in_array($basenamecmddump, $arrayofallowedcommand)) { // the provided command $cmddump must be an allowed command
+ $langs->load("errors");
+ $this->error = $langs->trans('CommandIsNotInsideAllowedCommands');
+ $this->error .= ' '.$langs->trans('ErrorCheckTheCommandInsideTheAdvancedOptions');
+ return -1;
+ }
+ }
+
// Parameters execution
$command = $cmddump;
$command = preg_replace('/(\$|%)/', '', $command); // We removed chars that can be used to inject vars that contains space inside path of command without seeing there is a space to bypass the escapeshellarg.
@@ -631,6 +649,23 @@ class Utils
$outputerror = $outputfile.'.err';
dol_mkdir($conf->admin->dir_output.'/backup');
+ // Sanitize and validate $cmddump
+ $cmddump = dol_sanitizePathName($cmddump); // Sanitize path
+ $cmddump = dol_string_nospecial($cmddump, '', array("|", ";", "<", ">", "&", "+")); // Sanitize command
+ $basenamecmddump = basename(str_replace('\\', '/', $cmddump));
+
+ if (!empty($dolibarr_main_restrict_os_commands)) {
+ $arrayofallowedcommand = explode(',', $dolibarr_main_restrict_os_commands);
+ $arrayofallowedcommand = array_map('trim', $arrayofallowedcommand);
+ dol_syslog("Command are restricted to ".$dolibarr_main_restrict_os_commands.". We check that one of this command is inside ".$cmddump);
+ if (!in_array($basenamecmddump, $arrayofallowedcommand)) { // the provided command $cmddump must be an allowed command
+ $langs->load("errors");
+ $this->error = $langs->trans('CommandIsNotInsideAllowedCommands');
+ $this->error .= ' '.$langs->trans('ErrorCheckTheCommandInsideTheAdvancedOptions');
+ return -1;
+ }
+ }
+
// Parameters execution
$command = $cmddump;
$command = preg_replace('/(\$|%)/', '', $command); // We removed chars that can be used to inject vars that contains space inside path of command without seeing there is a space to bypass the escapeshellarg.
diff --git a/htdocs/core/customreports.php b/htdocs/core/customreports.php
index b2d3bdf701e..56fef148376 100644
--- a/htdocs/core/customreports.php
+++ b/htdocs/core/customreports.php
@@ -92,10 +92,10 @@ if (!defined('USE_CUSTOM_REPORT_AS_INCLUDE')) {
return preg_replace('/[^a-z0-9\._\-]+/', '', $value);
}
- $search_measures = array_map('sanititzekey', $search_measures);
- $search_xaxis = array_map('sanititzekey', $search_xaxis);
- $search_yaxis = array_map('sanititzekey', $search_yaxis);
- $search_groupby = array_map('sanititzekey', $search_groupby);
+ $sanitized_search_measures = array_map('sanititzekey', $search_measures);
+ $sanitized_search_xaxis = array_map('sanititzekey', $search_xaxis);
+ $sanitized_search_yaxis = array_map('sanititzekey', $search_yaxis);
+ $sanitized_search_groupby = array_map('sanititzekey', $search_groupby);
// Load variable for pagination
$limit = GETPOSTINT('limit') ? GETPOSTINT('limit') : $conf->liste_limit;
@@ -116,7 +116,7 @@ if (!defined('USE_CUSTOM_REPORT_AS_INCLUDE')) {
* @var ?int $SHOWLEGEND Show legend or not
* @var string $customreportkey Custom report key
* @var ?string $customsql Custom SQL
- * @var ?string[] $search_groupby Array with the third dimension
+ * @var ?string[] $sanitized_search_groupby Array with the third dimension
* @var int $page
* @var string $sortfield
* @var string $sortorder
@@ -125,13 +125,13 @@ if (!defined('USE_CUSTOM_REPORT_AS_INCLUDE')) {
@phan-var-force int<0,1> $SHOWLEGEND
@phan-var-force string $customreportkey
@phan-var-force ?string $customsql
- @phan-var-force ?string[] $search_groupby Array with the third dimension
+ @phan-var-force ?string[] $sanitized_search_groupby Array with the third dimension
@phan-var-force int $page
@phan-var-force string $sortfield
@phan-var-force string $sortorder
';
- // $search_measures, $search_xaxis or $search_yaxis may have been defined by the parent.
+ // $sanitized_search_measures, $sanitized_search_xaxis or $sanitized_search_yaxis may have been defined by the parent.
if (empty($user) || empty($user->id)) {
print 'Page is called as an include but $user and its permission loaded with loadRights() are not defined. We stop here.';
@@ -150,20 +150,20 @@ $dolibarr_allow_unsecured_select_in_extrafields_filter = 0;
if (empty($mode)) {
$mode = 'graph';
}
-if (!isset($search_measures)) {
- $search_measures = array(0 => 't.count');
+if (!isset($sanitized_search_measures)) {
+ $sanitized_search_measures = array(0 => 't.count');
}
-if (!isset($search_xaxis)) {
+if (!isset($sanitized_search_xaxis)) {
// Ensure value is set and not null.
- $search_xaxis = array();
+ $sanitized_search_xaxis = array();
}
-if (!isset($search_yaxis)) {
+if (!isset($sanitized_search_yaxis)) {
// Ensure value is set and not null.
- $search_yaxis = array();
+ $sanitized_search_yaxis = array();
}
-if (!isset($search_groupby)) {
+if (!isset($sanitized_search_groupby)) {
// Ensure value is set and not null.
- $search_groupby = array();
+ $sanitized_search_groupby = array();
}
if (!isset($search_graph)) {
// Ensure value is set and not null
@@ -476,19 +476,19 @@ $arrayofgroupby = dol_sort_array($arrayofgroupby, 'position', 'asc', 0, 0, 1);
// Check parameters
if ($action == 'viewgraph') {
- if (!count($search_measures)) {
+ if (!count($sanitized_search_measures)) {
setEventMessages($langs->trans("AtLeastOneMeasureIsRequired"), null, 'warnings');
- } elseif ($mode == 'graph' && is_array($search_xaxis) && count($search_xaxis) > 1) {
+ } elseif ($mode == 'graph' && is_array($sanitized_search_xaxis) && count($sanitized_search_xaxis) > 1) {
setEventMessages($langs->trans("OnlyOneFieldForXAxisIsPossible"), null, 'warnings');
- $search_xaxis = array(0 => $search_xaxis[0]);
+ $sanitized_search_xaxis = array(0 => $sanitized_search_xaxis[0]);
}
- if (count($search_groupby) >= 2) {
+ if (count($sanitized_search_groupby) >= 2) {
setEventMessages($langs->trans("ErrorOnlyOneFieldForGroupByIsPossible"), null, 'warnings');
- $search_groupby = array(0 => $search_groupby[0]);
+ $sanitized_search_groupby = array(0 => $sanitized_search_groupby[0]);
}
- if (!count($search_xaxis)) {
+ if (!count($sanitized_search_xaxis)) {
setEventMessages($langs->trans("AtLeastOneXAxisIsRequired"), null, 'warnings');
- } elseif ($mode == 'graph' && $search_graph == 'bars' && count($search_measures) > $MAXMEASURESINBARGRAPH) {
+ } elseif ($mode == 'graph' && $search_graph == 'bars' && count($sanitized_search_measures) > $MAXMEASURESINBARGRAPH) {
$langs->load("errors");
setEventMessages($langs->trans("GraphInBarsAreLimitedToNMeasures", $MAXMEASURESINBARGRAPH), null, 'warnings');
$search_graph = 'lines';
@@ -497,34 +497,36 @@ if ($action == 'viewgraph') {
// Get all possible values of fields when a 'group by' is set, and save this into $arrayofvaluesforgroupby
// $arrayofvaluesforgroupby will be used to forge lael of each grouped series
-if (count($search_groupby)) {
+if (count($sanitized_search_groupby)) {
$fieldtocount = '';
- foreach ($search_groupby as $gkey => $gval) {
- $gvalwithoutprefix = preg_replace('/^[a-z]+\./i', '', $gval);
- $gvalsanitized = preg_replace('/[^a-z0-9\._\-]+/i', '', $gval);
+ foreach ($sanitized_search_groupby as $gkey => $sanitized_gval) {
+ $gvalwithoutprefix = preg_replace('/^[a-z]+\./i', '', $sanitized_gval);
+ $gvalsanitized = preg_replace('/[^a-z0-9\._\-]+/i', '', $sanitized_gval);
+
+ $sanitizedfieldtocount = '';
if (preg_match('/\-year$/', $gvalsanitized)) {
$tmpval = preg_replace('/\-year$/', '', $gvalsanitized);
- $fieldtocount .= 'DATE_FORMAT('.$tmpval.", '%Y')";
+ $sanitizedfieldtocount .= 'DATE_FORMAT('.$tmpval.", '%Y')";
} elseif (preg_match('/\-month$/', $gvalsanitized)) {
$tmpval = preg_replace('/\-month$/', '', $gvalsanitized);
- $fieldtocount .= 'DATE_FORMAT('.$tmpval.", '%Y-%m')";
+ $sanitizedfieldtocount .= 'DATE_FORMAT('.$tmpval.", '%Y-%m')";
} elseif (preg_match('/\-day$/', $gvalsanitized)) {
$tmpval = preg_replace('/\-day$/', '', $gvalsanitized);
- $fieldtocount .= 'DATE_FORMAT('.$tmpval.", '%Y-%m-%d')";
+ $sanitizedfieldtocount .= 'DATE_FORMAT('.$tmpval.", '%Y-%m-%d')";
} else {
- $fieldtocount = $gvalsanitized;
+ $sanitizedfieldtocount = $gvalsanitized;
}
- $sanitizedfieldtocount = $fieldtocount;
- $sql = "SELECT DISTINCT ".$sanitizedfieldtocount." as val"; // $fieldtocount has been sanitized by previous lines as we can't use db->sanitie()
+ $fieldtocount = $sanitizedfieldtocount;
+ $sql = "SELECT DISTINCT ".$sanitizedfieldtocount." as val"; // $fieldtocount has been sanitized by previous lines as we can't use db->sanitize()
if (strpos($fieldtocount, 'te') === 0) {
$tabletouse = $object->table_element;
$tablealiastouse = 'te';
- if (!empty($arrayofgroupby[$gval])) {
- $tmpval = explode('.', $gval);
- $tabletouse = $arrayofgroupby[$gval]['table'];
+ if (!empty($arrayofgroupby[$sanitized_gval])) {
+ $tmpval = explode('.', $sanitized_gval);
+ $tabletouse = $arrayofgroupby[$sanitized_gval]['table'];
$tablealiastouse = $tmpval[0];
}
//var_dump($tablealiastouse);exit;
@@ -534,9 +536,9 @@ if (count($search_groupby)) {
} else {
$tabletouse = $object->table_element;
$tablealiastouse = 't';
- if (!empty($arrayofgroupby[$gval])) {
- $tmpval = explode('.', $gval);
- $tabletouse = $arrayofgroupby[$gval]['table'];
+ if (!empty($arrayofgroupby[$sanitized_gval])) {
+ $tmpval = explode('.', $sanitized_gval);
+ $tabletouse = $arrayofgroupby[$sanitized_gval]['table'];
$tablealiastouse = $tmpval[0];
}
$sql .= " FROM ".MAIN_DB_PREFIX.$tabletouse." as ".$tablealiastouse;
@@ -601,10 +603,10 @@ if (count($search_groupby)) {
$arrayofvaluesforgroupby['g_'.$gkey][$keytouse] = $valuetranslated;
}
// Add also the possible NULL value if field is a parent field that is not a strict join
- $tmpfield = explode('.', $gval);
+ $tmpfield = explode('.', $sanitized_gval);
if ($tmpfield[0] != 't' || (isset($object->fields[$tmpfield[1]]) && is_array($object->fields[$tmpfield[1]]) && empty($object->fields[$tmpfield[1]]['notnull']))) {
- dol_syslog("The group by field ".$gval." may be null (because field is null or it is a left join), so we add __NULL__ entry in list of possible values");
- //var_dump($gval); var_dump($object->fields);
+ dol_syslog("The group by field ".$sanitized_gval." may be null (because field is null or it is a left join), so we add __NULL__ entry in list of possible values");
+ //var_dump($sanitized_gval); var_dump($object->fields);
$arrayofvaluesforgroupby['g_'.$gkey]['__NULL__'] = $langs->transnoentitiesnoconv("NotDefined");
}
@@ -648,7 +650,7 @@ if (count($search_groupby)) {
}
//var_dump($labeloffield);
setEventMessages($langs->transnoentitiesnoconv("ErrorTooManyDifferentValueForSelectedGroupBy", (string) $MAXUNIQUEVALFORGROUP, (string) $labeloffield), null, 'warnings');
- $search_groupby = array();
+ $sanitized_search_groupby = array();
}
$db->free($resql);
@@ -741,7 +743,7 @@ if (!defined('MAIN_CUSTOM_REPORT_KEEP_GRAPH_ONLY')) {
foreach ($arrayofmesures as $key => $val) {
$simplearrayofmesures[$key] = $arrayofmesures[$key]['label'];
}
- print $form->multiselectarray('search_measures', $simplearrayofmesures, $search_measures, 0, 0, 'minwidth300 widthcentpercentminusx', 1, 0, '', '', $langs->transnoentitiesnoconv("Measures")); // Fill the array $arrayofmeasures with possible fields
+ print $form->multiselectarray('search_measures', $simplearrayofmesures, $sanitized_search_measures, 0, 0, 'minwidth300 widthcentpercentminusx', 1, 0, '', '', $langs->transnoentitiesnoconv("Measures")); // Fill the array $arrayofmeasures with possible fields
print '';
// XAxis
@@ -749,14 +751,14 @@ if (!defined('MAIN_CUSTOM_REPORT_KEEP_GRAPH_ONLY')) {
print '
';
print '
';
//var_dump($arrayofxaxis);
- print $formother->selectXAxisField($object, $search_xaxis, $arrayofxaxis, $langs->trans("XAxis"), 'minwidth300 maxwidth400 widthcentpercentminusx'); // Fill the array $arrayofxaxis with possible fields
+ print $formother->selectXAxisField($object, $sanitized_search_xaxis, $arrayofxaxis, $langs->trans("XAxis"), 'minwidth300 maxwidth400 widthcentpercentminusx'); // Fill the array $arrayofxaxis with possible fields
print '
';
// Group by
$count = 0;
print '
';
print '
';
- print $formother->selectGroupByField($object, $search_groupby, $arrayofgroupby, 'minwidth250 maxwidth300 widthcentpercentminusx', $langs->trans("GroupBy")); // Fill the array $arrayofgroupby with possible fields
+ print $formother->selectGroupByField($object, $sanitized_search_groupby, $arrayofgroupby, 'minwidth250 maxwidth300 widthcentpercentminusx', $langs->trans("GroupBy")); // Fill the array $arrayofgroupby with possible fields
print '