2017-07-11 23:55:07 +00:00
< ? php
/* Copyright ( C ) 2009 - 2010 Laurent Destailleur < eldy @ users . sourceforge . net >
2025-02-26 22:14:28 +00:00
* Copyright ( C ) 2024 - 2025 MDW < mdeweerd @ users . noreply . github . com >
2024-09-05 14:05:37 +00:00
* Copyright ( C ) 2024 Frédéric France < frederic . france @ free . fr >
2017-07-11 23:55:07 +00:00
*
* 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
* the Free Software Foundation ; either version 3 of the License , or
* ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU General Public License for more details .
*
* You should have received a copy of the GNU General Public License
2019-09-23 19:55:30 +00:00
* along with this program . If not , see < https :// www . gnu . org / licenses />.
* or see https :// www . gnu . org /
2017-07-11 23:55:07 +00:00
*/
/**
2021-11-07 12:51:31 +00:00
* \file htdocs / core / lib / modulebuilder . lib . php
* \brief Set of function for modulebuilder management
2017-07-11 23:55:07 +00:00
*/
NEW : Replace MyObject MyModule occurences (#38370)
* [COREMB] Centralize myobject/mymodule substitutions via NamingContract value object
Introduce NamingContract (immutable value object) and NamingContractValidator
to replace 6 scattered ad-hoc $arrayreplacement blocks across index.php and
modulebuilder.lib.php. Fixes a critical bug where MYOBJECT token was commented
out in initobject, leaving generated class files with unsubstituted placeholders.
- NamingContract: canonical ordered substitution map (12 tokens with object,
7 module-only), applyTo() via str_replace (not make_substitutions, to avoid
processing Dolibarr __(key)__ patterns in raw templates), applyToFilename()
for lowercase-only filename substitution
- StrictNamingContractValidator: post-generation scan for residual myobject/
mymodule tokens, skipping /* BEGIN/END MODULEBUILDER */ structural markers
- modulebuilderValidateGeneratedFile() helper: non-blocking warnings with
XSS-safe output via dol_escape_htmltag()
- Collision guard: InvalidArgumentException if module === object name
(case-insensitive), wrapped in try-catch at all instantiation sites
- 24 PHPUnit tests, 52 assertions
* fix(initobject): apply object substitution to all module PHP files
After initmodule creates the module structure, files like index.php,
lib/module.lib.php, admin/setup.php and class/api_module.class.php
still contain myobject/mymodule placeholders. initobject now scans
ALL PHP files in the module directory and applies the full NamingContract
substitution, and deletes the orphan stats/myobject_index.php placeholder.
* fix(initmodule): prevent array_merge from renumbering the '500000' key
PHP casts numeric string keys like '500000' to int(500000) internally.
array_merge() then renumbers integer keys starting from 0, turning
'500000' into key 0. make_substitutions() then calls str_replace(0, ...)
which casts 0 to the string '0', replacing EVERY zero digit in every
generated file with the idmodule value.
Fix: replace the bare '500000' key with '$this->numero = 500000' so that
array_merge sees a non-numeric string key and preserves it as-is.
* Fix initapi: apply header substitution after addObjectsToApiFile
addObjectsToApiFile substitutes properties/constructor/includes via regex
but leaves PHPDoc header and class declaration with mymodule/myobject tokens.
Fix: apply $arrayreplacement (minus 'MYOBJECT' uppercase key) after
addObjectsToApiFile to resolve the remaining residuals.
'MYOBJECT' is excluded to preserve the /* BEGIN MODULEBUILDER API MYOBJECT */
placeholder that addObjectsToApiFile relies on for future object additions.
* Move NamingContractTest to core test directory
NamingContract and NamingContractValidator are core modulebuilder classes;
their tests belong alongside other Dolibarr core tests in test/phpunit/,
not in htdocs/modulebuilder/test/phpunit/ (the custom-module pattern).
Updated require_once paths accordingly.
* fix useless file
* fix(phpcs): add missing @param descriptions and function docblocks
- NamingContract: merge adjacent string literals in exception message,
add description to @param $content in applyTo()
- NamingContractValidator: add descriptions to all bare @param tags
in interface and StrictNamingContractValidator methods
- NamingContractTest: add /** @return void */ docblocks to all test
methods, merge unnecessary string concat on template variable
* fix(phan/phpstan): remove readonly, add baselines for test file
- NamingContract: replace readonly properties with plain public string
(readonly requires PHP 8.1, phan minimum target is PHP 7.2)
- dev/tools/phan/baseline.txt: add NamingContractTest.php suppression
for PhanUndeclaredMethod (PHPUnit stubs not available in phan scope)
- NamingContractTest.php: add @phpstan-ignore class.notFound on class
declaration and method.notFound on each assertion call (PHPUnit not
in phpstan bootstrap, consistent with CommonClassTest.class.php)
* fix(phan/phpstan): suppress typed property warnings, NoopNew, class.notFound
- baseline.txt: add PhanCompatibleTypedProperty for NamingContract.class.php
(typed properties require PHP 7.4, phan min target is 7.2 — warning only
but phan exits 1 on any issue including warnings)
- baseline.txt: add PhanNoopNew for NamingContractTest.php (constructor
called for exception side-effect in guard tests is intentional)
- NamingContractTest.php: move @phpstan-ignore class.notFound to inline
comment on class declaration (docblock placement not picked up by phpstan)
* fix(phan/phpstan): add missing phan suppress on index.php l.574, fix phpstan class docblock
- index.php l.574: add @phan-suppress-next-line PhanPluginSuspiciousParamPosition
(same pattern as l.577 and l.587 for identical dolReplaceInFile calls
where local variable name $destfile matches parameter #3 of the function)
- NamingContractTest.php: use standalone /** @phpstan-ignore class.notFound */
docblock before class declaration (inline // comment not picked up by phpstan
on class declarations; separate docblock matches CommonClassTest.class.php pattern)
* Update baseline.txt
* fix(phan): add phan-file-suppress on NamingContractTest for PHPUnit undeclared methods
* fix(phan): suppress PhanNoopNew on expectException patterns in NamingContractTest
---------
Co-authored-by: Laurent Destailleur <eldy@destailleur.fr>
2026-05-25 12:52:38 +00:00
require_once DOL_DOCUMENT_ROOT . '/modulebuilder/class/NamingContract.class.php' ;
2017-07-11 23:55:07 +00:00
/**
2017-09-05 18:42:34 +00:00
* Regenerate files . class . php
2017-07-11 23:55:07 +00:00
*
2024-08-18 16:16:08 +00:00
* @ param string $destdir Directory
* @ param string $module Module name
* @ param string $objectname Name of object
* @ param string $newmask New mask
* @ param string $readdir Directory source ( use $destdir when not defined )
2024-03-30 23:36:03 +00:00
* @ param array {} | array { name : string , key : string , type : string , label : string , picot ? : string , enabled : int < 0 , 1 > , notnull : int < 0 , 1 > , position : int , visible : int , noteditable ? : int < 0 , 1 > , alwayseditable ? : int < 0 , 1 > , default ? : string , index ? : int , foreignkey ? : string , searchall ? : int , isameasure ? : int < 0 , 1 > , css ? : string , cssview ? : string , csslist ? : string , help ? : string , showoncombobox ? : int < 0 , 1 > , disabled ? : int < 0 , 1 > , autofocusoncreate ? : int < 0 , 1 > , arrayofkeyval ? : array < string , string > , validate ? : int < 0 , 1 > , comment ? : string } $addfieldentry Array of 1 field entry to add
2024-08-18 16:16:08 +00:00
* @ param string $delfieldentry Id of field to remove
* @ return int <- 7 , - 1 >| CommonObject Return integer <= 0 if KO , Object if OK
2019-03-11 00:01:15 +00:00
* @ see rebuildObjectSql ()
2017-07-11 23:55:07 +00:00
*/
2019-01-27 14:20:16 +00:00
function rebuildObjectClass ( $destdir , $module , $objectname , $newmask , $readdir = '' , $addfieldentry = array (), $delfieldentry = '' )
2017-07-11 23:55:07 +00:00
{
2020-04-05 00:44:59 +00:00
global $db , $langs ;
2021-02-23 21:03:23 +00:00
if ( empty ( $objectname )) {
2022-07-15 13:35:45 +00:00
return - 6 ;
2021-02-23 21:03:23 +00:00
}
if ( empty ( $readdir )) {
$readdir = $destdir ;
}
2020-04-05 00:44:59 +00:00
2021-02-23 21:03:23 +00:00
if ( ! empty ( $addfieldentry [ 'arrayofkeyval' ]) && ! is_array ( $addfieldentry [ 'arrayofkeyval' ])) {
2024-01-20 08:22:38 +00:00
dol_print_error ( null , 'Bad parameter addfieldentry with a property arrayofkeyval defined but that is not an array.' );
2022-07-15 13:35:45 +00:00
return - 7 ;
2020-04-05 00:44:59 +00:00
}
2022-07-15 13:35:45 +00:00
$error = 0 ;
2025-08-04 22:45:00 +00:00
// Check parameters into $addfieldentry (this provided array is filled by modulebuilder/index.php)
2021-03-02 14:27:20 +00:00
if ( is_array ( $addfieldentry ) && count ( $addfieldentry ) > 0 ) {
2021-02-23 21:03:23 +00:00
if ( empty ( $addfieldentry [ 'name' ])) {
2020-04-05 00:44:59 +00:00
setEventMessages ( $langs -> trans ( 'ErrorFieldRequired' , $langs -> transnoentitiesnoconv ( " Name " )), null , 'errors' );
return - 2 ;
}
2021-02-23 21:03:23 +00:00
if ( empty ( $addfieldentry [ 'label' ])) {
2020-04-05 00:44:59 +00:00
setEventMessages ( $langs -> trans ( 'ErrorFieldRequired' , $langs -> transnoentitiesnoconv ( " Label " )), null , 'errors' );
return - 2 ;
}
2024-10-04 16:37:41 +00:00
if ( ! preg_match ( '/^(integer|price|sellist|varchar|double|text|html|duration|stars)/' , $addfieldentry [ 'type' ])
2025-08-04 22:45:00 +00:00
&& ! preg_match ( '/^(boolean|smallint|real|date|datetime|timestamp|phone|email|url|ip|password)$/' , $addfieldentry [ 'type' ])) { // Use email for email, mail is kept for compatibility
2022-07-15 13:35:45 +00:00
setEventMessages ( $langs -> trans ( 'BadValueForType' , $addfieldentry [ 'type' ]), null , 'errors' );
2020-04-05 00:44:59 +00:00
return - 2 ;
}
2024-10-04 16:37:41 +00:00
// Check for type stars(NumberOfStars), NumberOfStars must be an integer between 1 and 10
2025-08-04 22:45:00 +00:00
$matches = array ();
2024-10-04 16:37:41 +00:00
if ( preg_match ( '/^stars\((.+)\)$/' , $addfieldentry [ 'type' ], $matches )) {
if ( ! ctype_digit ( $matches [ 1 ]) || $matches [ 1 ] < 1 || $matches [ 1 ] > 10 ) {
setEventMessages ( $langs -> trans ( 'BadValueForType' , $addfieldentry [ 'type' ]), null , 'errors' );
return - 2 ;
}
}
2020-04-05 00:44:59 +00:00
}
$pathoffiletoeditsrc = $readdir . '/class/' . strtolower ( $objectname ) . '.class.php' ;
$pathoffiletoedittarget = $destdir . '/class/' . strtolower ( $objectname ) . '.class.php' . ( $readdir != $destdir ? '.new' : '' );
2021-02-23 21:03:23 +00:00
if ( ! dol_is_file ( $pathoffiletoeditsrc )) {
2020-04-05 00:44:59 +00:00
$langs -> load ( " errors " );
setEventMessages ( $langs -> trans ( " ErrorFileNotFound " , $pathoffiletoeditsrc ), null , 'errors' );
return - 3 ;
}
2024-08-18 16:16:08 +00:00
//$pathoffiletoedittmp = $destdir.'/class/'.strtolower($objectname).'.class.php.tmp';
2020-04-05 00:44:59 +00:00
//dol_delete_file($pathoffiletoedittmp, 0, 1, 1);
2020-05-20 23:41:27 +00:00
try {
2020-04-05 00:44:59 +00:00
include_once $pathoffiletoeditsrc ;
2021-02-23 21:03:23 +00:00
if ( class_exists ( $objectname )) {
$object = new $objectname ( $db );
} else {
return - 4 ;
}
2024-03-30 23:36:03 +00:00
'@phan-var-force CommonObject $object' ;
2020-04-05 00:44:59 +00:00
// Backup old file
dol_copy ( $pathoffiletoedittarget , $pathoffiletoedittarget . '.back' , $newmask , 1 );
// Edit class files
2024-03-18 02:55:52 +00:00
$contentclass = file_get_contents ( dol_osencode ( $pathoffiletoeditsrc ));
2020-04-05 00:44:59 +00:00
2022-09-26 23:05:22 +00:00
// Update ->fields (to add or remove entries defined into $addfieldentry)
2021-02-23 21:03:23 +00:00
if ( count ( $object -> fields )) {
if ( is_array ( $addfieldentry ) && count ( $addfieldentry )) {
2019-11-19 08:30:26 +00:00
$name = $addfieldentry [ 'name' ];
2020-04-05 00:44:59 +00:00
unset ( $addfieldentry [ 'name' ]);
$object -> fields [ $name ] = $addfieldentry ;
}
2021-02-23 21:03:23 +00:00
if ( ! empty ( $delfieldentry )) {
2020-04-05 00:44:59 +00:00
$name = $delfieldentry ;
unset ( $object -> fields [ $name ]);
}
}
dol_sort_array ( $object -> fields , 'position' );
$i = 0 ;
$texttoinsert = '// BEGIN MODULEBUILDER PROPERTIES' . " \n " ;
$texttoinsert .= " \t " . '/**' . " \n " ;
2024-08-18 16:16:08 +00:00
$texttoinsert .= " \t " . ' * @inheritdoc' . " \n " ;
$texttoinsert .= " \t " . ' * Array with all fields and their property. Do not use it as a static var. It may be modified by constructor.' . " \n " ;
2020-04-05 00:44:59 +00:00
$texttoinsert .= " \t " . ' */' . " \n " ;
2024-08-18 16:16:08 +00:00
$texttoinsert .= " \t " . 'public $fields = array(' . " \n " ;
2020-04-05 00:44:59 +00:00
2021-02-23 21:03:23 +00:00
if ( count ( $object -> fields )) {
foreach ( $object -> fields as $key => $val ) {
2020-04-05 00:44:59 +00:00
$i ++ ;
2023-10-13 22:47:25 +00:00
$texttoinsert .= " \t \t " . '"' . $key . '" => array(' ;
2024-08-18 16:16:08 +00:00
$texttoinsert .= '"type" => "' . dol_escape_php ( $val [ 'type' ]) . '",' ;
$texttoinsert .= ' "label" => "' . dol_escape_php ( $val [ 'label' ]) . '",' ;
2022-09-01 11:10:25 +00:00
if ( ! empty ( $val [ 'picto' ])) {
2024-08-18 16:16:08 +00:00
$texttoinsert .= ' "picto" => "' . dol_escape_php ( $val [ 'picto' ]) . '",' ;
2022-05-01 15:58:37 +00:00
}
2024-08-18 16:16:08 +00:00
$texttoinsert .= ' "enabled" => "' . ( $val [ 'enabled' ] !== '' ? dol_escape_php ( $val [ 'enabled' ]) : 1 ) . '",' ;
$texttoinsert .= " 'position' => " . ( $val [ 'position' ] !== '' ? ( int ) $val [ 'position' ] : 50 ) . " , " ;
$texttoinsert .= " 'notnull' => " . ( empty ( $val [ 'notnull' ]) ? 0 : ( int ) $val [ 'notnull' ]) . " , " ;
$texttoinsert .= ' "visible" => "' . ( $val [ 'visible' ] !== '' ? dol_escape_js ( $val [ 'visible' ]) : - 1 ) . '",' ;
2022-02-21 14:41:41 +00:00
if ( ! empty ( $val [ 'noteditable' ])) {
2025-02-26 22:14:28 +00:00
$texttoinsert .= ' "noteditable" => "' . dol_escape_php (( string ) $val [ 'noteditable' ]) . '",' ;
2021-02-23 21:03:23 +00:00
}
2022-09-26 23:05:22 +00:00
if ( ! empty ( $val [ 'alwayseditable' ])) {
2025-02-26 22:14:28 +00:00
$texttoinsert .= ' "alwayseditable" => "' . dol_escape_php (( string ) $val [ 'alwayseditable' ]) . '",' ;
2022-09-26 23:05:22 +00:00
}
2024-03-30 23:36:03 +00:00
if ( array_key_exists ( 'default' , $val ) && ( ! empty ( $val [ 'default' ]) || $val [ 'default' ] === '0' )) {
2024-08-18 16:16:08 +00:00
$texttoinsert .= ' "default" => "' . dol_escape_php ( $val [ 'default' ]) . '",' ;
2021-02-23 21:03:23 +00:00
}
2022-02-21 14:41:41 +00:00
if ( ! empty ( $val [ 'index' ])) {
2024-08-18 16:16:08 +00:00
$texttoinsert .= ' "index" => "' . ( int ) $val [ 'index' ] . '",' ;
2021-02-23 21:03:23 +00:00
}
2022-02-21 14:41:41 +00:00
if ( ! empty ( $val [ 'foreignkey' ])) {
2024-08-18 16:16:08 +00:00
$texttoinsert .= ' "foreignkey" => "' . ( int ) $val [ 'foreignkey' ] . '",' ;
2021-02-23 21:03:23 +00:00
}
2022-02-21 14:41:41 +00:00
if ( ! empty ( $val [ 'searchall' ])) {
2024-08-18 16:16:08 +00:00
$texttoinsert .= ' "searchall" => "' . ( int ) $val [ 'searchall' ] . '",' ;
2021-02-23 21:03:23 +00:00
}
2022-02-21 14:41:41 +00:00
if ( ! empty ( $val [ 'isameasure' ])) {
2024-08-18 16:16:08 +00:00
$texttoinsert .= ' "isameasure" => "' . ( int ) $val [ 'isameasure' ] . '",' ;
2021-02-23 21:03:23 +00:00
}
2022-02-21 14:41:41 +00:00
if ( ! empty ( $val [ 'css' ])) {
2024-08-18 16:16:08 +00:00
$texttoinsert .= ' "css" => "' . dol_escape_php ( $val [ 'css' ]) . '",' ;
2021-02-23 21:03:23 +00:00
}
2022-02-21 14:41:41 +00:00
if ( ! empty ( $val [ 'cssview' ])) {
2024-08-18 16:16:08 +00:00
$texttoinsert .= ' "cssview" => "' . dol_escape_php ( $val [ 'cssview' ]) . '",' ;
2021-04-06 16:51:43 +00:00
}
2022-02-21 14:41:41 +00:00
if ( ! empty ( $val [ 'csslist' ])) {
2024-08-18 16:16:08 +00:00
$texttoinsert .= ' "csslist" => "' . dol_escape_php ( $val [ 'csslist' ]) . '",' ;
2021-04-06 16:51:43 +00:00
}
2022-02-21 14:41:41 +00:00
if ( ! empty ( $val [ 'help' ])) {
2024-08-18 16:16:08 +00:00
$texttoinsert .= ' "help" => "' . dol_escape_php ( $val [ 'help' ]) . '",' ;
2021-02-23 21:03:23 +00:00
}
2022-02-21 14:41:41 +00:00
if ( ! empty ( $val [ 'showoncombobox' ])) {
2024-08-18 16:16:08 +00:00
$texttoinsert .= ' "showoncombobox" => "' . ( int ) $val [ 'showoncombobox' ] . '",' ;
2021-02-23 21:03:23 +00:00
}
2022-02-21 14:41:41 +00:00
if ( ! empty ( $val [ 'disabled' ])) {
2024-08-18 16:16:08 +00:00
$texttoinsert .= ' "disabled" => "' . ( int ) $val [ 'disabled' ] . '",' ;
2021-02-23 21:03:23 +00:00
}
2022-02-21 14:41:41 +00:00
if ( ! empty ( $val [ 'autofocusoncreate' ])) {
2024-08-18 16:16:08 +00:00
$texttoinsert .= ' "autofocusoncreate" => "' . ( int ) $val [ 'autofocusoncreate' ] . '",' ;
2021-02-23 21:03:23 +00:00
}
2022-02-21 14:41:41 +00:00
if ( ! empty ( $val [ 'arrayofkeyval' ])) {
2024-08-18 16:16:08 +00:00
$texttoinsert .= ' "arrayofkeyval" => array(' ;
2020-04-05 00:44:59 +00:00
$i = 0 ;
2021-02-23 21:03:23 +00:00
foreach ( $val [ 'arrayofkeyval' ] as $key2 => $val2 ) {
if ( $i ) {
$texttoinsert .= " , " ;
}
2023-10-13 22:47:25 +00:00
$texttoinsert .= '"' . dol_escape_php ( $key2 ) . '" => "' . dol_escape_php ( $val2 ) . '"' ;
2020-04-05 00:44:59 +00:00
$i ++ ;
}
2023-10-13 22:47:25 +00:00
$texttoinsert .= '),' ;
2020-04-05 00:44:59 +00:00
}
2022-02-21 14:41:41 +00:00
if ( ! empty ( $val [ 'validate' ])) {
2024-08-18 16:16:08 +00:00
$texttoinsert .= ' "validate" => "' . ( int ) $val [ 'validate' ] . '",' ;
2021-07-11 12:05:26 +00:00
}
2022-02-21 14:41:41 +00:00
if ( ! empty ( $val [ 'comment' ])) {
2024-08-18 16:16:08 +00:00
$texttoinsert .= ' "comment" => "' . dol_escape_php ( $val [ 'comment' ]) . '"' ;
2021-02-23 21:03:23 +00:00
}
2020-04-05 00:44:59 +00:00
$texttoinsert .= " ), \n " ;
2022-09-26 23:05:22 +00:00
//print $texttoinsert;
2020-04-05 00:44:59 +00:00
}
}
$texttoinsert .= " \t " . ');' . " \n " ;
2017-09-23 16:17:34 +00:00
//print ($texttoinsert);exit;
2017-07-12 09:52:07 +00:00
2021-02-23 21:03:23 +00:00
if ( count ( $object -> fields )) {
2024-08-18 16:16:08 +00:00
//$typetotypephp = array('integer' => 'integer', 'duration' => 'integer', 'varchar' => 'string');
2020-04-05 00:44:59 +00:00
2021-02-23 21:03:23 +00:00
foreach ( $object -> fields as $key => $val ) {
2020-04-05 00:44:59 +00:00
$i ++ ;
2024-08-18 16:16:08 +00:00
//$typephp = $typetotypephp[$val['type']];
2020-04-05 00:44:59 +00:00
$texttoinsert .= " \t " . 'public $' . $key . " ; " ;
//if ($key == 'rowid') $texttoinsert.= ' AUTO_INCREMENT PRIMARY KEY';
//if ($key == 'entity') $texttoinsert.= ' DEFAULT 1';
//$texttoinsert.= ($val['notnull']?' NOT NULL':'');
2024-08-18 16:16:08 +00:00
//if ($i < count($object->fields)) $texttoinsert. = ";";
2020-04-05 00:44:59 +00:00
$texttoinsert .= " \n " ;
}
}
$texttoinsert .= " \t " . '// END MODULEBUILDER PROPERTIES' ;
2022-09-26 23:05:22 +00:00
//print($texttoinsert);
2020-04-05 00:44:59 +00:00
$contentclass = preg_replace ( '/\/\/ BEGIN MODULEBUILDER PROPERTIES.*END MODULEBUILDER PROPERTIES/ims' , $texttoinsert , $contentclass );
2022-09-26 23:05:22 +00:00
//print $contentclass;
2020-04-05 00:44:59 +00:00
dol_mkdir ( dirname ( $pathoffiletoedittarget ));
//file_put_contents($pathoffiletoedittmp, $contentclass);
2022-07-15 13:35:45 +00:00
$result = file_put_contents ( dol_osencode ( $pathoffiletoedittarget ), $contentclass );
2022-09-26 23:05:22 +00:00
2022-07-15 13:35:45 +00:00
if ( $result ) {
2023-02-17 18:30:50 +00:00
dolChmod ( $pathoffiletoedittarget , $newmask );
2022-07-15 13:35:45 +00:00
} else {
$error ++ ;
}
2020-04-05 00:44:59 +00:00
2022-07-15 13:35:45 +00:00
return $error ? - 1 : $object ;
2021-02-23 21:03:23 +00:00
} catch ( Exception $e ) {
2020-04-05 00:44:59 +00:00
print $e -> getMessage ();
return - 5 ;
}
2017-07-12 09:52:07 +00:00
}
/**
* Save data into a memory area shared by all users , all sessions on server
*
2024-08-18 16:16:08 +00:00
* @ param string $destdir Directory
* @ param string $module Module name
* @ param string $objectname Name of object
* @ param string $newmask New mask
* @ param string $readdir Directory source ( use $destdir when not defined )
* @ param Object $object If object was already loaded / known , it is pass to avoid another include and new .
* @ param string $moduletype 'external' or 'internal'
* @ return int Return integer <= 0 if KO , > 0 if OK
2019-03-11 00:01:15 +00:00
* @ see rebuildObjectClass ()
2017-07-12 09:52:07 +00:00
*/
2020-08-06 13:55:04 +00:00
function rebuildObjectSql ( $destdir , $module , $objectname , $newmask , $readdir = '' , $object = null , $moduletype = 'external' )
2017-07-12 09:52:07 +00:00
{
2020-04-05 00:44:59 +00:00
global $db , $langs ;
2017-07-12 09:52:07 +00:00
2020-04-05 00:44:59 +00:00
$error = 0 ;
2017-09-23 16:17:34 +00:00
2021-02-23 21:03:23 +00:00
if ( empty ( $objectname )) {
return - 1 ;
}
if ( empty ( $readdir )) {
$readdir = $destdir ;
}
2017-08-05 09:35:42 +00:00
2020-04-05 00:44:59 +00:00
$pathoffiletoclasssrc = $readdir . '/class/' . strtolower ( $objectname ) . '.class.php' ;
2017-08-05 09:44:27 +00:00
2020-04-05 00:44:59 +00:00
// Edit .sql file
2020-08-06 13:55:04 +00:00
if ( $moduletype == 'internal' ) {
2022-07-15 13:35:45 +00:00
$pathoffiletoeditsrc = '/../install/mysql/tables/llx_' . strtolower ( $module ) . '_' . strtolower ( $objectname ) . '.sql' ;
if ( ! dol_is_file ( $readdir . $pathoffiletoeditsrc )) {
$pathoffiletoeditsrc = '/../install/mysql/tables/llx_' . strtolower ( $module ) . '_' . strtolower ( $objectname ) . '-' . strtolower ( $module ) . '.sql' ;
if ( ! dol_is_file ( $readdir . $pathoffiletoeditsrc )) {
$pathoffiletoeditsrc = '/../install/mysql/tables/llx_' . strtolower ( $module ) . '-' . strtolower ( $module ) . '.sql' ;
if ( ! dol_is_file ( $readdir . $pathoffiletoeditsrc )) {
$pathoffiletoeditsrc = '/../install/mysql/tables/llx_' . strtolower ( $module ) . '.sql' ;
}
}
}
2020-08-06 13:55:04 +00:00
} else {
2022-07-15 13:35:45 +00:00
$pathoffiletoeditsrc = '/sql/llx_' . strtolower ( $module ) . '_' . strtolower ( $objectname ) . '.sql' ;
if ( ! dol_is_file ( $readdir . $pathoffiletoeditsrc )) {
$pathoffiletoeditsrc = '/sql/llx_' . strtolower ( $module ) . '_' . strtolower ( $objectname ) . '-' . strtolower ( $module ) . '.sql' ;
if ( ! dol_is_file ( $readdir . $pathoffiletoeditsrc )) {
$pathoffiletoeditsrc = '/sql/llx_' . strtolower ( $module ) . '-' . strtolower ( $module ) . '.sql' ;
if ( ! dol_is_file ( $readdir . $pathoffiletoeditsrc )) {
$pathoffiletoeditsrc = '/sql/llx_' . strtolower ( $module ) . '.sql' ;
}
}
}
2020-08-06 13:55:04 +00:00
}
2022-07-15 13:35:45 +00:00
// Complete path to be full path
$pathoffiletoedittarget = $destdir . $pathoffiletoeditsrc . ( $readdir != $destdir ? '.new' : '' );
$pathoffiletoeditsrc = $readdir . $pathoffiletoeditsrc ;
2021-02-23 21:03:23 +00:00
if ( ! dol_is_file ( $pathoffiletoeditsrc )) {
2020-04-05 00:44:59 +00:00
$langs -> load ( " errors " );
setEventMessages ( $langs -> trans ( " ErrorFileNotFound " , $pathoffiletoeditsrc ), null , 'errors' );
return - 1 ;
}
// Load object from myobject.class.php
2020-05-20 23:41:27 +00:00
try {
2021-02-23 21:03:23 +00:00
if ( ! is_object ( $object )) {
2020-04-05 00:44:59 +00:00
include_once $pathoffiletoclasssrc ;
2021-02-23 21:03:23 +00:00
if ( class_exists ( $objectname )) {
$object = new $objectname ( $db );
} else {
return - 1 ;
}
2020-04-05 00:44:59 +00:00
}
2021-02-23 21:03:23 +00:00
} catch ( Exception $e ) {
2020-04-05 00:44:59 +00:00
print $e -> getMessage ();
}
// Backup old file
dol_copy ( $pathoffiletoedittarget , $pathoffiletoedittarget . '.back' , $newmask , 1 );
2024-03-18 02:55:52 +00:00
$contentsql = file_get_contents ( dol_osencode ( $pathoffiletoeditsrc ));
2020-04-05 00:44:59 +00:00
$i = 0 ;
$texttoinsert = '-- BEGIN MODULEBUILDER FIELDS' . " \n " ;
2021-02-23 21:03:23 +00:00
if ( count ( $object -> fields )) {
foreach ( $object -> fields as $key => $val ) {
2020-04-05 00:44:59 +00:00
$i ++ ;
$type = $val [ 'type' ];
$type = preg_replace ( '/:.*$/' , '' , $type ); // For case type = 'integer:Societe:societe/class/societe.class.php'
2021-02-23 21:03:23 +00:00
if ( $type == 'html' ) {
$type = 'text' ; // html modulebuilder type is a text type in database
} elseif ( $type == 'price' ) {
$type = 'double' ; // html modulebuilder type is a text type in database
} elseif ( in_array ( $type , array ( 'link' , 'sellist' , 'duration' ))) {
$type = 'integer' ;
2024-11-15 22:37:41 +00:00
} elseif ( $type == 'chkbxlst' ) {
$type = 'varchar(128)' ;
2025-08-04 22:45:00 +00:00
} elseif ( $type == 'mail' || $type == 'email' ) { // Prefer to use 'email'
2023-10-14 14:23:28 +00:00
$type = 'varchar(128)' ;
2024-10-04 16:37:41 +00:00
} elseif ( strpos ( $type , 'stars(' ) === 0 ) {
$type = 'integer' ;
2023-10-14 14:23:28 +00:00
} elseif ( $type == 'phone' ) {
$type = 'varchar(20)' ;
} elseif ( $type == 'ip' ) {
$type = 'varchar(32)' ;
2025-08-04 22:45:00 +00:00
} elseif ( $type == 'url' ) {
$type = 'varchar(255)' ;
2021-02-23 21:03:23 +00:00
}
2023-10-14 14:23:28 +00:00
2020-04-05 00:44:59 +00:00
$texttoinsert .= " \t " . $key . " " . $type ;
2021-02-23 21:03:23 +00:00
if ( $key == 'rowid' ) {
$texttoinsert .= ' AUTO_INCREMENT PRIMARY KEY' ;
2022-01-10 23:55:54 +00:00
} elseif ( $type == 'timestamp' ) {
$texttoinsert .= ' DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP' ;
2021-02-23 21:03:23 +00:00
}
if ( $key == 'entity' ) {
$texttoinsert .= ' DEFAULT 1' ;
} else {
2022-02-22 15:35:10 +00:00
if ( ! empty ( $val [ 'default' ])) {
2021-02-23 21:03:23 +00:00
if ( preg_match ( '/^null$/i' , $val [ 'default' ])) {
$texttoinsert .= " DEFAULT NULL " ;
} elseif ( preg_match ( '/varchar/' , $type )) {
$texttoinsert .= " DEFAULT ' " . $db -> escape ( $val [ 'default' ]) . " ' " ;
} else {
$texttoinsert .= (( $val [ 'default' ] > 0 ) ? ' DEFAULT ' . $val [ 'default' ] : '' );
}
2020-04-05 00:44:59 +00:00
}
}
2022-11-26 23:44:05 +00:00
$texttoinsert .= (( ! empty ( $val [ 'notnull' ]) && $val [ 'notnull' ] > 0 ) ? ' NOT NULL' : '' );
2021-02-23 21:03:23 +00:00
if ( $i < count ( $object -> fields )) {
$texttoinsert .= " , " ;
}
2020-04-05 00:44:59 +00:00
$texttoinsert .= " \n " ;
}
}
$texttoinsert .= " \t " . '-- END MODULEBUILDER FIELDS' ;
$contentsql = preg_replace ( '/-- BEGIN MODULEBUILDER FIELDS.*END MODULEBUILDER FIELDS/ims' , $texttoinsert , $contentsql );
$result = file_put_contents ( $pathoffiletoedittarget , $contentsql );
2021-02-23 21:03:23 +00:00
if ( $result ) {
2023-02-17 18:30:50 +00:00
dolChmod ( $pathoffiletoedittarget , $newmask );
2020-05-21 13:05:19 +00:00
} else {
2020-04-05 00:44:59 +00:00
$error ++ ;
2022-07-15 13:35:45 +00:00
setEventMessages ( $langs -> trans ( " ErrorFailToCreateFile " , $pathoffiletoedittarget ), null , 'errors' );
2020-04-05 00:44:59 +00:00
}
// Edit .key.sql file
2022-07-15 13:35:45 +00:00
$pathoffiletoeditsrc = preg_replace ( '/\.sql$/' , '.key.sql' , $pathoffiletoeditsrc );
$pathoffiletoedittarget = preg_replace ( '/\.sql$/' , '.key.sql' , $pathoffiletoedittarget );
$pathoffiletoedittarget = preg_replace ( '/\.sql.new$/' , '.key.sql.new' , $pathoffiletoedittarget );
2020-04-05 00:44:59 +00:00
2024-03-18 02:55:52 +00:00
$contentsql = file_get_contents ( dol_osencode ( $pathoffiletoeditsrc ));
2020-04-05 00:44:59 +00:00
$i = 0 ;
$texttoinsert = '-- BEGIN MODULEBUILDER INDEXES' . " \n " ;
2021-02-23 21:03:23 +00:00
if ( count ( $object -> fields )) {
foreach ( $object -> fields as $key => $val ) {
2020-04-05 00:44:59 +00:00
$i ++ ;
2021-02-23 21:03:23 +00:00
if ( ! empty ( $val [ 'index' ])) {
2024-12-23 23:56:22 +00:00
$texttoinsert .= " ALTER TABLE llx_ " . strtolower ( $module ) . '_' . strtolower ( $objectname ) . " ADD " . ( $key == 'ref' ? " UNIQUE INDEX uk_ " : " INDEX idx_ " ) . strtolower ( $module ) . '_' . strtolower ( $objectname ) . " _ " . $key . " ( " . $key . ( $key == 'ref' && array_key_exists ( 'entity' , $object -> fields ) ? " , entity " : " " ) . " ); " ;
2020-04-05 00:44:59 +00:00
$texttoinsert .= " \n " ;
}
2021-02-23 21:03:23 +00:00
if ( ! empty ( $val [ 'foreignkey' ])) {
2020-04-05 00:44:59 +00:00
$tmp = explode ( '.' , $val [ 'foreignkey' ]);
2021-02-23 21:03:23 +00:00
if ( ! empty ( $tmp [ 0 ]) && ! empty ( $tmp [ 1 ])) {
2020-04-05 00:44:59 +00:00
$texttoinsert .= " ALTER TABLE llx_ " . strtolower ( $module ) . '_' . strtolower ( $objectname ) . " ADD CONSTRAINT llx_ " . strtolower ( $module ) . '_' . strtolower ( $objectname ) . " _ " . $key . " FOREIGN KEY ( " . $key . " ) REFERENCES llx_ " . preg_replace ( '/^llx_/' , '' , $tmp [ 0 ]) . " ( " . $tmp [ 1 ] . " ); " ;
$texttoinsert .= " \n " ;
}
}
}
}
$texttoinsert .= '-- END MODULEBUILDER INDEXES' ;
$contentsql = preg_replace ( '/-- BEGIN MODULEBUILDER INDEXES.*END MODULEBUILDER INDEXES/ims' , $texttoinsert , $contentsql );
dol_mkdir ( dirname ( $pathoffiletoedittarget ));
$result2 = file_put_contents ( $pathoffiletoedittarget , $contentsql );
2022-07-15 13:35:45 +00:00
if ( $result2 ) {
2023-02-17 18:30:50 +00:00
dolChmod ( $pathoffiletoedittarget , $newmask );
2020-05-21 13:05:19 +00:00
} else {
2020-04-05 00:44:59 +00:00
$error ++ ;
2022-07-15 13:35:45 +00:00
setEventMessages ( $langs -> trans ( " ErrorFailToCreateFile " , $pathoffiletoedittarget ), null , 'errors' );
2020-04-05 00:44:59 +00:00
}
return $error ? - 1 : 1 ;
2017-07-11 23:55:07 +00:00
}
2023-03-15 09:33:42 +00:00
/**
2024-05-15 15:12:16 +00:00
* Get list of existing objects from a directory
2023-03-18 01:39:02 +00:00
*
2024-08-18 16:16:08 +00:00
* @ param string $destdir Directory
* @ return string [] | int Return integer <= 0 if KO , array if OK
2023-03-13 11:39:28 +00:00
*/
2023-03-14 13:48:48 +00:00
function dolGetListOfObjectClasses ( $destdir )
2023-03-13 11:39:28 +00:00
{
$objects = array ();
$listofobject = dol_dir_list ( $destdir . '/class' , 'files' , 0 , '\.class\.php$' );
foreach ( $listofobject as $fileobj ) {
if ( preg_match ( '/^api_/' , $fileobj [ 'name' ])) {
continue ;
}
if ( preg_match ( '/^actions_/' , $fileobj [ 'name' ])) {
continue ;
}
$tmpcontent = file_get_contents ( $fileobj [ 'fullname' ]);
$reg = array ();
if ( preg_match ( '/class\s+([^\s]*)\s+extends\s+CommonObject/ims' , $tmpcontent , $reg )) {
$objectnameloop = $reg [ 1 ];
$objects [ $fileobj [ 'fullname' ]] = $objectnameloop ;
}
}
2024-03-07 19:16:48 +00:00
if ( count ( $objects ) > 0 ) {
2023-03-13 11:39:28 +00:00
return $objects ;
2023-03-18 09:20:03 +00:00
}
return - 1 ;
2023-03-18 01:39:02 +00:00
}
2024-03-13 19:17:26 +00:00
2023-09-05 13:43:23 +00:00
/**
2024-04-18 00:21:42 +00:00
* Function to check if comment BEGIN and END exists in modMyModule class
2024-03-13 19:17:26 +00:00
*
2024-08-18 16:16:08 +00:00
* @ param string $file Filename or path
* @ param int < 0 , 2 > $number 0 = For Menus , 1 = For permissions , 2 = For Dictionaries
* @ return int 1 if OK , - 1 if KO
2023-09-05 13:43:23 +00:00
*/
function checkExistComment ( $file , $number )
{
if ( ! file_exists ( $file )) {
return - 1 ;
}
2024-03-13 19:17:26 +00:00
2023-09-05 13:43:23 +00:00
$content = file_get_contents ( $file );
if ( $number === 0 ) {
2024-03-13 19:17:26 +00:00
$ret = 0 ;
2024-04-18 00:21:42 +00:00
if ( strpos ( $content , '/* BEGIN MODULEBUILDER TOPMENU MYOBJECT */' ) !== false
|| strpos ( $content , '/* BEGIN MODULEBUILDER TOPMENU */' ) !== false ) {
2024-03-13 19:17:26 +00:00
$ret ++ ;
}
2024-04-18 00:21:42 +00:00
if ( strpos ( $content , '/* END MODULEBUILDER TOPMENU MYOBJECT */' ) !== false
|| strpos ( $content , '/* END MODULEBUILDER TOPMENU */' ) !== false ) {
2024-03-13 19:17:26 +00:00
$ret ++ ;
}
if ( strpos ( $content , '/* BEGIN MODULEBUILDER LEFTMENU MYOBJECT */' ) !== false ) {
$ret ++ ;
}
if ( strpos ( $content , '/* END MODULEBUILDER LEFTMENU MYOBJECT */' ) !== false ) {
$ret ++ ;
}
if ( $ret == 4 ) {
2023-09-05 13:43:23 +00:00
return 1 ;
}
} elseif ( $number === 1 ) {
if ( strpos ( $content , '/* BEGIN MODULEBUILDER PERMISSIONS */' ) !== false && strpos ( $content , '/* END MODULEBUILDER PERMISSIONS */' ) !== false ) {
return 1 ;
}
} elseif ( $number == 2 ) {
if ( strpos ( $content , '/* BEGIN MODULEBUILDER DICTIONARIES */' ) !== false && strpos ( $content , '/* END MODULEBUILDER DICTIONARIES */' ) !== false ) {
return 1 ;
}
}
return - 1 ;
}
2023-03-18 01:39:02 +00:00
/**
* Delete all permissions
*
2024-08-18 16:16:08 +00:00
* @ param string $file file with path
* @ return void
2023-03-15 09:33:42 +00:00
*/
2023-03-16 09:40:18 +00:00
function deletePerms ( $file )
2023-03-15 09:33:42 +00:00
{
$start = " /* BEGIN MODULEBUILDER PERMISSIONS */ " ;
$end = " /* END MODULEBUILDER PERMISSIONS */ " ;
2023-03-16 09:40:18 +00:00
$i = 1 ;
$array = array ();
$lines = file ( $file );
// Search for start and end lines
foreach ( $lines as $i => $line ) {
if ( strpos ( $line , $start ) !== false ) {
$start_line = $i + 1 ;
// Copy lines until the end on array
while (( $line = $lines [ ++ $i ]) !== false ) {
if ( strpos ( $line , $end ) !== false ) {
$end_line = $i + 1 ;
break ;
}
$array [] = $line ;
}
break ;
}
2023-03-15 09:33:42 +00:00
}
2023-03-16 09:40:18 +00:00
$allContent = implode ( " " , $array );
dolReplaceInFile ( $file , array ( $allContent => '' ));
}
2023-05-02 16:02:36 +00:00
/**
2024-08-18 16:16:08 +00:00
* Compare two values
* @ param int | string $a value 1
* @ param int | string $b value 2
* @ return int <- 1 , 1 > <= 0 if str1 is less than str2 ; > 0 if str1 is greater than str2 , and 0 if they are equal .
2025-02-26 22:14:28 +00:00
*/
2023-05-02 16:02:36 +00:00
function compareFirstValue ( $a , $b )
{
return strcmp ( $a [ 0 ], $b [ 0 ]);
}
2023-03-16 09:40:18 +00:00
/**
* Rewriting all permissions after any actions
2024-08-18 16:16:08 +00:00
* @ param string $file filename or path
* @ param array < int , string [] > $permissions permissions existing in file
* @ param ? int $key key for permission needed
* @ param ? array { 0 : string , 1 : string } $right $right to update or add
* @ param string $objectname name of object
* @ param string $module name of module
* @ param int <- 2 , 2 > $action 0 for delete , 1 for add , 2 for update , - 1 when delete object completely , - 2 for generate rights after add
* @ return int <- 1 , 1 > 1 if OK , - 1 if KO
2023-03-16 09:40:18 +00:00
*/
2023-05-03 13:33:58 +00:00
function reWriteAllPermissions ( $file , $permissions , $key , $right , $objectname , $module , $action )
2023-03-16 09:40:18 +00:00
{
$error = 0 ;
$rights = array ();
2024-08-18 16:16:08 +00:00
if ( $action == 0 && $key !== null ) {
2023-03-16 09:40:18 +00:00
// delete right from permissions array
array_splice ( $permissions , array_search ( $permissions [ $key ], $permissions ), 1 );
} elseif ( $action == 1 ) {
array_push ( $permissions , $right );
2024-08-18 16:16:08 +00:00
} elseif ( $action == 2 && ! empty ( $right ) && $key !== null ) {
2023-03-16 09:40:18 +00:00
// update right from permissions array
array_splice ( $permissions , array_search ( $permissions [ $key ], $permissions ), 1 , $right );
2023-05-03 13:33:58 +00:00
} elseif ( $action == - 1 && ! empty ( $objectname )) {
// when delete object
$key = null ;
$right = null ;
foreach ( $permissions as $perms ) {
if ( $perms [ 4 ] === strtolower ( $objectname )) {
array_splice ( $permissions , array_search ( $perms , $permissions ), 1 );
}
}
} elseif ( $action == - 2 && ! empty ( $objectname ) && ! empty ( $module )) {
2024-03-07 19:16:48 +00:00
$key = null ;
2023-05-03 13:33:58 +00:00
$right = null ;
$objectOfRights = array ();
//check if object already declared in rights file
foreach ( $permissions as $right ) {
2024-03-07 19:16:48 +00:00
$objectOfRights [] = $right [ 4 ];
2023-05-03 13:33:58 +00:00
}
if ( in_array ( strtolower ( $objectname ), $objectOfRights )) {
$error ++ ;
} else {
$permsToadd = array ();
$perms = array (
2023-06-12 12:16:09 +00:00
'read' => 'Read ' . $objectname . ' object of ' . ucfirst ( $module ),
'write' => 'Create/Update ' . $objectname . ' object of ' . ucfirst ( $module ),
'delete' => 'Delete ' . $objectname . ' object of ' . ucfirst ( $module )
2023-05-03 13:33:58 +00:00
);
$i = 0 ;
foreach ( $perms as $index => $value ) {
$permsToadd [ $i ][ 0 ] = '' ;
$permsToadd [ $i ][ 1 ] = $value ;
$permsToadd [ $i ][ 4 ] = strtolower ( $objectname );
$permsToadd [ $i ][ 5 ] = $index ;
array_push ( $permissions , $permsToadd [ $i ]);
$i ++ ;
}
}
2023-03-16 09:40:18 +00:00
} else {
2023-03-15 09:33:42 +00:00
$error ++ ;
}
2024-03-16 19:51:00 +00:00
'@phan-var-force array<int,string[]> $permissions' ;
2023-03-15 09:33:42 +00:00
if ( ! $error ) {
2023-03-16 09:40:18 +00:00
// prepare permissions array
2024-03-16 19:51:00 +00:00
foreach ( array_keys ( $permissions ) as $i ) {
2023-03-16 09:40:18 +00:00
$permissions [ $i ][ 0 ] = " \$ this->rights[ \$ r][0] = \$ this->numero . sprintf('%02d', \$ r + 1) " ;
$permissions [ $i ][ 1 ] = " \$ this->rights[ \$ r][1] = ' " . $permissions [ $i ][ 1 ] . " ' " ;
$permissions [ $i ][ 4 ] = " \$ this->rights[ \$ r][4] = ' " . $permissions [ $i ][ 4 ] . " ' " ;
$permissions [ $i ][ 5 ] = " \$ this->rights[ \$ r][5] = ' " . $permissions [ $i ][ 5 ] . " '; \n \t \t " ;
2023-03-15 09:33:42 +00:00
}
2023-12-04 11:05:28 +00:00
// for group permissions by object
$perms_grouped = array ();
2023-05-02 16:02:36 +00:00
foreach ( $permissions as $perms ) {
$object = $perms [ 4 ];
if ( ! isset ( $perms_grouped [ $object ])) {
2024-08-18 16:16:08 +00:00
$perms_grouped [ $object ] = array ();
2023-05-02 16:02:36 +00:00
}
$perms_grouped [ $object ][] = $perms ;
}
//$perms_grouped = array_values($perms_grouped);
$permissions = $perms_grouped ;
2024-01-13 18:48:20 +00:00
// parcourir les objects
2024-03-07 19:16:48 +00:00
$o = 0 ;
2023-05-02 16:02:36 +00:00
foreach ( $permissions as & $object ) {
// récupérer la permission de l'objet
$p = 1 ;
foreach ( $object as & $obj ) {
if ( str_contains ( $obj [ 5 ], 'read' )) {
$obj [ 0 ] = " \$ this->rights[ \$ r][0] = \$ this->numero . sprintf('%02d', ( " . $o . " * 10) + 0 + 1) " ;
} elseif ( str_contains ( $obj [ 5 ], 'write' )) {
$obj [ 0 ] = " \$ this->rights[ \$ r][0] = \$ this->numero . sprintf('%02d', ( " . $o . " * 10) + 1 + 1) " ;
} elseif ( str_contains ( $obj [ 5 ], 'delete' )) {
$obj [ 0 ] = " \$ this->rights[ \$ r][0] = \$ this->numero . sprintf('%02d', ( " . $o . " * 10) + 2 + 1) " ;
} else {
$obj [ 0 ] = " \$ this->rights[ \$ r][0] = \$ this->numero . sprintf('%02d', ( " . $o . " * 10) + " . $p . " + 1) " ;
$p ++ ;
}
}
usort ( $object , 'compareFirstValue' );
$o ++ ;
}
2023-03-16 09:40:18 +00:00
//convert to string
foreach ( $permissions as $perms ) {
2023-05-02 16:02:36 +00:00
foreach ( $perms as $per ) {
2024-08-18 16:16:08 +00:00
$rights [] = implode ( " ; \n \t \t " , $per ) . " \$ r++; \n " ;
2023-05-02 16:02:36 +00:00
}
2023-03-15 09:33:42 +00:00
}
2024-08-18 16:16:08 +00:00
$rights_str = implode ( " \t \t " , $rights );
2024-01-13 18:48:20 +00:00
// delete all permissions from file
2023-03-16 09:40:18 +00:00
deletePerms ( $file );
2024-01-13 18:48:20 +00:00
// rewrite all permissions again
2023-03-16 09:40:18 +00:00
dolReplaceInFile ( $file , array ( '/* BEGIN MODULEBUILDER PERMISSIONS */' => '/* BEGIN MODULEBUILDER PERMISSIONS */' . " \n \t \t " . $rights_str ));
return 1 ;
2023-05-02 16:02:36 +00:00
} else {
return - 1 ;
2023-03-15 09:33:42 +00:00
}
}
2023-03-18 01:55:35 +00:00
2023-08-30 15:00:12 +00:00
/**
* Converts a formatted properties string into an associative array .
*
2024-08-18 16:16:08 +00:00
* @ param string $string The formatted properties string .
* @ return array < string , bool | int | float | string | mixed [] > The resulting associative array .
2023-08-30 15:00:12 +00:00
*/
function parsePropertyString ( $string )
{
$string = str_replace ( " ' " , '' , $string );
// Uses a regular expression to capture keys and values
preg_match_all ( '/\s*([^\s=>]+)\s*=>\s*([^,]+),?/' , $string , $matches , PREG_SET_ORDER );
2024-08-18 16:16:08 +00:00
$propertyArray = array ();
2023-08-30 15:00:12 +00:00
foreach ( $matches as $match ) {
$key = trim ( $match [ 1 ]);
$value = trim ( $match [ 2 ]);
if ( strpos ( $value , 'array(' ) === 0 ) {
$nestedArray = substr ( $value , 6 );
$nestedArray = parsePropertyString ( $nestedArray );
$value = $nestedArray ;
} elseif ( strpos ( $value , '"Id")' ) !== false ) {
$value = str_replace ( ')' , '' , $value );
} else {
if ( is_numeric ( $value )) {
if ( strpos ( $value , '.' ) !== false ) {
$value = ( float ) $value ;
} else {
$value = ( int ) $value ;
}
} else {
if ( $value === 'true' ) {
$value = true ;
} elseif ( $value === 'false' ) {
$value = false ;
}
}
}
$propertyArray [ $key ] = $value ;
}
return $propertyArray ;
}
2023-03-18 01:55:35 +00:00
/**
* Write all properties of the object in AsciiDoc format
2024-08-18 16:16:08 +00:00
* @ param string $file path of the class
* @ param string $objectname name of the objectClass
* @ param string $destfile file where write table of properties
* @ return int 1 if OK , - 1 if KO
2023-03-18 01:55:35 +00:00
*/
function writePropsInAsciiDoc ( $file , $objectname , $destfile )
{
// stock all properties in array
2023-12-04 11:05:28 +00:00
$attributesUnique = array ( 'type' , 'label' , 'enabled' , 'position' , 'notnull' , 'visible' , 'noteditable' , 'index' , 'default' , 'foreignkey' , 'arrayofkeyval' , 'alwayseditable' , 'validate' , 'searchall' , 'comment' , 'isameasure' , 'css' , 'cssview' , 'csslist' , 'help' , 'showoncombobox' , 'picto' );
2023-03-18 01:55:35 +00:00
2024-08-18 16:16:08 +00:00
$start = " public \$ fields = array( " ;
2023-03-18 01:55:35 +00:00
$end = " ); " ;
$i = 1 ;
$keys = array ();
$lines = file ( $file );
// Search for start and end lines
foreach ( $lines as $i => $line ) {
if ( strpos ( $line , $start ) !== false ) {
// Copy lines until the end on array
while (( $line = $lines [ ++ $i ]) !== false ) {
if ( strpos ( $line , $end ) !== false ) {
break ;
}
$keys [] = $line ;
}
break ;
}
}
// write the begin of table with specifics options
$table = " == DATA SPECIFICATIONS \n " ;
2023-06-13 00:41:44 +00:00
$table .= " === Table of fields with properties for object * $objectname * : \n " ;
2023-03-18 01:55:35 +00:00
$table .= " [options='header',grid=rows,frame=topbot,width=100%,caption=Organisation] \n " ;
$table .= " |=== \n " ;
$table .= " |code " ;
// write all properties in the header of the table
foreach ( $attributesUnique as $attUnique ) {
$table .= " | " . $attUnique ;
}
2024-03-07 19:16:48 +00:00
$table .= " \n " ;
2023-08-30 15:00:12 +00:00
$valuesModif = array ();
2023-06-05 14:15:52 +00:00
foreach ( $keys as $string ) {
2023-03-18 01:55:35 +00:00
$string = trim ( $string , " ' " );
$string = rtrim ( $string , " , " );
2023-08-30 15:00:12 +00:00
$array = parsePropertyString ( $string );
// Iterate through the array to merge all key to one array
$code = '' ;
foreach ( $array as $key => $value ) {
if ( is_array ( $value )) {
$code = $key ;
continue ;
} else {
$array [ $code ][ $key ] = $value ;
unset ( $array [ $key ]);
}
}
// check if is array after parsing the string
2023-03-18 01:55:35 +00:00
if ( ! is_array ( $array )) {
return - 1 ;
}
2023-06-05 14:15:52 +00:00
$field = array_keys ( $array );
2023-08-30 15:00:12 +00:00
if ( $field [ 0 ] === '' ) {
$field [ 0 ] = 'label' ;
}
2023-06-05 14:15:52 +00:00
$values = array_values ( $array )[ 0 ];
2023-03-18 01:55:35 +00:00
// check each field has all properties and add it if missed
2023-06-05 14:15:52 +00:00
foreach ( $attributesUnique as $attUnique ) {
2023-08-30 15:00:12 +00:00
if ( $attUnique == 'type' && $field [ 0 ] === 'label' ) {
$values [ $attUnique ] = 'varchar(255)' ;
}
2023-06-05 14:15:52 +00:00
if ( ! array_key_exists ( $attUnique , $values )) {
2023-08-30 15:00:12 +00:00
$valuesModif [ $attUnique ] = '' ;
} else {
$valuesModif [ $attUnique ] = $values [ $attUnique ];
2023-03-18 01:55:35 +00:00
}
}
2023-06-05 14:15:52 +00:00
$table .= " |* " . $field [ 0 ] . " *| " ;
2023-08-30 15:00:12 +00:00
$table .= implode ( " | " , $valuesModif ) . " \n " ;
2023-03-18 01:55:35 +00:00
}
2023-08-30 15:00:12 +00:00
2023-03-18 01:55:35 +00:00
// end table
2023-06-12 12:16:09 +00:00
$table .= " |=== \n " ;
$table .= " __ end table for object $objectname\n " ;
2023-08-30 15:00:12 +00:00
2024-03-07 19:16:48 +00:00
//write in file @phan-suppress-next-line PhanPluginSuspiciousParamPosition
2023-06-13 00:41:44 +00:00
$writeInFile = dolReplaceInFile ( $destfile , array ( '== DATA SPECIFICATIONS' => $table ));
2024-03-07 19:16:48 +00:00
if ( $writeInFile < 0 ) {
2023-03-18 01:55:35 +00:00
return - 1 ;
}
return 1 ;
}
2023-03-24 17:21:06 +00:00
2023-08-30 15:00:12 +00:00
2023-03-20 18:43:23 +00:00
/**
2023-10-13 13:42:15 +00:00
* Delete property and permissions from documentation ascii file if we delete an object
*
2024-08-18 16:16:08 +00:00
* @ param string $file file or path
* @ param string $objectname name of object wants to deleted
* @ return void
2023-03-20 18:43:23 +00:00
*/
2023-08-30 15:00:12 +00:00
function deletePropsAndPermsFromDoc ( $file , $objectname )
2023-03-20 18:43:23 +00:00
{
2023-10-13 13:42:15 +00:00
if ( dol_is_file ( $file )) {
$start = " == Table of fields and their properties for object * " . ucfirst ( $objectname ) . " * : " ;
$end = " __ end table for object " . ucfirst ( $objectname );
$str = file_get_contents ( $file );
2023-03-20 18:43:23 +00:00
2023-10-13 13:42:15 +00:00
$search = '/' . preg_quote ( $start , '/' ) . '(.*?)' . preg_quote ( $end , '/' ) . '/s' ;
$new_contents = preg_replace ( $search , '' , $str );
file_put_contents ( $file , $new_contents );
2025-12-15 15:57:06 +00:00
dolChmod ( $file );
2023-10-13 13:42:15 +00:00
//perms If Exist
$perms = " |* " . strtolower ( $objectname ) . " *| " ;
$search_pattern_perms = '/' . preg_quote ( $perms , '/' ) . '.*?\n/' ;
$new_contents = preg_replace ( $search_pattern_perms , '' , $new_contents );
file_put_contents ( $file , $new_contents );
2025-12-15 15:57:06 +00:00
dolChmod ( $file );
2023-10-13 13:42:15 +00:00
}
2023-03-20 18:43:23 +00:00
}
2023-04-05 09:08:34 +00:00
2023-08-30 15:00:12 +00:00
2023-03-24 17:21:06 +00:00
/**
2024-03-14 15:39:12 +00:00
* Search a string and return all lines needed from file . Does not include line $start nor $end
*
2024-08-18 16:16:08 +00:00
* @ param string $file file for searching
* @ param string $start start line if it exists
* @ param string $end end line if it exists
* @ param string $excludestart Ignore if start line is $excludestart
* @ param int < 0 , 1 > $includese Include start and end line
* @ return string Return the lines between first line with $start and $end . " " if not found .
2023-03-24 17:21:06 +00:00
*/
2024-04-18 00:21:42 +00:00
function getFromFile ( $file , $start , $end , $excludestart = '' , $includese = 0 )
2023-03-24 17:21:06 +00:00
{
$keys = array ();
2024-04-18 00:21:42 +00:00
//$lines = file(dol_osencode($file));
$fhandle = fopen ( dol_osencode ( $file ), 'r' );
if ( $fhandle ) {
// Search for start and end lines
//foreach ($lines as $i => $line) {
while ( $line = fgets ( $fhandle )) {
if ( strpos ( $line , $start ) !== false && ( empty ( $excludestart ) || strpos ( $line , $excludestart ) === false )) {
if ( $includese ) {
$keys [] = $line ;
2023-03-24 17:21:06 +00:00
}
2024-04-18 00:21:42 +00:00
// Copy lines until we reach the end
while (( $line = fgets ( $fhandle )) !== false ) {
if ( strpos ( $line , $end ) !== false ) {
if ( $includese ) {
$keys [] = $line ;
}
break ;
}
$keys [] = $line ;
}
break ;
2023-03-24 17:21:06 +00:00
}
}
}
2024-04-18 00:21:42 +00:00
fclose ( $fhandle );
2023-03-24 17:21:06 +00:00
$content = implode ( " " , $keys );
return $content ;
}
/**
* Write all permissions of each object in AsciiDoc format
2024-08-18 16:16:08 +00:00
* @ param string $file path of the class
* @ param string $destfile file where write table of permissions
* @ return int <- 1 , 1 > 1 if OK , - 1 if KO
2023-03-24 17:21:06 +00:00
*/
function writePermsInAsciiDoc ( $file , $destfile )
{
global $langs ;
2024-01-13 18:48:20 +00:00
//search and get all permissions in string
2023-03-24 17:21:06 +00:00
$start = '/* BEGIN MODULEBUILDER PERMISSIONS */' ;
$end = '/* END MODULEBUILDER PERMISSIONS */' ;
$content = getFromFile ( $file , $start , $end );
if ( empty ( $content )) {
return - 1 ;
}
//prepare table
$string = " [options='header',grid=rows,width=60%,caption=Organisation] \n " ;
$string .= " |=== \n " ;
// header for table
$header = array ( $langs -> trans ( 'Objects' ), $langs -> trans ( 'Permission' ));
foreach ( $header as $h ) {
$string .= " | " . $h ;
}
$string .= " \n " ;
//content table
$array = explode ( " ; " , $content );
2023-06-05 14:15:52 +00:00
$permissions = array_filter ( $array );
2023-03-24 17:21:06 +00:00
// delete occurrences "$r++" and ID
2024-03-18 02:55:52 +00:00
$permissions = str_replace ( '$r++' , '1' , $permissions );
2023-03-24 17:21:06 +00:00
$permsN = array ();
foreach ( $permissions as $i => $element ) {
if ( $element == 1 ) {
unset ( $permissions [ $i ]);
}
if ( str_contains ( $element , '$this->numero' )) {
unset ( $permissions [ $i ]);
}
if ( str_contains ( $element , '$this->rights[$r][5]' )) {
unset ( $permissions [ $i ]);
}
}
// cleaning the string on each element
foreach ( $permissions as $key => $element ) {
$element = str_replace ( " ' " , '' , $element );
$element = trim ( $element , " ' " );
2024-03-07 19:16:48 +00:00
$permsN [] = substr ( $element , strpos ( $element , " = " ) + 1 );
2023-03-24 17:21:06 +00:00
}
array_pop ( $permsN );
// Group permissions by Object and add it to string
2024-08-18 16:16:08 +00:00
$final_array = array ();
2023-06-05 14:15:52 +00:00
$index = 0 ;
while ( $index < count ( $permsN )) {
2024-08-18 16:16:08 +00:00
$temp_array = array ( $permsN [ $index ], $permsN [ $index + 1 ]);
2023-03-24 17:21:06 +00:00
$final_array [] = $temp_array ;
2023-06-05 14:15:52 +00:00
$index += 2 ;
2023-03-24 17:21:06 +00:00
}
$result = array ();
foreach ( $final_array as $subarray ) {
// found object
$key = $subarray [ 1 ];
// add sub array to object
$result [ $key ][] = $subarray ;
}
foreach ( $result as $i => $pems ) {
$string .= " |* " . $i . " *| " ;
foreach ( $pems as $tab ) {
$string .= $tab [ 0 ] . " , " ;
}
$string .= " \n " ;
}
// end table
$string .= " \n |=== \n " ;
2024-03-07 19:16:48 +00:00
// @phan-suppress-next-line PhanPluginSuspiciousParamPosition
$write = dolReplaceInFile ( $destfile , array ( '__DATA_PERMISSIONS__' => $string ));
if ( $write < 0 ) {
2023-03-24 17:21:06 +00:00
return - 1 ;
}
return 1 ;
}
2023-04-13 15:13:56 +00:00
/**
* Add Object in ModuleApi File
2024-03-14 15:39:12 +00:00
*
2024-08-18 16:16:08 +00:00
* @ param string $srcfile Source file to use as example
* @ param string $file Path of modified file
* @ param string [] $objects Array of objects in the module
* @ param string $modulename Name of module
* @ return int <- 1 , 1 > Return 1 if OK , - 1 if KO
2023-04-13 15:13:56 +00:00
*/
2024-03-14 15:39:12 +00:00
function addObjectsToApiFile ( $srcfile , $file , $objects , $modulename )
2023-04-13 15:13:56 +00:00
{
2024-03-14 15:39:12 +00:00
global $langs , $user ;
2023-04-13 15:13:56 +00:00
if ( ! file_exists ( $file )) {
return - 1 ;
}
2024-03-14 15:39:12 +00:00
$now = dol_now ();
$content = file ( $file ); // $content is an array
$includeClass = " dol_include_once \ ( \ ' \ / \ w+ \ /class \ / \ w+ \ .class \ .php \ ' \ ); " ;
$props = 'public\s+\$\w+;' ;
$varcommented = '@var\s+\w+\s+\$\w+\s+{@type\s+\w+}' ;
$constructObj = '\$this->\w+\s+=\s+new\s+\w+\(\$this->db\);' ;
2023-04-13 15:13:56 +00:00
2024-01-13 18:48:20 +00:00
// add properties and declare them in constructor
2023-04-13 15:13:56 +00:00
foreach ( $content as $lineNumber => & $lineContent ) {
2024-03-14 15:39:12 +00:00
if ( preg_match ( '/' . $varcommented . '/' , $lineContent )) {
2023-04-13 15:13:56 +00:00
$lineContent = '' ;
2024-03-14 15:39:12 +00:00
foreach ( $objects as $objectname ) {
$lineContent .= " \t * @var " . $objectname . " \$ " . strtolower ( $objectname ) . " { @type " . $objectname . " } " . PHP_EOL ;
2023-04-13 15:13:56 +00:00
}
//var_dump($lineContent);exit;
2024-03-14 15:39:12 +00:00
} elseif ( preg_match ( '/' . $props . '/' , $lineContent )) {
2023-04-13 15:13:56 +00:00
$lineContent = '' ;
2024-03-14 15:39:12 +00:00
foreach ( $objects as $objectname ) {
2024-08-18 16:16:08 +00:00
$lineContent .= " \t /* " . PHP_EOL . " \t * @var mixed TODO: set type " . PHP_EOL . " \t */ " . PHP_EOL . " \t public \$ " . strtolower ( $objectname ) . " ; " . PHP_EOL ;
2023-04-13 15:13:56 +00:00
}
2024-03-14 15:39:12 +00:00
} elseif ( preg_match ( '/' . $constructObj . '/' , $lineContent )) {
2023-04-13 15:13:56 +00:00
$lineContent = '' ;
2024-03-14 15:39:12 +00:00
foreach ( $objects as $objectname ) {
$lineContent .= " \t \t \$ this-> " . strtolower ( $objectname ) . " = new " . $objectname . " ( \$ this->db); " . PHP_EOL ;
2023-04-13 15:13:56 +00:00
}
2024-03-14 15:39:12 +00:00
} elseif ( preg_match ( '/' . $includeClass . '/' , $lineContent )) {
2023-04-13 15:13:56 +00:00
$lineContent = '' ;
2024-03-14 15:39:12 +00:00
foreach ( $objects as $objectname ) {
$lineContent .= " dol_include_once('/ " . strtolower ( $modulename ) . " /class/ " . strtolower ( $objectname ) . " .class.php'); " . PHP_EOL ;
2023-04-13 15:13:56 +00:00
}
}
}
2024-03-14 15:39:12 +00:00
2023-04-13 15:13:56 +00:00
$allContent = implode ( " " , $content );
file_put_contents ( $file , $allContent );
2025-12-15 15:57:06 +00:00
dolChmod ( $file );
2023-04-13 15:13:56 +00:00
2024-03-14 15:39:12 +00:00
// Add methods for each object
$allContent = getFromFile ( $srcfile , '/* BEGIN MODULEBUILDER API MYOBJECT */' , '/* END MODULEBUILDER API MYOBJECT */' );
foreach ( $objects as $objectname ) {
NEW : Replace MyObject MyModule occurences (#38370)
* [COREMB] Centralize myobject/mymodule substitutions via NamingContract value object
Introduce NamingContract (immutable value object) and NamingContractValidator
to replace 6 scattered ad-hoc $arrayreplacement blocks across index.php and
modulebuilder.lib.php. Fixes a critical bug where MYOBJECT token was commented
out in initobject, leaving generated class files with unsubstituted placeholders.
- NamingContract: canonical ordered substitution map (12 tokens with object,
7 module-only), applyTo() via str_replace (not make_substitutions, to avoid
processing Dolibarr __(key)__ patterns in raw templates), applyToFilename()
for lowercase-only filename substitution
- StrictNamingContractValidator: post-generation scan for residual myobject/
mymodule tokens, skipping /* BEGIN/END MODULEBUILDER */ structural markers
- modulebuilderValidateGeneratedFile() helper: non-blocking warnings with
XSS-safe output via dol_escape_htmltag()
- Collision guard: InvalidArgumentException if module === object name
(case-insensitive), wrapped in try-catch at all instantiation sites
- 24 PHPUnit tests, 52 assertions
* fix(initobject): apply object substitution to all module PHP files
After initmodule creates the module structure, files like index.php,
lib/module.lib.php, admin/setup.php and class/api_module.class.php
still contain myobject/mymodule placeholders. initobject now scans
ALL PHP files in the module directory and applies the full NamingContract
substitution, and deletes the orphan stats/myobject_index.php placeholder.
* fix(initmodule): prevent array_merge from renumbering the '500000' key
PHP casts numeric string keys like '500000' to int(500000) internally.
array_merge() then renumbers integer keys starting from 0, turning
'500000' into key 0. make_substitutions() then calls str_replace(0, ...)
which casts 0 to the string '0', replacing EVERY zero digit in every
generated file with the idmodule value.
Fix: replace the bare '500000' key with '$this->numero = 500000' so that
array_merge sees a non-numeric string key and preserves it as-is.
* Fix initapi: apply header substitution after addObjectsToApiFile
addObjectsToApiFile substitutes properties/constructor/includes via regex
but leaves PHPDoc header and class declaration with mymodule/myobject tokens.
Fix: apply $arrayreplacement (minus 'MYOBJECT' uppercase key) after
addObjectsToApiFile to resolve the remaining residuals.
'MYOBJECT' is excluded to preserve the /* BEGIN MODULEBUILDER API MYOBJECT */
placeholder that addObjectsToApiFile relies on for future object additions.
* Move NamingContractTest to core test directory
NamingContract and NamingContractValidator are core modulebuilder classes;
their tests belong alongside other Dolibarr core tests in test/phpunit/,
not in htdocs/modulebuilder/test/phpunit/ (the custom-module pattern).
Updated require_once paths accordingly.
* fix useless file
* fix(phpcs): add missing @param descriptions and function docblocks
- NamingContract: merge adjacent string literals in exception message,
add description to @param $content in applyTo()
- NamingContractValidator: add descriptions to all bare @param tags
in interface and StrictNamingContractValidator methods
- NamingContractTest: add /** @return void */ docblocks to all test
methods, merge unnecessary string concat on template variable
* fix(phan/phpstan): remove readonly, add baselines for test file
- NamingContract: replace readonly properties with plain public string
(readonly requires PHP 8.1, phan minimum target is PHP 7.2)
- dev/tools/phan/baseline.txt: add NamingContractTest.php suppression
for PhanUndeclaredMethod (PHPUnit stubs not available in phan scope)
- NamingContractTest.php: add @phpstan-ignore class.notFound on class
declaration and method.notFound on each assertion call (PHPUnit not
in phpstan bootstrap, consistent with CommonClassTest.class.php)
* fix(phan/phpstan): suppress typed property warnings, NoopNew, class.notFound
- baseline.txt: add PhanCompatibleTypedProperty for NamingContract.class.php
(typed properties require PHP 7.4, phan min target is 7.2 — warning only
but phan exits 1 on any issue including warnings)
- baseline.txt: add PhanNoopNew for NamingContractTest.php (constructor
called for exception side-effect in guard tests is intentional)
- NamingContractTest.php: move @phpstan-ignore class.notFound to inline
comment on class declaration (docblock placement not picked up by phpstan)
* fix(phan/phpstan): add missing phan suppress on index.php l.574, fix phpstan class docblock
- index.php l.574: add @phan-suppress-next-line PhanPluginSuspiciousParamPosition
(same pattern as l.577 and l.587 for identical dolReplaceInFile calls
where local variable name $destfile matches parameter #3 of the function)
- NamingContractTest.php: use standalone /** @phpstan-ignore class.notFound */
docblock before class declaration (inline // comment not picked up by phpstan
on class declarations; separate docblock matches CommonClassTest.class.php pattern)
* Update baseline.txt
* fix(phan): add phan-file-suppress on NamingContractTest for PHPUnit undeclared methods
* fix(phan): suppress PhanNoopNew on expectException patterns in NamingContractTest
---------
Co-authored-by: Laurent Destailleur <eldy@destailleur.fr>
2026-05-25 12:52:38 +00:00
if ( strtolower ( $modulename ) === strtolower ( $objectname )) {
dol_syslog ( 'addObjectsToApiFile: skipping object "' . $objectname . '" — name collides with module "' . $modulename . '"' , LOG_WARNING );
continue ;
}
$nc = new NamingContract ( $modulename , $objectname );
$extraMap = [
'htdocs/modulebuilder/template' => $nc -> moduleNameLower ,
'---Replace with your own copyright and developer email---' => dol_print_date ( $now , '%Y' ) . ' ' . $user -> getFullName ( $langs ) . ( $user -> email ? ' <' . $user -> email . '>' : '' ),
];
$fullMap = array_merge ( $nc -> getSubstitutionMap (), $extraMap );
$contentReplaced = str_replace ( array_keys ( $fullMap ), array_values ( $fullMap ), $allContent );
2024-03-14 15:39:12 +00:00
dolReplaceInFile ( $file , array (
'/* BEGIN MODULEBUILDER API MYOBJECT */' => '/* BEGIN MODULEBUILDER API ' . strtoupper ( $objectname ) . ' */' . $contentReplaced . " \t " . '/* END MODULEBUILDER API ' . strtoupper ( $objectname ) . ' */' . " \n \n \n \t " . '/* BEGIN MODULEBUILDER API MYOBJECT */'
));
2023-04-13 15:13:56 +00:00
}
2024-03-14 15:39:12 +00:00
// Remove the block $allContent found in src file
// TODO Replace with a replacement of all text including into /* BEGIN MODULEBUILDER API MYOBJECT */ and /* END MODULEBUILDER API MYOBJECT */
dolReplaceInFile ( $file , array ( $allContent => '' ));
2023-04-13 15:13:56 +00:00
return 1 ;
}
/**
2024-03-14 15:39:12 +00:00
* Remove Object variables and methods from API_Module File
*
2024-08-18 16:16:08 +00:00
* @ param string $file File api module
* @ param string [] $objects Array of objects in the module
* @ param string $objectname Name of object want to remove
* @ return int <- 1 , 1 > 1 if OK , - 1 if KO
2023-04-13 15:13:56 +00:00
*/
2024-03-14 15:39:12 +00:00
function removeObjectFromApiFile ( $file , $objects , $objectname )
2023-04-13 15:13:56 +00:00
{
if ( ! file_exists ( $file )) {
return - 1 ;
}
2024-03-14 15:39:12 +00:00
$content = file ( $file ); // $content is an array
$includeClass = " dol_include_once \ ( \ ' \ / \ w+ \ /class \ / " . strtolower ( $objectname ) . " \ .class \ .php \ ' \ ); " ;
$props = 'public\s+\$' . strtolower ( $objectname );
$varcommented = '@var\s+\w+\s+\$' . strtolower ( $objectname ) . '\s+{@type\s+\w+}' ;
$constructObj = '\$this->' . strtolower ( $objectname ) . '\s+=\s+new\s+\w+\(\$this->db\);' ;
// add properties and declare them in constructor
2023-04-13 15:13:56 +00:00
foreach ( $content as $lineNumber => & $lineContent ) {
2024-03-14 15:39:12 +00:00
if ( preg_match ( '/' . $varcommented . '/i' , $lineContent )) {
2023-04-13 15:13:56 +00:00
$lineContent = '' ;
2024-03-14 15:39:12 +00:00
} elseif ( preg_match ( '/' . $props . '/i' , $lineContent )) {
2023-04-13 15:13:56 +00:00
$lineContent = '' ;
2024-03-14 15:39:12 +00:00
} elseif ( preg_match ( '/' . $constructObj . '/i' , $lineContent )) {
2023-04-13 15:13:56 +00:00
$lineContent = '' ;
2024-03-14 15:39:12 +00:00
} elseif ( preg_match ( '/' . $includeClass . '/i' , $lineContent )) {
2023-04-13 15:13:56 +00:00
$lineContent = '' ;
}
}
2024-03-14 15:39:12 +00:00
2023-04-13 15:13:56 +00:00
$allContent = implode ( " " , $content );
file_put_contents ( $file , $allContent );
2025-12-15 15:57:06 +00:00
dolChmod ( $file );
2024-03-14 15:39:12 +00:00
2023-04-13 15:13:56 +00:00
// for delete methods of object
2024-03-14 15:39:12 +00:00
$begin = '/* BEGIN MODULEBUILDER API ' . strtoupper ( $objectname ) . ' */' ;
$end = '/* END MODULEBUILDER API ' . strtoupper ( $objectname ) . ' */' ;
2023-04-13 15:13:56 +00:00
$allContent = getFromFile ( $file , $begin , $end );
$check = dolReplaceInFile ( $file , array ( $allContent => '' ));
if ( $check ) {
dolReplaceInFile ( $file , array ( $begin => '' , $end => '' ));
}
2024-03-14 15:39:12 +00:00
2023-04-13 15:13:56 +00:00
return 1 ;
}
2023-05-23 15:37:52 +00:00
/**
2024-08-18 16:16:08 +00:00
* @ param string $file path of filename
2024-11-04 22:53:20 +00:00
* @ param array < int , array { commentgroup ? : string , fk_menu : string , type : string , titre : string , mainmenu : string , leftmenu : string , url : string , langs : string , position : int | string , enabled : int | string , perms : string , target : string , user : int } > $menus all menus for module
* @ param null | string | array { commentgroup ? : string , fk_menu : string , type : string , titre : string , mainmenu : string , leftmenu : string , url : string , langs : string , position : int | string , enabled : int | string , perms : string , target : string , user : int } $menuWantTo menu get for do actions
2024-08-18 16:16:08 +00:00
* @ param ? int $key key for the concerned menu
* @ param int <- 1 , 2 > $action for specify what action ( 0 = delete perm , 1 = add perm , 2 = update perm , - 1 = when we delete object )
* @ return int <- 1 , 1 > 1 if OK , - 1 if KO
2023-05-23 15:37:52 +00:00
*/
function reWriteAllMenus ( $file , $menus , $menuWantTo , $key , $action )
{
2024-03-07 19:16:48 +00:00
$errors = 0 ;
2023-05-23 15:37:52 +00:00
$counter = 0 ;
if ( ! file_exists ( $file )) {
return - 1 ;
}
2024-04-18 00:21:42 +00:00
2023-05-23 15:37:52 +00:00
if ( $action == 0 && ! empty ( $key )) {
2024-01-13 18:48:20 +00:00
// delete menu manually
2023-05-23 15:37:52 +00:00
array_splice ( $menus , array_search ( $menus [ $key ], $menus ), 1 );
} elseif ( $action == 1 ) {
2024-01-13 18:48:20 +00:00
// add menu manually
2023-05-23 15:37:52 +00:00
array_push ( $menus , $menuWantTo );
} elseif ( $action == 2 && ! empty ( $key ) && ! empty ( $menuWantTo )) {
// update right from permissions array
2024-03-07 19:16:48 +00:00
$urlCounter = 0 ;
2023-05-23 15:37:52 +00:00
// check if the values already exists
foreach ( $menus as $index => $menu ) {
if ( $index !== $key ) {
if ( $menu [ 'type' ] === $menuWantTo [ 'type' ]) {
if ( strcasecmp ( str_replace ( ' ' , '' , $menu [ 'titre' ]), str_replace ( ' ' , '' , $menuWantTo [ 'titre' ])) === 0 ) {
$counter ++ ;
}
if ( strcasecmp ( str_replace ( ' ' , '' , $menu [ 'url' ]), str_replace ( ' ' , '' , $menuWantTo [ 'url' ])) === 0 ) {
2023-07-19 09:57:13 +00:00
$urlCounter ++ ;
2023-05-23 15:37:52 +00:00
}
}
}
}
2023-07-19 09:57:13 +00:00
if ( ! $counter && $urlCounter < 2 ) {
2023-05-23 15:37:52 +00:00
$menus [ $key ] = $menuWantTo ;
} else {
$errors ++ ;
}
2024-08-18 16:16:08 +00:00
} elseif ( $action == - 1 && ! empty ( $menuWantTo ) && is_string ( $menuWantTo )) {
2023-05-23 15:37:52 +00:00
// delete menus when delete Object
foreach ( $menus as $index => $menu ) {
if (( strpos ( strtolower ( $menu [ 'fk_menu' ]), strtolower ( $menuWantTo )) !== false ) || ( strpos ( strtolower ( $menu [ 'leftmenu' ]), strtolower ( $menuWantTo )) !== false )) {
array_splice ( $menus , array_search ( $menu , $menus ), 1 );
}
}
} else {
$errors ++ ;
}
if ( ! $errors ) {
2024-04-18 00:21:42 +00:00
// delete All LEFT Menus (except for commented template MYOBJECT)
$beginMenu = '/* BEGIN MODULEBUILDER LEFTMENU' ;
$excludeBeginMenu = '/* BEGIN MODULEBUILDER LEFTMENU MYOBJECT' ;
$endMenu = '/* END MODULEBUILDER LEFTMENU' ;
$protection = 0 ;
while ( $protection <= 1000 && $allMenus = getFromFile ( $file , $beginMenu , $endMenu , $excludeBeginMenu , 1 )) {
$protection ++ ;
dolReplaceInFile ( $file , array ( $allMenus => '' ));
}
2023-05-23 15:37:52 +00:00
2024-04-18 00:21:42 +00:00
// forge the menu code in a string
2023-05-23 15:37:52 +00:00
$str_menu = " " ;
2023-10-13 13:42:15 +00:00
foreach ( $menus as $index => $menu ) {
2023-05-23 15:37:52 +00:00
$menu [ 'position' ] = " 1000 + \$ r " ;
if ( $menu [ 'type' ] === 'left' ) {
2024-04-18 00:21:42 +00:00
$start = " \t \t " . '/* BEGIN MODULEBUILDER LEFTMENU ' . strtoupper ( empty ( $menu [ 'object' ]) ? $menu [ 'titre' ] : $menu [ 'object' ]) . ' */' ;
$end = " \t \t " . '/* END MODULEBUILDER LEFTMENU ' . strtoupper ( empty ( $menu [ 'object' ]) ? $menu [ 'titre' ] : $menu [ 'object' ]) . ' */' ;
2023-10-13 13:42:15 +00:00
2023-05-23 15:37:52 +00:00
$val_actuel = $menu ;
2023-10-13 13:42:15 +00:00
$next_val = empty ( $menus [ $index + 1 ]) ? null : $menus [ $index + 1 ];
2023-10-29 15:44:41 +00:00
//var_dump(dol_escape_php($menu['perms'], 1)); exit;
2023-10-13 13:42:15 +00:00
2023-05-23 15:37:52 +00:00
$str_menu .= $start . " \n " ;
2024-08-18 16:16:08 +00:00
$str_menu .= " \t \t \$ this->menu[ \$ r++] = array( \n " ;
$str_menu .= " \t \t \t 'fk_menu' => ' " . dol_escape_php ( $menu [ 'fk_menu' ], 1 ) . " ', \n " ;
$str_menu .= " \t \t \t 'type' => ' " . dol_escape_php ( $menu [ 'type' ], 1 ) . " ', \n " ;
$str_menu .= " \t \t \t 'titre' => ' " . dol_escape_php ( $menu [ 'titre' ], 1 ) . " ', \n " ;
$str_menu .= " \t \t \t 'mainmenu' => ' " . dol_escape_php ( $menu [ 'mainmenu' ], 1 ) . " ', \n " ;
$str_menu .= " \t \t \t 'leftmenu' => ' " . dol_escape_php ( $menu [ 'leftmenu' ], 1 ) . " ', \n " ;
$str_menu .= " \t \t \t 'url' => ' " . dol_escape_php ( $menu [ 'url' ], 1 ) . " ', \n " ;
$str_menu .= " \t \t \t 'langs' => ' " . dol_escape_php ( $menu [ 'langs' ], 1 ) . " ', \n " ;
$str_menu .= " \t \t \t 'position' => " . (( int ) $menu [ 'position' ]) . " , \n " ;
2024-09-05 14:05:37 +00:00
$str_menu .= " \t \t \t 'enabled' => ' " . dol_escape_php (( string ) $menu [ 'enabled' ], 1 ) . " ', \n " ;
2024-08-18 16:16:08 +00:00
$str_menu .= " \t \t \t 'perms' => ' " . dol_escape_php ( $menu [ 'perms' ], 1 ) . " ', \n " ;
$str_menu .= " \t \t \t 'target' => ' " . dol_escape_php ( $menu [ 'target' ], 1 ) . " ', \n " ;
$str_menu .= " \t \t \t 'user' => " . (( int ) $menu [ 'user' ]) . " , \n " ;
$str_menu .= " \t \t \t 'object' => ' " . dol_escape_php ( $menu [ 'object' ], 1 ) . " ', \n " ;
2024-03-07 19:16:48 +00:00
$str_menu .= " \t \t ); \n " ;
2023-05-23 15:37:52 +00:00
2023-10-13 13:42:15 +00:00
if ( is_null ( $next_val ) || $val_actuel [ 'leftmenu' ] !== $next_val [ 'leftmenu' ]) {
2023-05-23 15:37:52 +00:00
$str_menu .= $end . " \n " ;
}
}
}
2024-04-18 00:21:42 +00:00
dolReplaceInFile ( $file , array ( '/* BEGIN MODULEBUILDER LEFTMENU MYOBJECT */' => $str_menu . " \n \t \t /* BEGIN MODULEBUILDER LEFTMENU MYOBJECT */ " ));
2023-05-23 15:37:52 +00:00
return 1 ;
2023-12-04 11:05:28 +00:00
}
return - 1 ;
2023-05-23 15:37:52 +00:00
}
2023-08-09 10:37:20 +00:00
2023-08-09 11:19:52 +00:00
/**
* Updates a dictionary in a module descriptor file .
*
2024-08-18 16:16:08 +00:00
* @ param string $module The name of the module .
* @ param string $file The path to the module descriptor file .
2024-08-18 20:02:25 +00:00
* @ param array { langs : string , tabname : string [], tablib : string [], tabsql : string [], tabsqlsort : string [], tabfield : string [], tabfieldvalue : string [], tabfieldinsert : string [], tabrowid : string [], tabcond : array < string | bool | int > , tabhelp : array < array { code : string , field2 : string } > } $dicts The dictionary data to be updated .
2024-08-18 16:16:08 +00:00
* @ return int Returns the number of replacements made in the file .
2023-08-09 11:19:52 +00:00
*/
function updateDictionaryInFile ( $module , $file , $dicts )
{
$isEmpty = false ;
2024-08-18 16:16:08 +00:00
$dicData = " \t \t \$ this->dictionaries = array( \n " ;
2023-08-09 11:43:44 +00:00
$module = strtolower ( $module );
2023-08-09 11:19:52 +00:00
foreach ( $dicts as $key => $value ) {
if ( empty ( $value )) {
$isEmpty = true ;
2024-08-18 16:16:08 +00:00
$dicData = " \t \t \$ this->dictionaries = array(); " ;
2023-08-09 11:19:52 +00:00
break ;
}
2024-08-18 16:16:08 +00:00
$dicData .= " \t \t \t ' $key ' => " ;
2023-08-09 11:19:52 +00:00
if ( $key === 'tabcond' ) {
2024-03-14 20:59:15 +00:00
$conditions = array_map (
/**
2024-08-18 16:16:08 +00:00
* @ param bool | string | int $val
* @ return string | int
2024-03-14 20:59:15 +00:00
*/
2024-08-18 16:16:08 +00:00
static function ( $val ) use ( $module ) {
2024-03-14 20:59:15 +00:00
return is_bool ( $val ) ? " isModEnabled(' $module ') " : $val ;
},
$value
);
2024-08-18 16:16:08 +00:00
$dicData .= " array( " . implode ( " , " , $conditions ) . " ) " ;
2023-08-09 11:19:52 +00:00
} elseif ( $key === 'tabhelp' ) {
$helpItems = array ();
2024-01-09 19:39:04 +00:00
foreach ( $value as $helpValue ) {
2024-08-18 16:16:08 +00:00
$helpItems [] = " array('code' => \$ langs->trans(' " . $helpValue [ 'code' ] . " '), 'field2' => 'field2tooltip') " ;
2023-08-09 11:19:52 +00:00
}
$dicData .= " array( " . implode ( " , " , $helpItems ) . " ) " ;
} else {
if ( is_array ( $value )) {
2024-03-14 20:59:15 +00:00
$dicData .= " array( " . implode (
" , " ,
array_map (
/**
2024-08-18 16:16:08 +00:00
* @ param string $val
* @ return string
2024-03-14 20:59:15 +00:00
*/
static function ( $val ) {
return " ' $val ' " ;
},
$value
)
) . " ) " ;
2023-08-09 11:19:52 +00:00
} else {
$dicData .= " ' $value ' " ;
}
}
$dicData .= " , \n " ;
}
$dicData .= ( ! $isEmpty ? " \t \t ); " : '' );
$stringDic = getFromFile ( $file , '/* BEGIN MODULEBUILDER DICTIONARIES */' , '/* END MODULEBUILDER DICTIONARIES */' );
$writeInfile = dolReplaceInFile ( $file , array ( $stringDic => $dicData . " \n " ));
return $writeInfile ;
}
2023-08-09 10:37:20 +00:00
/**
2024-08-18 16:16:08 +00:00
* Create a new dictionary table .
2023-08-09 10:37:20 +00:00
*
2024-08-18 16:16:08 +00:00
* It generates the necessary SQL code to define the table structure ,
2023-08-09 10:37:20 +00:00
* including columns such as 'rowid' , 'code' , 'label' , 'position' , 'use_default' , 'active' , etc . The table name is constructed based on the provided $namedic parameter .
*
2024-08-18 16:16:08 +00:00
* @ param string $modulename The lowercase name of the module for which the dictionary table is being created .
* @ param string $file The file path to the Dolibarr module builder file where the dictionaries are defined .
* @ param string $namedic The name of the dictionary , which will also be used as the base for the table name .
2024-08-18 20:02:25 +00:00
* @ param ? array { langs : string , tabname : string [], tablib : string [], tabsql : string [], tabsqlsort : string [], tabfield : string [], tabfieldvalue : string [], tabfieldinsert : string [], tabrowid : string [], tabcond : array < string | bool | int > , tabhelp : array < array { code : string , field2 : string } > } $dictionnaires An optional array containing pre - existing dictionary data , including tabname , tablib , tabsql , etc .
2024-08-18 16:16:08 +00:00
* @ return int <- 1 , - 1 > Return int < 0 if error , return nothing on success
2023-08-09 10:37:20 +00:00
*/
function createNewDictionnary ( $modulename , $file , $namedic , $dictionnaires = null )
{
global $db , $langs ;
if ( empty ( $namedic )) {
setEventMessages ( $langs -> trans ( " ErrorEmptyNameDic " ), null , 'errors' );
2024-06-07 23:23:01 +00:00
return - 1 ;
2023-08-09 10:37:20 +00:00
}
if ( ! file_exists ( $file )) {
return - 1 ;
}
$modulename = strtolower ( $modulename );
if ( empty ( $dictionnaires )) {
2023-09-08 10:11:52 +00:00
$dictionnaires = array ( 'langs' => '' , 'tabname' => array (), 'tablib' => array (), 'tabsql' => array (), 'tabsqlsort' => array (), 'tabfield' => array (), 'tabfieldvalue' => array (), 'tabfieldinsert' => array (), 'tabrowid' => array (), 'tabcond' => array (), 'tabhelp' => array ());
2023-08-09 10:37:20 +00:00
}
$columns = array (
2024-03-21 14:54:29 +00:00
'rowid' => array ( 'type' => 'integer' , 'value' => 11 , 'extra' => 'AUTO_INCREMENT' ),
2024-03-30 23:36:03 +00:00
'code' => array ( 'type' => 'varchar' , 'value' => 255 , 'null' => 'NOT NULL' ),
'label' => array ( 'type' => 'varchar' , 'value' => 255 , 'null' => 'NOT NULL' ),
'position' => array ( 'type' => 'integer' , 'value' => 11 , 'null' => 'NULL' ),
'use_default' => array ( 'type' => 'varchar' , 'value' => 11 , 'default' => '1' ),
2024-03-21 11:10:09 +00:00
'active' => array ( 'type' => 'integer' , 'value' => 3 )
2023-08-09 10:37:20 +00:00
);
$primaryKey = 'rowid' ;
foreach ( $columns as $key => $value ) {
if ( $key === 'rowid' ) {
$primaryKey = 'rowid' ;
break ;
}
if ( ! array_key_exists ( 'rowid' , $columns )) {
$primaryKey = array_key_first ( $columns );
break ;
}
}
2024-03-21 11:58:39 +00:00
2023-08-09 11:19:52 +00:00
// check if tablename exist in Database and create it if not
2024-03-21 14:54:29 +00:00
$checkTable = $db -> DDLDescTable ( MAIN_DB_PREFIX . strtolower ( $namedic ));
2023-08-09 10:37:20 +00:00
if ( $checkTable && $db -> num_rows ( $checkTable ) > 0 ) {
setEventMessages ( $langs -> trans ( " ErrorTableExist " , $namedic ), null , 'errors' );
2024-06-07 23:23:01 +00:00
return - 1 ;
2023-08-09 10:37:20 +00:00
} else {
2024-03-21 11:10:09 +00:00
$_results = $db -> DDLCreateTable ( MAIN_DB_PREFIX . strtolower ( $namedic ), $columns , $primaryKey , " " );
2023-08-09 10:37:20 +00:00
if ( $_results < 0 ) {
dol_print_error ( $db );
$langs -> load ( " errors " );
setEventMessages ( $langs -> trans ( " ErrorTableNotFound " , $namedic ), null , 'errors' );
}
}
2024-01-13 18:48:20 +00:00
// rewrite dictionary if
2023-09-08 10:11:52 +00:00
$dictionnaires [ 'langs' ] = $modulename . '@' . $modulename ;
2023-08-09 11:43:44 +00:00
$dictionnaires [ 'tabname' ][] = strtolower ( $namedic );
2023-08-09 10:37:20 +00:00
$dictionnaires [ 'tablib' ][] = ucfirst ( substr ( $namedic , 2 ));
2024-03-16 16:59:31 +00:00
$dictionnaires [ 'tabsql' ][] = 'SELECT t.rowid as rowid, t.code, t.label, t.active FROM ' . MAIN_DB_PREFIX . strtolower ( $namedic ) . ' as t' ;
2023-08-09 10:37:20 +00:00
$dictionnaires [ 'tabsqlsort' ][] = ( array_key_exists ( 'label' , $columns ) ? 'label ASC' : '' );
$dictionnaires [ 'tabfield' ][] = ( array_key_exists ( 'code' , $columns ) && array_key_exists ( 'label' , $columns ) ? 'code,label' : '' );
$dictionnaires [ 'tabfieldvalue' ][] = ( array_key_exists ( 'code' , $columns ) && array_key_exists ( 'label' , $columns ) ? 'code,label' : '' );
$dictionnaires [ 'tabfieldinsert' ][] = ( array_key_exists ( 'code' , $columns ) && array_key_exists ( 'label' , $columns ) ? 'code,label' : '' );
$dictionnaires [ 'tabrowid' ][] = $primaryKey ;
2024-03-15 17:44:03 +00:00
$dictionnaires [ 'tabcond' ][] = isModEnabled ( '$modulename' ); // @phan-suppress-current-line UnknownModuleName
2024-03-07 19:16:48 +00:00
$dictionnaires [ 'tabhelp' ][] = ( array_key_exists ( 'code' , $columns ) ? array ( 'code' => $langs -> trans ( 'CodeTooltipHelp' ), 'field2' => 'field2tooltip' ) : '' );
2023-08-09 10:37:20 +00:00
// Build the dictionary string
2023-12-04 11:05:28 +00:00
$writeInfile = updateDictionaryInFile ( $modulename , $file , $dictionnaires );
2023-08-09 10:37:20 +00:00
if ( $writeInfile > 0 ) {
setEventMessages ( $langs -> trans ( " DictionariesCreated " , ucfirst ( substr ( $namedic , 2 ))), null );
}
2024-06-07 23:23:01 +00:00
return - 1 ;
2023-08-09 10:37:20 +00:00
}
2023-08-16 10:45:01 +00:00
/**
2024-01-13 18:48:20 +00:00
* Generate Urls and add them to documentation module
2023-08-16 10:45:01 +00:00
*
2024-08-18 16:16:08 +00:00
* @ param string $file_api filename or path of api
* @ param string $file_doc filename or path of documentation
* @ return int <- 1 , 1 > - 1 if KO , 1 if OK , 0 if nothing change
2023-08-16 10:45:01 +00:00
*/
function writeApiUrlsInDoc ( $file_api , $file_doc )
{
$error = 0 ;
if ( ! dol_is_file ( $file_api ) || ! dol_is_file ( $file_doc )) {
$error ++ ;
}
$string = getFromFile ( $file_api , '/*begin methods CRUD*/' , '/*end methods CRUD*/' );
$extractUrls = explode ( " \n " , $string );
// extract urls from file
2024-08-18 16:16:08 +00:00
$urlValues = array ();
2023-08-16 10:45:01 +00:00
foreach ( $extractUrls as $key => $line ) {
$lineWithoutTabsSpaces = preg_replace ( '/^[\t\s]+/' , '' , $line );
if ( strpos ( $lineWithoutTabsSpaces , '* @url' ) === 0 ) {
$urlValue = trim ( substr ( $lineWithoutTabsSpaces , strlen ( '* @url' )));
$urlValues [] = $urlValue ;
}
}
// get urls by object
$str = $_SERVER [ 'HTTP_HOST' ] . '/api/index.php/' ;
2024-08-18 16:16:08 +00:00
$groupedUrls = array ();
2023-08-16 10:45:01 +00:00
foreach ( $urlValues as $url ) {
if ( preg_match ( '/(?:GET|POST|PUT|DELETE) (\w+)s/' , $url , $matches )) {
$objectName = $matches [ 1 ];
$url = $str . trim ( strstr ( $url , ' ' ));
$groupedUrls [ $objectName ][] = $url ;
}
}
if ( empty ( $groupedUrls )) {
$error ++ ;
}
2024-01-13 18:48:20 +00:00
// build format asciidoc for urls in table
2023-08-16 10:45:01 +00:00
if ( ! $error ) {
2024-01-14 11:26:37 +00:00
$asciiDocTable = " [options= \" header \" ] \n |=== \n |Object | URLs \n " ; // phpcs:ignore
2023-08-16 10:45:01 +00:00
foreach ( $groupedUrls as $objectName => $urls ) {
$urlsList = implode ( " + \n * " , $urls );
$asciiDocTable .= " | $objectName | \n * $urlsList + \n " ;
}
$asciiDocTable .= " |=== \n " ;
$file_write = dolReplaceInFile ( $file_doc , array ( '__API_DOC__' => '__API_DOC__' . " \n " . $asciiDocTable ));
if ( $file_write < 0 ) {
return - 1 ;
}
return 1 ;
}
return - 1 ;
}
2023-09-05 16:50:04 +00:00
/**
* count directories or files in modulebuilder folder
2024-08-18 16:16:08 +00:00
* @ param string $path path of directory
* @ param int < 1 , 2 > $type type of file 1 = file , 2 = directory
* @ return int | false False on failure ( path is not a directory )
2023-09-05 16:50:04 +00:00
*/
function countItemsInDirectory ( $path , $type = 1 )
{
if ( ! is_dir ( $path )) {
return false ;
}
$allFilesAndDirs = scandir ( $path );
$count = 0 ;
foreach ( $allFilesAndDirs as $item ) {
if ( $item != '.' && $item != '..' ) {
if ( $type == 1 && is_file ( $path . DIRECTORY_SEPARATOR . $item ) && strpos ( $item , '.back' ) === false ) {
$count ++ ;
} elseif ( $type == 2 && is_dir ( $path . DIRECTORY_SEPARATOR . $item )) {
$count ++ ;
}
}
}
return $count ;
}
NEW: ModuleBuilder - selectable object tabs (+ fixes for multi-object generation) (#38570)
* 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>
2026-06-09 17:04:00 +00:00
/**
* Return the map of optional tabs that can be generated for a ModuleBuilder object .
* The CARD tab is always generated and is therefore not listed here .
* HISTORY is an alias of AGENDA ( object event history is the agenda tab in Dolibarr ) .
*
* @ return array < string , array { file : string , var : string , marker : string , label : string } > Map : tab key => metadata
*/
function getModuleBuilderObjectTabs ()
{
return array (
'contact' => array ( 'file' => 'myobject_contact.php' , 'var' => 'showtabofpagecontact' , 'marker' => 'CONTACT' , 'label' => 'Contacts' ),
'note' => array ( 'file' => 'myobject_note.php' , 'var' => 'showtabofpagenote' , 'marker' => 'NOTE' , 'label' => 'Notes' ),
'document' => array ( 'file' => 'myobject_document.php' , 'var' => 'showtabofpagedocument' , 'marker' => 'DOCUMENT' , 'label' => 'Documents' ),
'agenda' => array ( 'file' => 'myobject_agenda.php' , 'var' => 'showtabofpageagenda' , 'marker' => 'AGENDA' , 'label' => 'Events' ),
);
}
/**
* Filter a list of requested tab keys against the known optional tabs map .
* Protects against injection of unknown keys , removes duplicates , normalizes order .
*
* @ param string [] $requested Raw tab keys requested by the user ( e . g . from GETPOST array )
* @ param array < string , array { file : string , var : string , marker : string , label : string } > $map Map from getModuleBuilderObjectTabs ()
* @ return string [] Sanitized list of valid tab keys , in map order
*/
function filterEnabledTabs ( $requested , $map )
{
$valid = array ();
if ( ! is_array ( $requested ) || empty ( $requested )) {
return $valid ;
}
foreach ( array_keys ( $map ) as $tabkey ) {
if ( in_array ( $tabkey , $requested , true )) {
$valid [] = $tabkey ;
}
}
return $valid ;
}
/**
* Apply substitutions to a module descriptor file while preserving the MODULEBUILDER comment markers .
* Markers such as " BEGIN MODULEBUILDER LEFTMENU MYOBJECT " must keep their MYOBJECT / MYMODULE placeholder
* so that generating subsequent objects can still locate them ( see checkExistComment ()) . A blanket
* substitution would rewrite them to the first object name and break the generation of further objects .
*
* @ param string $file Path to the module descriptor file
* @ param array < string , string > $arrayreplacement Substitution map ( search => replace ), applied as literal strings
* @ return int 1 on success , - 1 on read / write error
*/
function dolReplaceInFilePreservingModuleBuilderMarkers ( $file , $arrayreplacement )
{
if ( ! file_exists ( $file )) {
return - 1 ;
}
$content = file_get_contents ( $file );
if ( $content === false ) {
return - 1 ;
}
// Hide every "/* BEGIN|END MODULEBUILDER ... */" marker behind a sentinel before substituting
$foundmarkers = array ();
preg_match_all ( '/\/\*\s*(?:BEGIN|END) MODULEBUILDER [^*]*\*\//' , $content , $foundmarkers );
$sentinels = array ();
foreach ( array_values ( array_unique ( $foundmarkers [ 0 ])) as $index => $marker ) {
$key = " \0 MODULEBUILDERMARKER " . $index . " \0 " ;
$sentinels [ $key ] = $marker ;
$content = str_replace ( $marker , $key , $content );
}
$content = str_replace ( array_keys ( $arrayreplacement ), array_values ( $arrayreplacement ), $content );
// Restore the protected markers untouched
if ( ! empty ( $sentinels )) {
$content = strtr ( $content , $sentinels );
}
if ( file_put_contents ( $file , $content ) === false ) {
return - 1 ;
}
return 1 ;
}