`getOnlineSignatureUrl()` ends each public signature URL with
`&securekey=` followed by the output of `dol_hash(..., '0')`. With the
default `MAIN_SECURITY_HASH_ALGO = password_hash`, that output is a
bcrypt string whose alphabet is `[./A-Za-z0-9]`, so a non-trivial share
of generated URLs legitimately end with `.` or `/`.
Gmail and several mobile mail clients autolink plain-text URLs but
drop trailing punctuation, so the recipient clicks on a URL with the
last character missing and gets a securekey mismatch error
(`Bad value for securitykey`). The user-facing symptom is exactly the
one in #31464.
When multicompany is enabled, the URL already ends with `&entity=N`,
which sidesteps the issue. For installations without multicompany,
append a stable trailing `&_=1` so the URL never ends with the hash.
The receiver in `public/onlinesign/newonlinesign.php` does not read
`_`, only `source`, `ref`, `securekey` and (when multicompany) `entity`,
so adding the parameter has no behavior impact and existing links keep
verifying as before. Only the real URL (mode 0) is affected, not the
mode 1 preview shown in the admin setup.
Signed-off-by: Dolicraft <contact@dolicraft.com>
Co-authored-by: Dolicraft <contact@dolicraft.com>
The minimum-price guard on the customer invoice, customer order and
customer proposal forms compares the entered unit price to the
product's price_min. When the document is in a foreign currency the
unit price is provided in the foreign currency ($pu_ht_devise), so
it must first be converted back to the base (company) currency before
the comparison.
card.php was multiplying by $currency_tx:
$pu_equivalent = (float) $pu_ht_devise * $currency_tx;
but multicurrency_tx is defined as 'units of foreign currency per
1 unit of base', exactly as price.lib.php uses it
(calcul_price_total at line 181):
$pu = $pu_devise / $multicurrency_tx;
With the reporter's example (1 MXN = 0.055 USD, so tx = 0.055,
product min price = 1000 MXN, entered 55 USD), the wrong direction
gives 55 * 0.055 = 3.025 MXN < 1000 MXN and the form errors out with
'CantBeLessThanMinPrice', even though 55 USD is exactly 1000 MXN.
Dividing yields 55 / 0.055 = 1000 MXN and the check passes.
Mirror calcul_price_total's direction at all four invoice sites
(htdocs/compta/facture/card.php lines 2503/2506 and 2684/2687) plus
the corresponding sites in htdocs/commande/card.php and
htdocs/comm/propal/card.php. Documents in the base currency
($pu_ht_devise empty) are not affected because the conversion
branch is gated on $pu_ht_devise being non-empty.
Signed-off-by: Dolicraft <contact@dolicraft.com>
Co-authored-by: Dolicraft <contact@dolicraft.com>
htdocs/comm/action/card.php buffered the assignee working set in
$_SESSION['assignedtouser'] and $_SESSION['assignedtoresource'], two
flat keys shared by every browser tab of the same logged-in user. With
two tabs editing two different events, the second tab overwrote the
first one's working set, and a plain save of the first tab then
replaced its assignees with those of the second tab.
Suffix both session keys with the event id (assignedtouser_<id>,
assignedtoresource_<id>), falling back to _new for the still-unsaved
create form. Two tabs editing different events now each have their own
bucket; two tabs creating two new events still share the _new bucket,
which is the same edge the previous design had and is much less common
in practice than the cross-tab edit case the reporter hit.
Compute the keys once near the top of the file and use the variables
everywhere the old flat keys appeared (add, remove, persist, unset
sites). No HTML form change is needed because the event id is already
preserved in the query string across the assignee subform submits.
Per @eldy review on PR #38531, this is the develop-branch version of
the fix; the 21.0 PR is closed in favor of this one.
Signed-off-by: Dolicraft <contact@dolicraft.com>
Co-authored-by: Dolicraft <contact@dolicraft.com>
* Add new language entries for AI tool management
* Update file reference in assistant.php documentation
* Implement isSystem method in conversation class
* Add Tool Access Control tab
* Initial commit AI general css
* Initial commit AI general js file
* Implement isSystem method in mcptool class
Add isSystem method to determine tool visibility.
* Enhance MCPServer to enforce tool allow-list
* Enhance McpHandler with tool context handling
Added support for tool context in McpHandler class.
* Remove empty line at the start of mcp.class.php
* Refactor tool schema handling for LLM integration
* Modify McpHandler initialization in execute_tool.php
Updated McpHandler initialization to include configuration and context for assistant.
* New AI tool access control
* Fix pre-commit
* fix pre-commit
* fix pre-commit
* fix pre-commit
* fix pre-commit
* fix phan php-stan
The per-card Send by email button on Propal, Commande and Facture
already honours the hidden constants
`PROPOSAL_SENDBYEMAIL_FOR_ALL_STATUS`,
`COMMANDE_SENDBYEMAIL_FOR_ALL_STATUS` and
`FACTURE_SENDBYEMAIL_FOR_ALL_STATUS` to allow sending drafts. The
mass Send by email action in the corresponding lists silently dropped
draft objects regardless of those constants, so an administrator who
intentionally enabled the override saw inconsistent behaviour between
the card and the list.
Apply the same constant check on the three draft-skip guards in
`htdocs/core/actions_massactions.inc.php` so the mass action behaves
the same way as the single-object button. Default behaviour is
unchanged: with none of the constants set, drafts are still skipped
with the existing 'OnlyXxxNotDraftCanBeSentInMassAction' error
message.
Signed-off-by: Dolicraft <contact@dolicraft.com>
Co-authored-by: Dolicraft <contact@dolicraft.com>
The select2 combobox built around select_all_categories renders the
truncated label via the data-html attribute (see templateResult in
core/lib/ajax.lib.php ajax_combobox), and runs its search matcher on
the bare option text. When the option text itself was the truncated
label, any character outside the visible portion was unreachable from
the search box, so long category names whose distinctive part fell
in the hidden middle stopped matching.
Write the full label in the option text and keep data-html as the
truncated visible rendering. The dropdown still looks compact, but
the matcher can now find a hit anywhere in the full category name.
The full label is run through dol_escape_htmltag to keep the
generated HTML safe (the previous bare dol_trunc output was not
escaped). Same regression on 21.0, 22.0, 23.0 and develop; PR targets
21.0.
Reported by @SteveS1609. Regression introduced by commit 1e76422a50
("NEW Show picto and color into combo for selection of tags") shipped
in v21.
Signed-off-by: Dolicraft <contact@dolicraft.com>
Co-authored-by: Dolicraft <contact@dolicraft.com>
* Fix#38491 Name the field in the ticket too-long error message
When a ticket message exceeds 65000 characters, Ticket::verify() and
Ticket::update() appended the bare string "ErrorFieldTooLong" to
$this->errors. setEventMessages forwards that string and
dol_htmloutput_mesg applies $langs->trans() to it, which yields the
raw template "Field %s is too long." with the %s placeholder still
visible because no argument was passed.
Replace the bare key with a pre-translated message that names the
field ("Initial message"), the same pattern already used in
htdocs/barcode/printsheet.php for the same lang key. Two identical
sites in the file are updated (one in verify, called by create, and
one in update). The "errors" and "ticket" lang files are loaded
defensively before translation because both methods can run before
the page-level loadLangs.
Reported by @ocemag, expected field name confirmed by @aspangaro.
Signed-off-by: Dolicraft <contact@dolicraft.com>
* Fix phan: declare $langs as global in ticket too-long branches
phan level 10 raised PhanUndeclaredVariable and PhanNonClassMethodCall
on the two branches that translate the new error message. The enclosing
Ticket::verify() and Ticket::update() do not declare global $langs;
add it where needed instead of opening the method scope further.
Signed-off-by: Dolicraft <contact@dolicraft.com>
---------
Signed-off-by: Dolicraft <contact@dolicraft.com>
Co-authored-by: Dolicraft <contact@dolicraft.com>
Co-authored-by: Alexandre SPANGARO <alexandre.spangaro@gmail.com>
ExtraFields::showInputField normalises the legacy "(field=123)"
filter syntax of "List from a table" extrafields into the modern
USF "(field:=:123)" form. The compatibility regex
"/^\(?([a-z0-9]+)([=<>]+)(\d+)\)?$/i" only accepts alphanumeric
field names, so a filter like "fk_typent=2" does not match, the
filter is left as is, and forgeSQLFromUniversalSearchCriteria later
fails with "Filter error" because the raw expression is not valid
universal search syntax.
Accept the underscore character in the field name class, the rest
of the predicate is untouched. The two identical occurrences in
the file (one per branch of the dispatch) are updated.
Reported and root cause pinpointed by @stephanebourdon-cmyk.
Signed-off-by: Dolicraft <contact@dolicraft.com>
Co-authored-by: Dolicraft <contact@dolicraft.com>
Updating a product selling price via /product/price.php triggered a
fatal "Call to undefined method stdClass::isEmpty()" at
Product::update line 1457 of 21.0. In the update_price flow,
$this->oldcopy is a stdClass (not a Product instance), so isEmpty()
does not exist on it. The page returned HTTP 500 and the open
transaction was forcibly closed by DoliDBMysqli::close.
Replace the isEmpty() call with empty($this->oldcopy->id), which is
the form already shipped in 22.0, 23.0 and develop after upstream
commit ef94452f77. The accompanying comment from that commit is
also brought in.
Same root cause as issue 37792 (closed, fixed in 23.0.3 and
forward); the fix was never backported to 21.0 even though the bug
is still triggered on that branch.
Reported by @YOYOKMC.
Signed-off-by: Dolicraft <contact@dolicraft.com>
Co-authored-by: Dolicraft <contact@dolicraft.com>
* NEW : add ModuleBuilder object-tabs map and filter helpers
Add getModuleBuilderObjectTabs() (source-of-truth map of optional object
tabs) and filterEnabledTabs() (sanitizes user-requested tab keys against the
map). Covered by ModuleBuilderLibTest.
Next: add BEGIN/END markers around tab flags and blocks in template lib (Task 2)
* NEW : wrap object tab flags and blocks with MODULEBUILDER markers
Surround each optional tab flag declaration (TABFLAG) and each prepareHead tab
block (TAB) with BEGIN/END MODULEBUILDER comment markers, so the generator can
purge a whole tab atomically while keeping $h and $head intact. Card tab is
not wrapped (always generated).
Next: add tab selection checkboxes to the newobject form in modulebuilder/index.php (Task 3)
* NEW : add object tab selection checkboxes to newobject form
Render one checkbox per optional tab (contact, note, document, agenda) in the
ModuleBuilder newobject form, all checked by default and reflecting posted
state on redisplay.
Next: handle enabledtab in initobject — exclude files, purge blocks, hardcode flags (Task 4)
* NEW : apply object tab selection during object generation
Excludes page files of unselected tabs, purges their prepareHead flag and
block via MODULEBUILDER markers, and hardcodes the show flag to 1 for selected
tabs. Emits a best-effort warning when regenerating an existing object.
Next: add language keys EnabledTabsForObject(Help) + WarningTabSelectionOnRegeneration (Task 5)
* NEW : add language keys for object tab selection
Next: add ChangeLog entry (Task 6)
* DOC : changelog entry for ModuleBuilder selectable object tabs
* FIX : purge agenda card widget when agenda tab is excluded
Audit finding: myobject_card.php hardcodes a 'SeeAll' link to myobject_agenda.php
in an event widget. When the agenda tab is excluded the page is not generated,
leaving a latent dead link if the widget is enabled. Wrap the widget with
MODULEBUILDER TAB AGENDA markers and purge it from the generated card when the
agenda tab is not selected.
Next: functional verification (step 5 of dev workflow)
* FIX : check file operation results when applying object tab selection
PR review: align with the nogeneratelines pattern by checking the return of
removePatternFromFile/dolReplaceInFile and raising $error + dol_syslog with the
failing tab/file context, instead of ignoring silent write failures during
object generation. Add PR reference to the changelog entry.
* FIX : do not abort object generation when nogeneratelines targets a missing file
The nogeneratelines handler purges the MODULEBUILDER LINES block from class,
API and card files. When API generation is disabled the api_<module>.class.php
file does not exist, so removePatternFromFile returns false and $error was
incremented, which skipped the whole success path (name substitution,
rebuildObjectClass, $tabobj assignment). Generated classes kept the literal
'class MyObject', so dolGetListOfObjectClasses listed every object as MyObject.
Guard each purge with file_exists so a missing optional file is skipped.
* FIX : keep module descriptor out of the blanket name substitution
The 'substitute all module php files' pass applied the object/module name map
to every .php file including the module descriptor. That rewrote the persistent
/* ... MODULEBUILDER TOPMENU/LEFTMENU MYOBJECT */ marker placeholders to the
first object name, so generating a second object could no longer find them
(checkExistComment returned -1, 'comments not found for section Menus' warning)
and its menu entries were not inserted. Skip the descriptor in that loop: its
module name is already resolved by initmodule and its object entries are added
by the dedicated menu/permission blocks.
* FIX : preserve MODULEBUILDER markers when substituting the module descriptor
The previous commit only skipped the descriptor in the secondary substitution
pass, but the primary pass (over $filetogenerate, which includes the descriptor)
still rewrote its persistent /* ... MODULEBUILDER TOPMENU/LEFTMENU MYOBJECT */
markers to the first object name. Those markers cannot simply be excluded from
substitution because the descriptor also has functional MYOBJECT/MYMODULE tokens
(e.g. the MYMODULE_MYOBJECT_ADDON numbering constant) that must be resolved.
Add dolReplaceInFilePreservingModuleBuilderMarkers(): it hides every MODULEBUILDER
marker behind a sentinel, applies the substitution, then restores the markers
verbatim. Use it for the descriptor in the primary pass; the descriptor stays
excluded from the secondary pass. Generating a second object now finds the
markers (checkExistComment) and inserts its menu entries without warning.
Verified by CLI: markers preserved across two successive objects, while content
(rights, exports, addon constant) is correctly substituted.
* FIX : silence phan on the new ModuleBuilder test and typed closure
CI phan (diff-only) flagged the freshly added test file: PhanUndeclaredExtendedClass
(\CommonClassTest) and PhanUndeclaredMethod (assertSame), like the sibling
NamingContractTest. Add the same @phan-file-suppress block (plus
PhanTypeMismatchArgumentProbablyReal for the non-array guard test case). Also
type the preg_replace_callback parameter (array $matches) to clear
PhanPluginUnknownClosureParamType. phpstan is already green; no phpstan
annotations added.
* FIX : drop closure in marker-preserving substitution to satisfy phan
phan's PhanPluginUnknownArrayClosureParamType wanted key/value types on the
preg_replace_callback closure parameter. Replace the callback with preg_match_all
plus a str_replace loop over the unique markers: same behaviour, no closure,
no plugin warning.
---------
Co-authored-by: Laurent Destailleur <eldy@destailleur.fr>
Categorie::containing() returns an integer (-1) when its SQL query fails
(e.g. the categorie_xxx table does not exist). The $remove_existing=false
branch of setCategoriesCommon() passed that int straight to array_diff(),
producing "array_diff(): Expected parameter 2 to be an array, int given"
and "Invalid argument supplied for foreach()" (a fatal TypeError on PHP 8),
which in turn broke the redirect with "headers already sent".
Normalize $existing to an empty array right after the containing() call so
both branches are safe, and drop the now-redundant inner is_array() guard.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Qual: Improve type hinting for $type (in hash related functions)
For $type parameter now use specific type hints.
* fix: Update type parameter documentation in dol_hash and dol_verifyHash functions
The $type parameter PHPDoc in the dol_hash and dol_verifyHash is updated to be more specific and include 'hash'
---------
Co-authored-by: Laurent Destailleur <eldy@destailleur.fr>
* Fix#27987 refuse to save signature when too-many-links sanitizer fires
When the user signature contains more inline links/images than
MAIN_SECURITY_MAX_IMG_IN_HTML_CONTENT allows, GETPOST('signature',
'restricthtml') returns the literal string 'ErrorTooManyLinksIntoHTMLString'
instead of the sanitized HTML (see dol_string_unicode in
htdocs/core/lib/functions.lib.php:8755-8757). On the user/card.php save
sites, that literal was assigned verbatim to $object->signature and
persisted; the next outgoing email then used the literal as the
signature block, so the customer received an email whose entire
signature was the string 'ErrorTooManyLinksIntoHTMLString'.
Detect the literal at both save sites (action == 'add' around line 316
and action == 'update' around line 499), increment $error, surface a
clear setEventMessages and route the user back to the form so the
signature is not persisted. The MAIN_SECURITY_MAX_IMG_IN_HTML_CONTENT
setting is the legitimate guard; this PR only ensures its rejection
output never reaches the database or downstream emails.
Signed-off-by: Dolicraft <contact@dolicraft.com>
* Add ErrorTooManyLinksIntoHTMLString en_US translation
pre-commit CI on PR #38534 (Fix#27987) flagged the new lang key
ErrorTooManyLinksIntoHTMLString used in user/card.php as missing.
Add the en_US entry so the existing helper text from the constant
MAIN_SECURITY_MAX_IMG_IN_HTML_CONTENT is reachable through
$langs->trans().
Signed-off-by: Dolicraft <contact@dolicraft.com>
---------
Signed-off-by: Dolicraft <contact@dolicraft.com>
Co-authored-by: Dolicraft <contact@dolicraft.com>
* Fix#29799 widen RUM truncation so customer code fits
BonPrelevement::buildRumNumber() builds an auto-generated SEPA UMR
when no UMR was supplied on a new bank-mandate row. The format is
{RUM/UMR}-{yymmddHHMM}-{drum_id-customer_code}
with the tail truncated to 13 characters. That truncation is too
aggressive: typical Dolibarr customer codes (CU1908-00002,
CU2509-00012, ...) are already 12 characters, so any drum id at all
pushes the tail past 13 and the customer code is cut mid-string.
The generated UMR then no longer matches the actual customer code,
which is the symptom in the report ("customer code CU1908-00002
becomes 1-CU1908-0000 in the UMR").
The SEPA standard allows up to 35 characters for MndtId. The
non-truncatable prefix (RUM/UMR + '-' + 10-char date + '-') uses
15 characters, leaving up to 20 characters for the tail before hitting
the 35-char limit, and 17 before hitting the more conservative 32-char
target the original comment referenced. Bump the truncation to 20:
the typical drum-id + 12-char customer code (14 chars) now fits
cleanly, and longer codes still get truncated but at a much later
point.
Only the generation of newly created mandates is affected. Existing
$companybankaccount->rum values stored in the database are not
touched, so no migration is needed.
Signed-off-by: Dolicraft <contact@dolicraft.com>
* Update bonprelevement.class.php
---------
Signed-off-by: Dolicraft <contact@dolicraft.com>
Co-authored-by: Dolicraft <contact@dolicraft.com>
Co-authored-by: Laurent Destailleur <eldy@destailleur.fr>