Passed
Pull Request — dev (#6)
by Rafael
79:24 queued 24:08
created

DolibarrModules   F

Complexity

Total Complexity 462

Size/Duplication

Total Lines 2823
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 1260
dl 0
loc 2823
rs 0.8
c 0
b 0
f 0
wmc 462

55 Methods

Rating   Name   Duplication   Size   Complexity  
B setPerms() 0 35 7
A getModules() 0 6 2
A __construct() 0 3 1
A getImportDatasetLabel() 0 12 2
A insert_dirs() 0 30 3
A remove() 0 3 1
B getChangeLog() 0 38 6
A forceDeactivate() 0 16 1
A _unactive() 0 18 3
D insert_module_parts() 0 105 25
A getObj() 0 9 2
A getLastActivationDate() 0 22 3
A checkForUpdate() 0 20 5
A declareNewDictionary() 0 10 5
F insert_const() 0 61 15
B getDescLongReadmeFound() 0 27 7
F getKanbanView() 0 82 19
A getModulePosition() 0 10 3
D insert_cronjobs() 0 124 41
A isExperimental() 0 3 1
A getName() 0 25 6
B isCoreOrExternalModule() 0 15 8
A getExportDatasetLabel() 0 11 2
A isDeprecated() 0 3 1
D insert_permissions() 0 157 22
F insert_menus() 0 92 23
B _active() 0 41 7
A getLangFilesArray() 0 3 1
F _init() 0 98 20
D create_dirs() 0 57 18
A getPublisher() 0 3 1
A delete_cronjobs() 0 23 4
B delete_module_parts() 0 33 9
A delete_tabs() 0 18 2
B insert_tabs() 0 59 10
B _getModules() 0 32 8
B getLastActivationInfo() 0 31 7
A isActivated() 0 4 1
A isDevelopment() 0 3 1
B delete_boxes() 0 66 11
A delete_permissions() 0 24 4
A _getModuleName() 0 14 6
F _remove() 0 86 17
A delete_const() 0 28 6
A delete_menus() 0 23 3
A init() 0 3 1
D insert_boxes() 0 88 23
A getNameOf() 0 16 3
A getModule() 0 20 5
B getDescLong() 0 47 7
A getPublisherUrl() 0 3 1
A delete_dirs() 0 18 2
F _load_tables() 0 174 54
A getDesc() 0 25 6
B getVersion() 0 24 10

How to fix   Complexity   

Complex Class

Complex classes like DolibarrModules often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DolibarrModules, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/* Copyright (C) 2003-2007  Rodolphe Quiedeville        <[email protected]>
4
 * Copyright (C) 2004		Sebastien Di Cintio		    <[email protected]>
5
 * Copyright (C) 2004		Benoit Mortier			    <[email protected]>
6
 * Copyright (C) 2004		Eric Seigne				    <[email protected]>
7
 * Copyright (C) 2005-2013	Laurent Destailleur		    <[email protected]>
8
 * Copyright (C) 2005-2024	Regis Houssin			    <[email protected]>
9
 * Copyright (C) 2014		Raphaël Doursenaud		    <[email protected]>
10
 * Copyright (C) 2018		Josep Lluís Amador		    <[email protected]>
11
 * Copyright (C) 2019-2024	Frédéric France			    <[email protected]>
12
 * Copyright (C) 2024		MDW						    <[email protected]>
13
 * Copyright (C) 2024       Rafael San José             <[email protected]>
14
 *
15
 * This program is free software; you can redistribute it and/or modify
16
 * it under the terms of the GNU General Public License as published by
17
 * the Free Software Foundation; either version 3 of the License, or
18
 * (at your option) any later version.
19
 *
20
 * This program is distributed in the hope that it will be useful,
21
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
22
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
23
 * GNU General Public License for more details.
24
 *
25
 * You should have received a copy of the GNU General Public License
26
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
27
 */
28
29
namespace Dolibarr\Core\Base;
30
31
use Dolibarr\Code\Core\Classes\InfoBox;
32
use Dolibarr\Code\Core\Classes\Menubase;
33
use Dolibarr\Code\User\Classes\User;
34
use DoliDB;
35
36
/**
37
 * \file           htdocs/core/modules/DolibarrModules.class.php
38
 * \brief          File of parent class of module descriptor class files
39
 */
40
41
/**
42
 * Class DolibarrModules
43
 *
44
 * Parent class for module descriptor class files
45
 */
46
abstract class DolibarrModules
47
{
48
    const KEY_ID = 0;
49
    const KEY_LABEL = 1;
50
    const KEY_TYPE = 2;
51
    const KEY_DEFAULT = 3;
52
    const KEY_FIRST_LEVEL = 4;
53
    const KEY_SECOND_LEVEL = 5;
54
    const KEY_MODULE = 6;
55
    const KEY_ENABLED = 7;
56
    /**
57
     * @var DoliDB  Database handler
58
     */
59
    public $db;
60
    /**
61
     * @var int     Module unique ID
62
     * @see https://wiki.dolibarr.org/index.php/List_of_modules_id
63
     */
64
    public $numero;
65
    /**
66
     * @var string  Publisher name
67
     */
68
    public $editor_name;
69
    /**
70
     * @var string  URL of module at publisher site
71
     */
72
    public $editor_url;
73
    /**
74
     * @var string  URL of logo of the publisher. Must be image filename into the module/img directory followed with @modulename. Example: 'myimage.png@mymodule'.
75
     */
76
    public $editor_squarred_logo;
77
    /**
78
     * @var string  Family
79
     * @see $familyinfo
80
     *
81
     * Native values: 'crm', 'financial', 'hr', 'projects', 'products', 'ecm', 'technic', 'other'.
82
     * Use familyinfo to declare a custom value.
83
     */
84
    public $family;
85
    /**
86
     * @var array<string,array{position:string,label:string}> Custom family information
87
     * @see $family
88
     *
89
     * e.g.:
90
     * array(
91
     *     'myownfamily' => array(
92
     *         'position' => '001',
93
     *         'label' => $langs->trans("MyOwnFamily")
94
     *     )
95
     * );
96
     */
97
    public $familyinfo;
98
    /**
99
     * @var string  Module position on 2 digits
100
     */
101
    public $module_position = '50';
102
    /**
103
     * @var string  Module name
104
     *
105
     * Only used if Module[ID]Name translation string is not found.
106
     *
107
     * You can use the following code to automatically derive it from your module's class name:
108
     * preg_replace('/^mod/i', '', get_class($this))
109
     */
110
    public $name;
111
    /**
112
     * @var string[] Paths to create when module is activated
113
     *
114
     * e.g.: array('/mymodule/temp')
115
     */
116
    public $dirs = array();
117
    /**
118
     * @var array Module boxes
119
     */
120
    public $boxes = array(); // deprecated
121
    /**
122
     * @var array Module constants
123
     */
124
    public $const = array();
125
    /**
126
     * @var array Module cron jobs entries
127
     */
128
    public $cronjobs = array();
129
    /**
130
     * @var array   Module access rights
131
     */
132
    public $rights;
133
    /**
134
     * @var int     1=Admin is always granted of permission of modules (even when module is disabled)
135
     */
136
    public $rights_admin_allowed;
137
    /**
138
     * @var string  Module access rights family
139
     */
140
    public $rights_class;
141
    /**
142
     * @var array|int   Module menu entries (1 means the menu entries are not declared into module descriptor but are hardcoded into menu manager)
143
     */
144
    public $menu = array();
145
146
    /**
147
     * @var array{triggers?:int<0,1>,login?:int<0,1>,substitutions?:int<0,1>,menus?:int<0,1>,theme?:int<0,1>,tpl?:int<0,1>,barcode?:int<0,1>,models?:int<0,1>,printing?:int<0,1>,css?:string[],js?:string[],hooks?:array{data?:string[],entity?:string},moduleforexternal?:int<0,1>,websitetemplates?:int<0,1>,contactelement?:int<0,1>} Module parts
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{triggers?:int<0,1>...ntactelement?:int<0,1>} at position 4 could not be parsed: Expected '}' at position 4, but found 'int'.
Loading history...
148
     *  array(
149
     *      // Set this to 1 if module has its own trigger directory (/mymodule/core/triggers)
150
     *      'triggers' => 0,
151
     *      // Set this to 1 if module has its own login method directory (/mymodule/core/login)
152
     *      'login' => 0,
153
     *      // Set this to 1 if module has its own substitution function file (/mymodule/core/substitutions)
154
     *      'substitutions' => 0,
155
     *      // Set this to 1 if module has its own menus handler directory (/mymodule/core/menus)
156
     *      'menus' => 0,
157
     *      // Set this to 1 if module has its own theme directory (/mymodule/theme)
158
     *      'theme' => 0,
159
     *      // Set this to 1 if module overwrite template dir (/mymodule/core/tpl)
160
     *      'tpl' => 0,
161
     *      // Set this to 1 if module has its own barcode directory (/mymodule/core/modules/barcode)
162
     *      'barcode' => 0,
163
     *      // Set this to 1 if module has its own models directory (/mymodule/core/modules/xxx)
164
     *      'models' => 0,
165
     *      // Set this to relative path of css file if module has its own css file
166
     *      'css' => '/mymodule/css/mymodule.css.php',
167
     *      // Set this to relative path of js file if module must load a js on all pages
168
     *      'js' => '/mymodule/js/mymodule.js',
169
     *      // Set here all hooks context managed by module
170
     *      'hooks' => array('hookcontext1','hookcontext2')
171
     *  )
172
     */
173
    public $module_parts = array();
174
175
    /**
176
     * @var string Error message
177
     */
178
    public $error;
179
180
    /**
181
     * @var string[] Array of Errors messages
182
     */
183
    public $errors;
184
185
    /**
186
     * @var string Module version
187
     * @see http://semver.org
188
     *
189
     * The following keywords can also be used:
190
     * 'development'
191
     * 'experimental'
192
     * 'dolibarr': only for core modules that share its version
193
     * 'dolibarr_deprecated': only for deprecated core modules
194
     */
195
    public $version;
196
197
    /**
198
     * Module last version
199
     * @var string $lastVersion
200
     */
201
    public $lastVersion = '';
202
203
    /**
204
     * true indicate this module need update
205
     * @var bool $needUpdate
206
     */
207
    public $needUpdate = false;
208
209
    /**
210
     * @var string Module description (short text)
211
     *
212
     * Only used if Module[ID]Desc translation string is not found.
213
     */
214
    public $description;
215
216
    /**
217
     * @var   string Module description (long text)
218
     * @since 4.0.0
219
     *
220
     * HTML content supported.
221
     */
222
    public $descriptionlong;
223
224
    /**
225
     * @var array dictionaries description
226
     */
227
    public $dictionaries = array();
228
229
    /**
230
     * @var array tabs description
231
     */
232
    public $tabs;
233
234
    // For exports
235
236
    /**
237
     * @var string Module export code
238
     */
239
    public $export_code;
240
241
    /**
242
     * @var string[] Module export label
243
     */
244
    public $export_label;
245
246
    public $export_icon;
247
248
    /**
249
     * @var array export enabled
250
     */
251
    public $export_enabled;
252
    public $export_permission;
253
    public $export_fields_array;
254
    public $export_TypeFields_array; // Array of key=>type where type can be 'Numeric', 'Date', 'Text', 'Boolean', 'Status', 'List:xxx:fieldlabel:rowid'
255
    public $export_entities_array;
256
    public $export_aggregate_array;
257
    public $export_examplevalues_array;
258
    public $export_help_array;
259
    public $export_special_array; // special or computed field
260
    public $export_dependencies_array;
261
    public $export_sql_start;
262
    public $export_sql_end;
263
    public $export_sql_order;
264
265
266
    // For import
267
268
    /**
269
     * @var string Module import code
270
     */
271
    public $import_code;
272
273
    /**
274
     * @var string[] Module import label
275
     */
276
    public $import_label;
277
278
    public $import_icon;
279
    public $import_entities_array;
280
    public $import_tables_array;
281
    public $import_tables_creator_array;
282
    public $import_fields_array;
283
    public $import_fieldshidden_array;
284
    public $import_convertvalue_array;
285
    public $import_regex_array;
286
    public $import_examplevalues_array;
287
    public $import_updatekeys_array;
288
    public $import_run_sql_after_array;
289
    public $import_TypeFields_array;
290
    public $import_help_array;
291
292
    /**
293
     * @var string Module constant name
294
     */
295
    public $const_name;
296
297
    /**
298
     * @var bool Module can't be disabled
299
     */
300
    public $always_enabled;
301
302
    /**
303
     * @var bool Module is disabled
304
     */
305
    public $disabled;
306
307
    /**
308
     * @var int Module is enabled globally (Multicompany support)
309
     */
310
    public $core_enabled;
311
312
    /**
313
     * @var string Name of image file used for this module
314
     *
315
     * If file is in theme/yourtheme/img directory under name object_pictoname.png use 'pictoname'
316
     * If file is in module/img directory under name object_pictoname.png use 'pictoname@module'
317
     */
318
    public $picto;
319
320
    /**
321
     * @var string[]|string     List of config pages (Old modules uses a string. New one must use an array)
322
     *
323
     * Name of php pages stored into module/admin directory, used to setup module.
324
     * e.g.: array("setup.php@mymodule")
325
     */
326
    public $config_page_url;
327
328
329
    /**
330
     * @var array   List of module class names that must be enabled if this module is enabled. e.g.: array('modAnotherModule', 'FR'=>'modYetAnotherModule')
331
     *              Another example : array('always'=>array("modBanque", "modFacture", "modProduct", "modCategorie"), 'FR'=>array('modBlockedLog'));
332
     * @see $requiredby
333
     */
334
    public $depends;
335
336
    /**
337
     * @var string[] List of module class names to disable if the module is disabled.
338
     * @see $depends
339
     */
340
    public $requiredby;
341
342
    /**
343
     * @var string[] List of module class names as string this module is in conflict with.
344
     * @see $depends
345
     */
346
    public $conflictwith;
347
348
    /**
349
     * @var string[] Module language files
350
     */
351
    public $langfiles;
352
353
    /**
354
     * @var array<string,string> Array of warnings to show when we activate the module
355
     *
356
     * array('always'='text') or array('FR'='text')
357
     */
358
    public $warnings_activation;
359
360
    /**
361
     * @var array<string,string> Array of warnings to show when we activate an external module
362
     *
363
     * array('always'='text') or array('FR'='text')
364
     */
365
    public $warnings_activation_ext;
366
367
    /**
368
     * @var array<string,string> Array of warnings to show when we disable the module
369
     *
370
     * array('always'='text') or array('FR'='text')
371
     */
372
    public $warnings_unactivation;
373
374
    /**
375
     * @var array Minimum version of PHP required by module.
376
     * e.g.: PHP ≥ 7.0 = array(7, 0)
377
     */
378
    public $phpmin;
379
380
    public $phpmax;
381
382
    /**
383
     * @var array Minimum version of Dolibarr required by module.
384
     * e.g.: Dolibarr ≥ 3.6 = array(3, 6)
385
     */
386
    public $need_dolibarr_version;
387
388
    public $need_javascript_ajax;
389
390
    public $enabled_bydefault;
391
392
    /**
393
     * @var bool|int Whether to hide the module.
394
     */
395
    public $hidden = false;
396
397
    /**
398
     * @var string url to check for module update
399
     */
400
    public $url_last_version;
401
402
403
    /**
404
     * Constructor. Define names, constants, directories, boxes, permissions
405
     *
406
     * @param DoliDB $db Database handler
407
     */
408
    public function __construct($db)
409
    {
410
        $this->db = $db;
411
    }
412
413
    public static function setPerms($db, $langs, $entity)
414
    {
415
        $db->begin();
416
417
// Search all modules with permission and reload permissions def.
418
        $modules = array();
419
        $modulesdir = dolGetModulesDirs();
420
421
        $allModules = static::getModules($modulesdir);
422
        foreach ($allModules as $modName => $filename) {
423
            if (str_starts_with($modName, 'mod')) {
424
                include_once $filename;
425
                $objMod = new $modName($db);
426
            } else {
427
                $className = "\\Dolibarr\\Modules\\" . $modName;
428
                $objMod = new $className($db);
429
            }
430
431
            // Load all lang files of module
432
            if (isset($objMod->langfiles) && is_array($objMod->langfiles)) {
433
                foreach ($objMod->langfiles as $domain) {
434
                    $langs->load($domain);
435
                }
436
            }
437
            // Load all permissions
438
            if ($objMod->rights_class) {
439
                $ret = $objMod->insert_permissions(0, $entity);
440
                $modules[$objMod->rights_class] = $objMod;
441
                //print "modules[".$objMod->rights_class."]=$objMod;";
442
            }
443
        }
444
445
        $db->commit();
446
447
        return $modules;
448
    }
449
450
    public static function getModules($modulesdir = null)
451
    {
452
        if (!isset($modulesdir)) {
453
            $modulesdir = dolGetModulesDirs();
454
        }
455
        return static::_getModules($modulesdir);
456
    }
457
    // We should but can't set this as abstract because this will make dolibarr hang
458
    // after migration due to old module not implementing. We must wait PHP is able to make
459
    // a try catch on Fatal error to manage this correctly.
460
    // We need constructor into function unActivateModule into admin.lib.php
461
462
    public static function _getModuleName($file)
463
    {
464
        $new_type = !str_starts_with($file, 'mod') && str_ends_with($file, '.php');
465
        $old_type = str_starts_with($file, 'mod') && str_ends_with($file, '.class.php');
466
467
        if (!($new_type || $old_type)) {
468
            return null;
469
        }
470
471
        if ($new_type) {
472
            return substr($file, 0, strrpos($file, '.'));
473
        }
474
475
        return substr($file, 0, dol_strlen($file) - 10);
476
    }
477
478
    /**
479
     * Obtiene un array con los módulos instalados.
480
     * TODO: Sustituir donde se use:
481
     * if (is_readable($dir . $file) && substr($file, 0, 3) == 'mod' && substr($file, dol_strlen($file) - 10) == '.class.php') {
482
     *
483
     * @param $modulesdir
484
     * @return array
485
     * @throws \Exception
486
     */
487
    private static function _getModules($modulesdir): array
488
    {
489
        $result = [];
490
        foreach ($modulesdir as $dir) {
491
            dol_syslog("Scan directory " . $dir . " for module descriptor files (XXX.php OR modXXX.class.php)");
492
            $handle = @opendir($dir);
493
            if (!is_resource($handle)) {
494
                dol_syslog("htdocs/admin/modules.php: Failed to open directory " . $dir . ". See permission and open_basedir option.", LOG_WARNING);
495
                continue;
496
            }
497
498
            while (($file = readdir($handle)) !== false) {
499
                $filename = $dir . $file;
500
                if (is_dir($filename) || !is_readable($filename)) {
501
                    continue;
502
                }
503
504
                $className = static::_getModuleName($file);
505
506
                if (empty($className)) {
507
                    continue;
508
                }
509
510
                if (isset($result[$className])) {
511
                    continue;
512
                }
513
514
                $result[$className] = $filename;
515
            }
516
            closedir($handle);
517
        }
518
        return $result;
519
    }
520
521
    /**
522
     * Adds access rights
523
     *
524
     * @param int $reinitadminperms If 1, we also grant them to all admin users
525
     * @param int $force_entity Force current entity
526
     * @param int $notrigger 1=Does not execute triggers, 0= execute triggers
527
     * @return int                      Error count (0 if OK)
528
     */
529
    public function insert_permissions($reinitadminperms = 0, $force_entity = null, $notrigger = 0)
530
    {
531
        // phpcs:enable
532
        global $conf, $user;
533
534
        $err = 0;
535
        $entity = (!empty($force_entity) ? $force_entity : $conf->entity);
536
537
        dol_syslog(get_class($this) . "::insert_permissions", LOG_DEBUG);
538
539
        // Test if module is activated
540
        $sql_del = "SELECT " . $this->db->decrypt('value') . " as value";
541
        $sql_del .= " FROM " . MAIN_DB_PREFIX . "const";
542
        $sql_del .= " WHERE " . $this->db->decrypt('name') . " = '" . $this->db->escape($this->const_name) . "'";
543
        $sql_del .= " AND entity IN (0," . ((int)$entity) . ")";
544
545
        $resql = $this->db->query($sql_del);
546
547
        if ($resql) {
548
            $obj = $this->db->fetch_object($resql);
549
550
            if ($obj !== null && !empty($obj->value) && !empty($this->rights)) {
551
    
552
                // TODO rights parameters with integer indexes are deprecated
553
                // $this->rights[$key][0] = $this->rights[$key][self::KEY_ID]
554
                // $this->rights[$key][1] = $this->rights[$key][self::KEY_LABEL]
555
                // $this->rights[$key][3] = $this->rights[$key][self::KEY_DEFAULT]
556
                // $this->rights[$key][4] = $this->rights[$key][self::KEY_FIRST_LEVEL]
557
                // $this->rights[$key][5] = $this->rights[$key][self::KEY_SECOND_LEVEL]
558
559
                // new parameters
560
                // $this->rights[$key][self::KEY_MODULE]    // possibility to define user right for an another module (default: current module name)
561
                // $this->rights[$key][self::KEY_ENABLED]   // condition to show or hide a user right (default: 1) (eg isModEnabled('anothermodule'))
562
563
                // If the module is active
564
                foreach ($this->rights as $key => $value) {
565
                    $r_id = $this->rights[$key][self::KEY_ID];  // permission id in llx_rights_def (not unique because primary key is couple id-entity)
566
                    $r_label = $this->rights[$key][self::KEY_LABEL];
567
                    $r_type = $this->rights[$key][self::KEY_TYPE] ?? 'w';   // TODO deprecated
568
                    $r_default = $this->rights[$key][self::KEY_DEFAULT] ?? 0;
569
                    $r_perms = $this->rights[$key][self::KEY_FIRST_LEVEL] ?? '';
570
                    $r_subperms = $this->rights[$key][self::KEY_SECOND_LEVEL] ?? '';
571
572
                    // KEY_FIRST_LEVEL (perms) must not be empty
573
                    if (empty($r_perms)) {
574
                        continue;
575
                    }
576
577
                    // name of module (default: current module name)
578
                    $r_module = (empty($this->rights_class) ? strtolower($this->name) : $this->rights_class);
579
580
                    // name of the module from which the right comes (default: empty means same module the permission is for)
581
                    $r_module_origin = '';
582
583
                    if (isset($this->rights[$key][self::KEY_MODULE])) {
584
                        // name of the module to which the right must be applied
585
                        $r_module = $this->rights[$key][self::KEY_MODULE];
586
                        // name of the module from which the right comes
587
                        $r_module_origin = (empty($this->rights_class) ? strtolower($this->name) : $this->rights_class);
588
                    }
589
590
                    // condition to show or hide a user right (default: 1) (eg isModEnabled('anothermodule') or ($conf->global->MAIN_FEATURES_LEVEL > 0) or etc..)
591
                    $r_enabled = $this->rights[$key][self::KEY_ENABLED] ?? '1';
592
593
                    // Search if perm already present
594
                    $sql = "SELECT count(*) as nb FROM " . MAIN_DB_PREFIX . "rights_def";
595
                    $sql .= " WHERE entity = " . ((int)$entity);
596
                    $sql .= " AND id = " . ((int)$r_id);
597
598
                    $resqlselect = $this->db->query($sql);
599
                    if ($resqlselect) {
600
                        $objcount = $this->db->fetch_object($resqlselect);
601
                        if ($objcount && $objcount->nb == 0) {
602
                            $sql = "INSERT INTO " . MAIN_DB_PREFIX . "rights_def (";
603
                            $sql .= "id";
604
                            $sql .= ", entity";
605
                            $sql .= ", libelle";
606
                            $sql .= ", module";
607
                            $sql .= ", module_origin";
608
                            $sql .= ", type";   // TODO deprecated
609
                            $sql .= ", bydefault";
610
                            $sql .= ", perms";
611
                            $sql .= ", subperms";
612
                            $sql .= ", enabled";
613
                            $sql .= ") VALUES (";
614
                            $sql .= ((int)$r_id);
615
                            $sql .= ", " . ((int)$entity);
616
                            $sql .= ", '" . $this->db->escape($r_label) . "'";
617
                            $sql .= ", '" . $this->db->escape($r_module) . "'";
618
                            $sql .= ", '" . $this->db->escape($r_module_origin) . "'";
619
                            $sql .= ", '" . $this->db->escape($r_type) . "'";   // TODO deprecated
620
                            $sql .= ", " . ((int)$r_default);
621
                            $sql .= ", '" . $this->db->escape($r_perms) . "'";
622
                            $sql .= ", '" . $this->db->escape($r_subperms) . "'";
623
                            $sql .= ", '" . $this->db->escape($r_enabled) . "'";
624
                            $sql .= ")";
625
626
                            $resqlinsert = $this->db->query($sql, 1);
627
628
                            if (!$resqlinsert) {
629
                                if ($this->db->errno() != "DB_ERROR_RECORD_ALREADY_EXISTS") {
630
                                    $this->error = $this->db->lasterror();
631
                                    $err++;
632
                                    break;
633
                                } else {
634
                                    dol_syslog(get_class($this) . "::insert_permissions record already exists", LOG_INFO);
635
                                }
636
                            }
637
638
                            $this->db->free($resqlinsert);
639
                        }
640
641
                        $this->db->free($resqlselect);
642
                    }
643
644
                    // If we want to init permissions on admin users
645
                    if (!empty($reinitadminperms)) {
646
                        $sql = "SELECT rowid FROM " . MAIN_DB_PREFIX . "user WHERE admin = 1";
647
                        dol_syslog(get_class($this) . "::insert_permissions Search all admin users", LOG_DEBUG);
648
649
                        $resqlseladmin = $this->db->query($sql, 1);
650
651
                        if ($resqlseladmin) {
652
                            $num = $this->db->num_rows($resqlseladmin);
653
                            $i = 0;
654
                            while ($i < $num) {
655
                                $obj2 = $this->db->fetch_object($resqlseladmin);
656
                                dol_syslog(get_class($this) . "::insert_permissions Add permission id " . $r_id . " to user id=" . $obj2->rowid);
657
658
                                $tmpuser = new User($this->db);
659
                                $result = $tmpuser->fetch($obj2->rowid);
660
                                if ($result > 0) {
661
                                    $tmpuser->addrights($r_id, '', '', 0, 1);
662
                                } else {
663
                                    dol_syslog(get_class($this) . "::insert_permissions Failed to add the permission to user because fetch return an error", LOG_ERR);
664
                                }
665
                                $i++;
666
                            }
667
                        } else {
668
                            dol_print_error($this->db);
669
                        }
670
                    }
671
                }
672
673
                if (!empty($reinitadminperms) && !empty($user->admin)) {  // Reload permission for current user if defined
674
                    // We reload permissions
675
                    $user->clearrights();
676
                    $user->getrights();
677
                }
678
            }
679
            $this->db->free($resql);
680
        } else {
681
            $this->error = $this->db->lasterror();
682
            $err++;
683
        }
684
685
        return $err;
686
    }
687
688
    public static function forceDeactivate($modName)
689
    {
690
        // phpcs:enable
691
        global $conf, $db;
692
693
        $name = static::getNameOf($modName);
694
        $entity = $conf->entity;
695
696
        $sql = "DELETE FROM " . MAIN_DB_PREFIX . "const";
697
        $sql .= " WHERE " . $db->decrypt('name') . " = '" . $name . "'";
698
        $sql .= " AND entity IN (0, " . $entity . ")";
699
700
        dump(['sql in DolibarrModules::forceDeactivate' => $sql]);
701
702
        dol_syslog($name . "::_unactive", LOG_DEBUG);
703
        $db->query($sql);
704
    }
705
706
    public static function getNameOf($modName)
707
    {
708
        /**
709
         * Find the last occurrence of a double backslash, to remove the namespace (if it exists)
710
         */
711
        $name = strrchr($modName, '\\');
712
        $name = $name ? ltrim($name, '\\') : $modName;
713
714
        /**
715
         * If the name is preceded by 'mod', 'mod' is removed.
716
         */
717
        if (str_starts_with($modName, 'mod')) {
718
            $name = substr($name, 3);
719
        }
720
721
        return strtoupper($name);
722
    }
723
724
    public static function isActivated($modName)
725
    {
726
        $name = 'MAIN_MODULE_' . static::getNameOf($modName);
727
        return !empty(getDolGlobalString($name));
728
    }
729
730
    /**
731
     * Obtains an instance of $modName, or null.
732
     *
733
     * @param $modName
734
     * @return DolibarrModules
735
     */
736
    public static function getModule($modName): ?DolibarrModules
737
    {
738
        global $db;
739
740
        $modules = static::getModules();
741
        if (str_starts_with($modName, 'mod')) {
742
            if (isset($modules[$modName]) && file_exists($modules[$modName])) {
743
                dump('FileExists ' . $modules[$modName] . ' for ' . $modName);
744
                include_once $modules[$modName];
745
                return new $modName($db);
746
            }
747
            $modName = str_replace('mod', '', $modName);
748
        }
749
750
        if (!isset($modules[$modName])) {
751
            return null;
752
        }
753
754
        $className = "\\Dolibarr\\Modules\\" . $modName;
755
        return new $className($db);
756
    }
757
758
    public static function getObj($db, $modName, $filename)
759
    {
760
        if (str_starts_with($modName, 'mod')) {
761
            include_once $filename;
762
            return new $modName($db);
763
        }
764
765
        $className = "Dolibarr\\Modules\\" . $modName;
766
        return $objMod = new $className($db);
767
    }
768
769
    public function isDevelopment()
770
    {
771
        return $this->version === 'development';
772
    }
773
774
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.PublicUnderscore
775
776
    public function isExperimental()
777
    {
778
        return $this->version === 'development';
779
    }
780
781
    public function isDeprecated()
782
    {
783
        return str_contains($this->version, 'deprecated');
784
    }
785
786
    /**
787
     * Function called when module is disabled.
788
     * The remove function removes tabs, constants, boxes, permissions and menus from Dolibarr database.
789
     * Data directories are not deleted
790
     *
791
     * @param string $options Options when enabling module ('', 'noboxes')
792
     * @return int                     1 if OK, 0 if KO
793
     */
794
    public function remove($options = '')
795
    {
796
        return $this->_remove(array(), $options);
797
    }
798
799
    /**
800
     * Disable function. Deletes the module constants and boxes from the database.
801
     *
802
     * @param string[] $array_sql SQL requests to be executed when module is disabled
803
     * @param string $options Options when disabling module:
804
     *
805
     * @return int                     1 if OK, 0 if KO
806
     */
807
    protected function _remove($array_sql, $options = '')
808
    {
809
        global $conf;
810
811
        // phpcs:enable
812
        $err = 0;
813
814
        $this->db->begin();
815
816
        // Remove activation module line (constant MAIN_MODULE_MYMODULE in llx_const)
817
        if (!$err) {
818
            $err += $this->_unactive();
819
        }
820
821
        // Remove activation of module's new tabs (MAIN_MODULE_MYMODULE_TABS_XXX in llx_const)
822
        if (!$err) {
823
            $err += $this->delete_tabs();
824
        }
825
826
        // Remove activation of module's parts (MAIN_MODULE_MYMODULE_XXX in llx_const)
827
        if (!$err) {
828
            $err += $this->delete_module_parts();
829
        }
830
831
        // Remove constants defined by modules
832
        if (!$err) {
833
            $err += $this->delete_const();
834
        }
835
836
        // Remove list of module's available boxes (entry in llx_boxes)
837
        if (!$err && !preg_match('/(newboxdefonly|noboxes)/', $options)) {
838
            $err += $this->delete_boxes(); // We don't have to delete if option ask to keep boxes safe or ask to add new box def only
839
        }
840
841
        // Remove list of module's cron job entries (entry in llx_cronjobs)
842
        if (!$err) {
843
            $err += $this->delete_cronjobs();
844
        }
845
846
        // Remove module's permissions from list of available permissions (entries in llx_rights_def)
847
        if (!$err) {
848
            $err += $this->delete_permissions();
849
        }
850
851
        // Remove module's menus (entries in llx_menu)
852
        if (!$err) {
853
            $err += $this->delete_menus();
854
        }
855
856
        // Remove module's directories
857
        if (!$err) {
858
            $err += $this->delete_dirs();
859
        }
860
861
        // Run complementary sql requests
862
        $num = count((array)$array_sql);
863
        for ($i = 0; $i < $num; $i++) {
864
            if (!$err) {
865
                dol_syslog(get_class($this) . "::_remove", LOG_DEBUG);
866
                $result = $this->db->query($array_sql[$i]);
867
                if (!$result) {
868
                    $this->error = $this->db->error();
869
                    $err++;
870
                }
871
            }
872
        }
873
874
        // Return code
875
        if (!$err) {
876
            $this->db->commit();
877
878
            // Disable modules
879
            $moduleNameInConf = strtolower(preg_replace('/^MAIN_MODULE_/', '', $this->const_name));
880
            // two exceptions to handle
881
            if ($moduleNameInConf === 'propale') {
882
                $moduleNameInConf = 'propal';
883
            } elseif ($moduleNameInConf === 'supplierproposal') {
884
                $moduleNameInConf = 'supplier_proposal';
885
            }
886
887
            unset($conf->modules[$moduleNameInConf]);   // Add this module in list of enabled modules so isModEnabled() will work (conf->module->enabled must no more be used)
888
889
            return 1;
890
        } else {
891
            $this->db->rollback();
892
            return 0;
893
        }
894
    }
895
896
    /**
897
     * Module deactivation
898
     *
899
     * @return int Error count (0 if OK)
900
     */
901
    protected function _unactive()
902
    {
903
        // phpcs:enable
904
        global $conf;
905
906
        $err = 0;
907
908
        // Common module
909
        $entity = ((!empty($this->always_enabled) || !empty($this->core_enabled)) ? 0 : $conf->entity);
910
911
        $sql = "DELETE FROM " . MAIN_DB_PREFIX . "const";
912
        $sql .= " WHERE " . $this->db->decrypt('name') . " = '" . $this->db->escape($this->const_name) . "'";
913
        $sql .= " AND entity IN (0, " . $entity . ")";
914
915
        dol_syslog(get_class($this) . "::_unactive", LOG_DEBUG);
916
        $this->db->query($sql);
917
918
        return $err;
919
    }
920
921
    /**
922
     * Removes tabs
923
     *
924
     * @return int Error count (0 if OK)
925
     */
926
    public function delete_tabs()
927
    {
928
        // phpcs:enable
929
        global $conf;
930
931
        $err = 0;
932
933
        $sql = "DELETE FROM " . MAIN_DB_PREFIX . "const";
934
        $sql .= " WHERE " . $this->db->decrypt('name') . " like '" . $this->db->escape($this->const_name) . "_TABS_%'";
935
        $sql .= " AND entity = " . $conf->entity;
936
937
        dol_syslog(get_class($this) . "::delete_tabs", LOG_DEBUG);
938
        if (!$this->db->query($sql)) {
939
            $this->error = $this->db->lasterror();
940
            $err++;
941
        }
942
943
        return $err;
944
    }
945
946
    /**
947
     * Removes generic parts
948
     *
949
     * @return int Error count (0 if OK)
950
     */
951
    public function delete_module_parts()
952
    {
953
        // phpcs:enable
954
        global $conf;
955
956
        $err = 0;
957
958
        if (is_array($this->module_parts)) {
959
            dol_syslog(get_class($this) . "::delete_module_parts", LOG_DEBUG);
960
961
            if (empty($this->module_parts['icon']) && !empty($this->picto) && preg_match('/^fa\-/', $this->picto)) {
962
                $this->module_parts['icon'] = $this->picto;
963
            }
964
965
            foreach ($this->module_parts as $key => $value) {
966
                // If entity is defined
967
                if (is_array($value) && isset($value['entity'])) {
968
                    $entity = $value['entity'];
969
                } else {
970
                    $entity = $conf->entity;
971
                }
972
973
                $sql = "DELETE FROM " . MAIN_DB_PREFIX . "const";
974
                $sql .= " WHERE " . $this->db->decrypt('name') . " LIKE '" . $this->db->escape($this->const_name) . "_" . strtoupper($key) . "'";
975
                $sql .= " AND entity = " . ((int)$entity);
976
977
                if (!$this->db->query($sql)) {
978
                    $this->error = $this->db->lasterror();
979
                    $err++;
980
                }
981
            }
982
        }
983
        return $err;
984
    }
985
986
    /**
987
     * Removes constants tagged 'deleteonunactive'
988
     *
989
     * @return int Return integer <0 if KO, 0 if OK
990
     */
991
    public function delete_const()
992
    {
993
        // phpcs:enable
994
        global $conf;
995
996
        $err = 0;
997
998
        if (empty($this->const)) {
999
            return 0;
1000
        }
1001
1002
        foreach ($this->const as $key => $value) {
1003
            $name = $this->const[$key][0];
1004
            $deleteonunactive = (!empty($this->const[$key][6])) ? 1 : 0;
1005
1006
            if ($deleteonunactive) {
1007
                $sql = "DELETE FROM " . MAIN_DB_PREFIX . "const";
1008
                $sql .= " WHERE " . $this->db->decrypt('name') . " = '" . $this->db->escape($name) . "'";
1009
                $sql .= " AND entity in (0, " . $conf->entity . ")";
1010
                dol_syslog(get_class($this) . "::delete_const", LOG_DEBUG);
1011
                if (!$this->db->query($sql)) {
1012
                    $this->error = $this->db->lasterror();
1013
                    $err++;
1014
                }
1015
            }
1016
        }
1017
1018
        return $err;
1019
    }
1020
1021
    /**
1022
     * Removes boxes
1023
     *
1024
     * @return int Error count (0 if OK)
1025
     */
1026
    public function delete_boxes()
1027
    {
1028
        // phpcs:enable
1029
        global $conf;
1030
1031
        $err = 0;
1032
1033
        if (is_array($this->boxes)) {
1034
            foreach ($this->boxes as $key => $value) {
1035
                //$titre = $this->boxes[$key][0];
1036
                if (empty($this->boxes[$key]['file'])) {
1037
                    $file = isset($this->boxes[$key][1]) ? $this->boxes[$key][1] : ''; // For backward compatibility
1038
                } else {
1039
                    $file = $this->boxes[$key]['file'];
1040
                }
1041
1042
                //$note  = $this->boxes[$key][2];
1043
1044
                // TODO If the box is also included by another module and the other module is still on, we should not remove it.
1045
                // For the moment, we manage this with hard coded exception
1046
                //print "Remove box ".$file.'<br>';
1047
                if ($file == 'box_graph_product_distribution.php') {
1048
                    if (isModEnabled("product") || isModEnabled("service")) {
1049
                        dol_syslog("We discard deleting module " . $file . " because another module still active requires it.");
1050
                        continue;
1051
                    }
1052
                }
1053
1054
                if ($this->db->type == 'sqlite3') {
1055
                    // sqlite doesn't support "USING" syntax.
1056
                    // TODO: remove this dependency.
1057
                    $sql = "DELETE FROM " . MAIN_DB_PREFIX . "boxes ";
1058
                    $sql .= "WHERE " . MAIN_DB_PREFIX . "boxes.box_id IN (";
1059
                    $sql .= "SELECT " . MAIN_DB_PREFIX . "boxes_def.rowid ";
1060
                    $sql .= "FROM " . MAIN_DB_PREFIX . "boxes_def ";
1061
                    $sql .= "WHERE " . MAIN_DB_PREFIX . "boxes_def.file = '" . $this->db->escape($file) . "') ";
1062
                    $sql .= "AND " . MAIN_DB_PREFIX . "boxes.entity = " . $conf->entity;
1063
                } else {
1064
                    $sql = "DELETE FROM " . MAIN_DB_PREFIX . "boxes";
1065
                    $sql .= " USING " . MAIN_DB_PREFIX . "boxes, " . MAIN_DB_PREFIX . "boxes_def";
1066
                    $sql .= " WHERE " . MAIN_DB_PREFIX . "boxes.box_id = " . MAIN_DB_PREFIX . "boxes_def.rowid";
1067
                    $sql .= " AND " . MAIN_DB_PREFIX . "boxes_def.file = '" . $this->db->escape($file) . "'";
1068
                    $sql .= " AND " . MAIN_DB_PREFIX . "boxes.entity = " . $conf->entity;
1069
                }
1070
1071
                dol_syslog(get_class($this) . "::delete_boxes", LOG_DEBUG);
1072
                $resql = $this->db->query($sql);
1073
                if (!$resql) {
1074
                    $this->error = $this->db->lasterror();
1075
                    $err++;
1076
                }
1077
1078
                $sql = "DELETE FROM " . MAIN_DB_PREFIX . "boxes_def";
1079
                $sql .= " WHERE file = '" . $this->db->escape($file) . "'";
1080
                $sql .= " AND entity = " . $conf->entity;     // Do not use getEntity here, we want to delete only in current company
1081
1082
                dol_syslog(get_class($this) . "::delete_boxes", LOG_DEBUG);
1083
                $resql = $this->db->query($sql);
1084
                if (!$resql) {
1085
                    $this->error = $this->db->lasterror();
1086
                    $err++;
1087
                }
1088
            }
1089
        }
1090
1091
        return $err;
1092
    }
1093
1094
    /**
1095
     * Removes boxes
1096
     *
1097
     * @return int Error count (0 if OK)
1098
     */
1099
    public function delete_cronjobs()
1100
    {
1101
        // phpcs:enable
1102
        global $conf;
1103
1104
        $err = 0;
1105
1106
        if (is_array($this->cronjobs)) {
1107
            $sql = "DELETE FROM " . MAIN_DB_PREFIX . "cronjob";
1108
            $sql .= " WHERE module_name = '" . $this->db->escape(empty($this->rights_class) ? strtolower($this->name) : $this->rights_class) . "'";
1109
            $sql .= " AND entity = " . $conf->entity;
1110
            $sql .= " AND test = '1'"; // We delete on lines that are not set with a complete test that is '$conf->module->enabled' so when module is disabled, the cron is also removed.
1111
            // For crons declared with a '$conf->module->enabled', there is no need to delete the line, so we don't loose setup if we reenable module.
1112
1113
            dol_syslog(get_class($this) . "::delete_cronjobs", LOG_DEBUG);
1114
            $resql = $this->db->query($sql);
1115
            if (!$resql) {
1116
                $this->error = $this->db->lasterror();
1117
                $err++;
1118
            }
1119
        }
1120
1121
        return $err;
1122
    }
1123
1124
    /**
1125
     * Removes access rights
1126
     *
1127
     * @return int                     Error count (0 if OK)
1128
     */
1129
    public function delete_permissions()
1130
    {
1131
        // phpcs:enable
1132
        global $conf;
1133
1134
        $err = 0;
1135
1136
        $module = empty($this->rights_class) ? strtolower($this->name) : $this->rights_class;
1137
1138
        $sql = "DELETE FROM " . MAIN_DB_PREFIX . "rights_def";
1139
        $sql .= " WHERE (module = '" . $this->db->escape($module) . "' OR module_origin = '" . $this->db->escape($module) . "')";
1140
1141
        // Delete all entities if core module
1142
        if (empty($this->core_enabled)) {
1143
            $sql .= " AND entity = " . ((int)$conf->entity);
1144
        }
1145
1146
        dol_syslog(get_class($this) . "::delete_permissions", LOG_DEBUG);
1147
        if (!$this->db->query($sql)) {
1148
            $this->error = $this->db->lasterror();
1149
            $err++;
1150
        }
1151
1152
        return $err;
1153
    }
1154
1155
    /**
1156
     * Removes menu entries
1157
     *
1158
     * @return int Error count (0 if OK)
1159
     */
1160
    public function delete_menus()
1161
    {
1162
        // phpcs:enable
1163
        global $conf;
1164
1165
        $err = 0;
1166
1167
        //$module=strtolower($this->name);        TODO When right_class will be same than module name
1168
        $module = empty($this->rights_class) ? strtolower($this->name) : $this->rights_class;
1169
1170
        $sql = "DELETE FROM " . MAIN_DB_PREFIX . "menu";
1171
        $sql .= " WHERE module = '" . $this->db->escape($module) . "'";
1172
        $sql .= " AND menu_handler = 'all'";    // We delete only lines that were added manually or by the module activation. We keep entry added by menuhandler like 'auguria'
1173
        $sql .= " AND entity IN (0, " . $conf->entity . ")";
1174
1175
        dol_syslog(get_class($this) . "::delete_menus", LOG_DEBUG);
1176
        $resql = $this->db->query($sql);
1177
        if (!$resql) {
1178
            $this->error = $this->db->lasterror();
1179
            $err++;
1180
        }
1181
1182
        return $err;
1183
    }
1184
1185
    /**
1186
     * Removes directories
1187
     *
1188
     * @return int Error count (0 if OK)
1189
     */
1190
    public function delete_dirs()
1191
    {
1192
        // phpcs:enable
1193
        global $conf;
1194
1195
        $err = 0;
1196
1197
        $sql = "DELETE FROM " . MAIN_DB_PREFIX . "const";
1198
        $sql .= " WHERE " . $this->db->decrypt('name') . " LIKE '" . $this->db->escape($this->const_name) . "_DIR_%'";
1199
        $sql .= " AND entity = " . $conf->entity;
1200
1201
        dol_syslog(get_class($this) . "::delete_dirs", LOG_DEBUG);
1202
        if (!$this->db->query($sql)) {
1203
            $this->error = $this->db->lasterror();
1204
            $err++;
1205
        }
1206
1207
        return $err;
1208
    }
1209
1210
    /**
1211
     * Gives the long description of a module. First check README-la_LA.md then README.md
1212
     * If no markdown files found, it returns translated value of the key ->descriptionlong.
1213
     *
1214
     * @return string     Long description of a module from README.md of from property.
1215
     */
1216
    public function getDescLong()
1217
    {
1218
        global $langs;
1219
        $langs->load("admin");
1220
1221
        include_once DOL_DOCUMENT_ROOT . '/core/lib/files.lib.php';
1222
        include_once DOL_DOCUMENT_ROOT . '/core/lib/geturl.lib.php';
1223
1224
        $content = '';
1225
        $pathoffile = $this->getDescLongReadmeFound();
1226
1227
        if ($pathoffile) {     // Mostly for external modules
1228
            $content = file_get_contents($pathoffile, false, null, 0, 1024 * 1024); // Max size loaded 1Mb
1229
1230
            if ((float)DOL_VERSION >= 6.0) {
1231
                @include_once DOL_DOCUMENT_ROOT . '/core/lib/parsemd.lib.php';
1232
1233
                $content = dolMd2Html(
1234
                    $content,
1235
                    'parsedown',
1236
                    array(
1237
                        'doc/' => dol_buildpath(strtolower($this->name) . '/doc/', 1),
1238
                        'img/' => dol_buildpath(strtolower($this->name) . '/img/', 1),
1239
                        'images/' => dol_buildpath(strtolower($this->name) . '/images/', 1),
1240
                    )
1241
                );
1242
1243
                $content = preg_replace('/<a href="/', '<a target="_blank" rel="noopener noreferrer" href="', $content);
1244
            } else {
1245
                $content = nl2br($content);
1246
            }
1247
        } else {
1248
            // Mostly for internal modules
1249
            if (!empty($this->descriptionlong)) {
1250
                if (is_array($this->langfiles)) {
1251
                    foreach ($this->langfiles as $val) {
1252
                        if ($val) {
1253
                            $langs->load($val);
1254
                        }
1255
                    }
1256
                }
1257
1258
                $content = $langs->transnoentitiesnoconv($this->descriptionlong);
1259
            }
1260
        }
1261
1262
        return $content;
1263
    }
1264
1265
    /**
1266
     * Return path of file if a README file was found.
1267
     *
1268
     * @return string      Path of file if a README file was found.
1269
     */
1270
    public function getDescLongReadmeFound()
1271
    {
1272
        global $langs;
1273
1274
        $filefound = false;
1275
1276
        // Define path to file README.md.
1277
        // First check README-la_LA.md then README-la.md then README.md
1278
        $pathoffile = dol_buildpath(strtolower($this->name) . '/README-' . $langs->defaultlang . '.md', 0);
1279
        if (dol_is_file($pathoffile)) {
1280
            $filefound = true;
1281
        }
1282
        if (!$filefound) {
1283
            $tmp = explode('_', $langs->defaultlang);
1284
            $pathoffile = dol_buildpath(strtolower($this->name) . '/README-' . $tmp[0] . '.md', 0);
1285
            if (dol_is_file($pathoffile)) {
1286
                $filefound = true;
1287
            }
1288
        }
1289
        if (!$filefound) {
1290
            $pathoffile = dol_buildpath(strtolower($this->name) . '/README.md', 0);
1291
            if (dol_is_file($pathoffile)) {
1292
                $filefound = true;
1293
            }
1294
        }
1295
1296
        return ($filefound ? $pathoffile : '');
1297
    }
1298
1299
    /**
1300
     * Gives the changelog. First check ChangeLog-la_LA.md then ChangeLog.md
1301
     *
1302
     * @return string  Content of ChangeLog
1303
     */
1304
    public function getChangeLog()
1305
    {
1306
        global $langs;
1307
        $langs->load("admin");
1308
1309
        include_once DOL_DOCUMENT_ROOT . '/core/lib/files.lib.php';
1310
        include_once DOL_DOCUMENT_ROOT . '/core/lib/geturl.lib.php';
1311
1312
        $filefound = false;
1313
1314
        // Define path to file README.md.
1315
        // First check ChangeLog-la_LA.md then ChangeLog.md
1316
        $pathoffile = dol_buildpath(strtolower($this->name) . '/ChangeLog-' . $langs->defaultlang . '.md', 0);
1317
        if (dol_is_file($pathoffile)) {
1318
            $filefound = true;
1319
        }
1320
        if (!$filefound) {
1321
            $pathoffile = dol_buildpath(strtolower($this->name) . '/ChangeLog.md', 0);
1322
            if (dol_is_file($pathoffile)) {
1323
                $filefound = true;
1324
            }
1325
        }
1326
1327
        if ($filefound) {     // Mostly for external modules
1328
            $content = file_get_contents($pathoffile);
1329
1330
            if ((float)DOL_VERSION >= 6.0) {
1331
                @include_once DOL_DOCUMENT_ROOT . '/core/lib/parsemd.lib.php';
1332
1333
                $content = dolMd2Html($content, 'parsedown', array('doc/' => dol_buildpath(strtolower($this->name) . '/doc/', 1)));
1334
            } else {
1335
                $content = nl2br($content);
1336
            }
1337
        } else {
1338
            $content = '';
1339
        }
1340
1341
        return $content;
1342
    }
1343
1344
1345
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.PublicUnderscore
1346
1347
    /**
1348
     * Gives the publisher name
1349
     *
1350
     * @return string  Publisher name
1351
     */
1352
    public function getPublisher()
1353
    {
1354
        return $this->editor_name;
1355
    }
1356
1357
1358
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.PublicUnderscore
1359
1360
    /**
1361
     * Gives the publisher url
1362
     *
1363
     * @return string  Publisher url
1364
     */
1365
    public function getPublisherUrl()
1366
    {
1367
        return $this->editor_url;
1368
    }
1369
1370
1371
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps,PEAR.NamingConventions.ValidFunctionName.PublicUnderscore
1372
1373
    /**
1374
     * Gives the module position
1375
     *
1376
     * @return string   Module position (an external module should never return a value lower than 100000. 1-100000 are reserved for core)
1377
     */
1378
    public function getModulePosition()
1379
    {
1380
        if (in_array($this->version, array('dolibarr', 'experimental', 'development'))) {   // core module
1381
            return $this->module_position;
1382
        } else {                                                                            // external module
1383
            if ($this->module_position >= 100000) {
1384
                return $this->module_position;
1385
            } else {
1386
                $position = intval($this->module_position) + 100000;
1387
                return strval($position);
1388
            }
1389
        }
1390
    }
1391
1392
1393
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1394
1395
    /**
1396
     * Gives module related language files list
1397
     *
1398
     * @return string[]    Language files list
1399
     */
1400
    public function getLangFilesArray()
1401
    {
1402
        return $this->langfiles;
1403
    }
1404
1405
1406
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1407
1408
    /**
1409
     * Gives translated label of an export dataset
1410
     *
1411
     * @param int $r Dataset index
1412
     *
1413
     * @return string       Translated databaset label
1414
     */
1415
    public function getExportDatasetLabel($r)
1416
    {
1417
        global $langs;
1418
1419
        $langstring = "ExportDataset_" . $this->export_code[$r];
1420
        if ($langs->trans($langstring) == $langstring) {
1421
            // Translation not found
1422
            return $langs->trans($this->export_label[$r]);
1423
        } else {
1424
            // Translation found
1425
            return $langs->trans($langstring);
1426
        }
1427
    }
1428
1429
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1430
1431
    /**
1432
     * Gives translated label of an import dataset
1433
     *
1434
     * @param int $r Dataset index
1435
     *
1436
     * @return string      Translated dataset label
1437
     */
1438
    public function getImportDatasetLabel($r)
1439
    {
1440
        global $langs;
1441
1442
        $langstring = "ImportDataset_" . $this->import_code[$r];
1443
        //print "x".$langstring;
1444
        if ($langs->trans($langstring) == $langstring) {
1445
            // Translation not found
1446
            return $langs->transnoentitiesnoconv($this->import_label[$r]);
1447
        } else {
1448
            // Translation found
1449
            return $langs->transnoentitiesnoconv($langstring);
1450
        }
1451
    }
1452
1453
1454
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1455
1456
    /**
1457
     * Gives the last date of activation
1458
     *
1459
     * @return  int|string          Date of last activation or '' if module was never activated
1460
     */
1461
    public function getLastActivationDate()
1462
    {
1463
        global $conf;
1464
1465
        $err = 0;
1466
1467
        $sql = "SELECT tms FROM " . MAIN_DB_PREFIX . "const";
1468
        $sql .= " WHERE " . $this->db->decrypt('name') . " = '" . $this->db->escape($this->const_name) . "'";
1469
        $sql .= " AND entity IN (0, " . ((int)$conf->entity) . ")";
1470
1471
        dol_syslog(get_class($this) . "::getLastActiveDate", LOG_DEBUG);
1472
        $resql = $this->db->query($sql);
1473
        if (!$resql) {
1474
            $err++;
1475
        } else {
1476
            $obj = $this->db->fetch_object($resql);
1477
            if ($obj) {
1478
                return $this->db->jdate($obj->tms);
1479
            }
1480
        }
1481
1482
        return '';
1483
    }
1484
1485
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1486
1487
    /**
1488
     * Gives the last author of activation
1489
     *
1490
     * @return array       Array array('authorid'=>Id of last activation user, 'lastactivationdate'=>Date of last activation)
1491
     */
1492
    public function getLastActivationInfo()
1493
    {
1494
        global $conf;
1495
1496
        $err = 0;
1497
1498
        $sql = "SELECT tms, note FROM " . MAIN_DB_PREFIX . "const";
1499
        $sql .= " WHERE " . $this->db->decrypt('name') . " = '" . $this->db->escape($this->const_name) . "'";
1500
        $sql .= " AND entity IN (0, " . $conf->entity . ")";
1501
1502
        dol_syslog(get_class($this) . "::getLastActiveDate", LOG_DEBUG);
1503
        $resql = $this->db->query($sql);
1504
        if (!$resql) {
1505
            $err++;
1506
        } else {
1507
            $obj = $this->db->fetch_object($resql);
1508
            if ($obj) {
1509
                $tmp = array();
1510
                if ($obj->note) {
1511
                    $tmp = json_decode($obj->note, true);
1512
                }
1513
                return array(
1514
                    'authorid' => empty($tmp['authorid']) ? '' : $tmp['authorid'],
1515
                    'ip' => empty($tmp['ip']) ? '' : $tmp['ip'],
1516
                    'lastactivationdate' => $this->db->jdate($obj->tms),
1517
                    'lastactivationversion' => (!empty($tmp['lastactivationversion']) ? $tmp['lastactivationversion'] : 'unknown'),
1518
                );
1519
            }
1520
        }
1521
1522
        return array();
1523
    }
1524
1525
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1526
1527
    /**
1528
     * Function called when module is enabled.
1529
     * The init function adds tabs, constants, boxes, permissions and menus (defined in constructor) into Dolibarr database.
1530
     * It also creates data directories
1531
     *
1532
     * @param string $options Options when enabling module ('', 'newboxdefonly', 'noboxes', 'menuonly')
1533
     *                         'noboxes' = Do not insert boxes 'newboxdefonly' = For boxes, insert def of boxes only and not boxes activation
1534
     * @return int                1 if OK, 0 if KO
1535
     */
1536
    public function init($options = '')
1537
    {
1538
        return $this->_init(array(), $options);
1539
    }
1540
1541
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1542
1543
    /**
1544
     * Enables a module.
1545
     * Inserts all information into database.
1546
     *
1547
     * @param array $array_sql SQL requests to be executed when enabling module
1548
     * @param string $options String with options when disabling module:
1549
     *                              - 'noboxes' = Do all actions but do not insert boxes
1550
     *                              - 'newboxdefonly' = Do all actions but for boxes, insert def of boxes only and not boxes activation
1551
     * @return int                  1 if OK, 0 if KO
1552
     */
1553
    protected function _init($array_sql, $options = '')
1554
    {
1555
        // phpcs:enable
1556
        global $conf;
1557
        $err = 0;
1558
1559
        $this->db->begin();
1560
1561
        // Insert activation module constant
1562
        if (!$err) {
1563
            $err += $this->_active();
1564
        }
1565
1566
        // Insert new pages for tabs (into llx_const)
1567
        if (!$err) {
1568
            $err += $this->insert_tabs();
1569
        }
1570
1571
        // Insert activation of module's parts. Copy website templates into doctemplates.
1572
        if (!$err) {
1573
            $err += $this->insert_module_parts();
1574
        }
1575
1576
        // Insert constant defined by modules (into llx_const)
1577
        if (!$err && !preg_match('/newboxdefonly/', $options)) {
1578
            $err += $this->insert_const(); // Test on newboxdefonly to avoid to erase value during upgrade
1579
        }
1580
1581
        // Insert boxes def (into llx_boxes_def) and boxes setup (into llx_boxes)
1582
        if (!$err && !preg_match('/noboxes/', $options)) {
1583
            $err += $this->insert_boxes($options);
1584
        }
1585
1586
        // Insert cron job entries (entry in llx_cronjobs)
1587
        if (!$err) {
1588
            $err += $this->insert_cronjobs();
1589
        }
1590
1591
        // Insert permission definitions of module into llx_rights_def. If user is admin, grant this permission to user.
1592
        if (!$err) {
1593
            $err += $this->insert_permissions(1, null, 1);
1594
        }
1595
1596
        // Insert specific menus entries into database
1597
        if (!$err) {
1598
            $err += $this->insert_menus();
1599
        }
1600
1601
        // Create module's directories
1602
        if (!$err) {
1603
            $err += $this->create_dirs();
1604
        }
1605
1606
        // Execute addons requests
1607
        $num = count($array_sql);
1608
        for ($i = 0; $i < $num; $i++) {
1609
            if (!$err) {
1610
                $val = $array_sql[$i];
1611
                $sql = $val;
1612
                $ignoreerror = 0;
1613
                if (is_array($val)) {
1614
                    $sql = $val['sql'];
1615
                    $ignoreerror = $val['ignoreerror'];
1616
                }
1617
                // Add current entity id
1618
                $sql = str_replace('__ENTITY__', (string)$conf->entity, $sql);
1619
1620
                dol_syslog(get_class($this) . "::_init ignoreerror=" . $ignoreerror, LOG_DEBUG);
1621
                $result = $this->db->query($sql, $ignoreerror);
1622
                if (!$result) {
1623
                    if (!$ignoreerror) {
1624
                        $this->error = $this->db->lasterror();
1625
                        $err++;
1626
                    } else {
1627
                        dol_syslog(get_class($this) . "::_init Warning " . $this->db->lasterror(), LOG_WARNING);
1628
                    }
1629
                }
1630
            }
1631
        }
1632
1633
        // Return code
1634
        if (!$err) {
1635
            $this->db->commit();
1636
1637
            $moduleNameInConf = strtolower(preg_replace('/^MAIN_MODULE_/', '', $this->const_name));
1638
            // two exceptions to handle
1639
            if ($moduleNameInConf === 'propale') {
1640
                $moduleNameInConf = 'propal';
1641
            } elseif ($moduleNameInConf === 'supplierproposal') {
1642
                $moduleNameInConf = 'supplier_proposal';
1643
            }
1644
1645
            $conf->modules[$moduleNameInConf] = $moduleNameInConf; // Add this module in list of enabled modules so isModEnabled() will work (conf->module->enabled must no more be used)
1646
1647
            return 1;
1648
        } else {
1649
            $this->db->rollback();
1650
            return 0;
1651
        }
1652
    }
1653
1654
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1655
1656
    /**
1657
     * Insert constants for module activation
1658
     *
1659
     * @return int Error count (0 if OK)
1660
     */
1661
    protected function _active()
1662
    {
1663
        // phpcs:enable
1664
        global $conf, $user;
1665
1666
        $err = 0;
1667
1668
        // Common module
1669
        $entity = ((!empty($this->always_enabled) || !empty($this->core_enabled)) ? 0 : $conf->entity);
1670
1671
        $sql = "DELETE FROM " . MAIN_DB_PREFIX . "const";
1672
        $sql .= " WHERE " . $this->db->decrypt('name') . " = '" . $this->db->escape($this->const_name) . "'";
1673
        $sql .= " AND entity IN (0, " . $entity . ")";
1674
1675
        dol_syslog(get_class($this) . "::_active delete activation constant", LOG_DEBUG);
1676
        $resql = $this->db->query($sql);
1677
        if (!$resql) {
1678
            $err++;
1679
        }
1680
1681
        $note = json_encode(
1682
            array(
1683
                'authorid' => (is_object($user) ? $user->id : 0),
1684
                'ip' => (empty($_SERVER['REMOTE_ADDR']) ? '' : $_SERVER['REMOTE_ADDR']),
1685
                'lastactivationversion' => $this->version,
1686
            )
1687
        );
1688
1689
        $sql = "INSERT INTO " . MAIN_DB_PREFIX . "const (name, value, visible, entity, note) VALUES";
1690
        $sql .= " (" . $this->db->encrypt($this->const_name);
1691
        $sql .= ", " . $this->db->encrypt('1');
1692
        $sql .= ", 0, " . ((int)$entity);
1693
        $sql .= ", '" . $this->db->escape($note) . "')";
1694
1695
        dol_syslog(get_class($this) . "::_active insert activation constant", LOG_DEBUG);
1696
        $resql = $this->db->query($sql);
1697
        if (!$resql) {
1698
            $err++;
1699
        }
1700
1701
        return $err;
1702
    }
1703
1704
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1705
1706
    /**
1707
     * Adds tabs
1708
     *
1709
     * @return int  Error count (0 if ok)
1710
     */
1711
    public function insert_tabs()
1712
    {
1713
        // phpcs:enable
1714
        global $conf;
1715
1716
        $err = 0;
1717
1718
        if (!empty($this->tabs)) {
1719
            dol_syslog(get_class($this) . "::insert_tabs", LOG_DEBUG);
1720
1721
            $i = 0;
1722
            foreach ($this->tabs as $key => $value) {
1723
                if (is_array($value) && count($value) == 0) {
1724
                    continue; // Discard empty arrays
1725
                }
1726
1727
                $entity = $conf->entity;
1728
                $newvalue = $value;
1729
1730
                if (is_array($value)) {
1731
                    $newvalue = $value['data'];
1732
                    if (isset($value['entity'])) {
1733
                        $entity = $value['entity'];
1734
                    }
1735
                }
1736
1737
                if ($newvalue) {
1738
                    $sql = "INSERT INTO " . MAIN_DB_PREFIX . "const (";
1739
                    $sql .= "name";
1740
                    $sql .= ", type";
1741
                    $sql .= ", value";
1742
                    $sql .= ", note";
1743
                    $sql .= ", visible";
1744
                    $sql .= ", entity";
1745
                    $sql .= ")";
1746
                    $sql .= " VALUES (";
1747
                    $sql .= $this->db->encrypt($this->const_name . "_TABS_" . $i);
1748
                    $sql .= ", 'chaine'";
1749
                    $sql .= ", " . $this->db->encrypt($newvalue);
1750
                    $sql .= ", null";
1751
                    $sql .= ", '0'";
1752
                    $sql .= ", " . ((int)$entity);
1753
                    $sql .= ")";
1754
1755
                    $resql = $this->db->query($sql);
1756
                    if (!$resql) {
1757
                        dol_syslog($this->db->lasterror(), LOG_ERR);
1758
                        if ($this->db->lasterrno() != 'DB_ERROR_RECORD_ALREADY_EXISTS') {
1759
                            $this->error = $this->db->lasterror();
1760
                            $this->errors[] = $this->db->lasterror();
1761
                            $err++;
1762
                            break;
1763
                        }
1764
                    }
1765
                }
1766
                $i++;
1767
            }
1768
        }
1769
        return $err;
1770
    }
1771
1772
1773
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1774
1775
    /**
1776
     * Save configuration for generic features.
1777
     * This also generate website templates if the module provide some.
1778
     *
1779
     * @return int Error count (0 if OK)
1780
     */
1781
    public function insert_module_parts()
1782
    {
1783
        // phpcs:enable
1784
        global $conf, $langs;
1785
1786
        $error = 0;
1787
1788
        if (is_array($this->module_parts)) {
1789
            if (empty($this->module_parts['icon']) && !empty($this->picto) && preg_match('/^fa\-/', $this->picto)) {
1790
                $this->module_parts['icon'] = $this->picto;
1791
            }
1792
1793
            foreach ($this->module_parts as $key => $value) {
1794
                if (is_array($value) && count($value) == 0) {
1795
                    continue; // Discard empty arrays
1796
                }
1797
1798
                // If module brings website templates, we must generate the zip like we do whenenabling the website module
1799
                if ($key == 'websitetemplates' && $value == 1) {
1800
                    $srcroot = dol_buildpath('/' . strtolower($this->name) . '/doctemplates/websites');
1801
1802
                    // Copy templates in dir format (recommended) into zip file
1803
                    $docs = dol_dir_list($srcroot, 'directories', 0, 'website_.*$');
1804
                    foreach ($docs as $cursorfile) {
1805
                        $src = $srcroot . '/' . $cursorfile['name'];
1806
                        $dest = DOL_DATA_ROOT . '/doctemplates/websites/' . $cursorfile['name'];
1807
1808
                        dol_delete_file($dest . '.zip');
1809
1810
                        // Compress it
1811
                        global $errormsg;
1812
                        $errormsg = '';
1813
                        $result = dol_compress_dir($src, $dest . '.zip', 'zip');
1814
                        if ($result < 0) {
1815
                            $error++;
1816
                            $this->error = ($errormsg ? $errormsg : $langs->trans('ErrorFailToCreateZip', $dest));
1817
                            $this->errors[] = ($errormsg ? $errormsg : $langs->trans('ErrorFailToCreateZip', $dest));
1818
                        }
1819
                    }
1820
1821
                    // Copy also the preview website_xxx.jpg file
1822
                    $docs = dol_dir_list($srcroot, 'files', 0, 'website_.*\.jpg$');
1823
                    foreach ($docs as $cursorfile) {
1824
                        $src = $srcroot . '/' . $cursorfile['name'];
1825
                        $dest = DOL_DATA_ROOT . '/doctemplates/websites/' . $cursorfile['name'];
1826
1827
                        dol_copy($src, $dest);
1828
                    }
1829
                }
1830
1831
                $entity = $conf->entity; // Reset the current entity
1832
                $newvalue = $value;
1833
1834
                // Serialize array parameters
1835
                if (is_array($value)) {
1836
                    // Can defined other parameters
1837
                    // Example when $key='hooks', then $value is an array('data'=>array('hookcontext1','hookcontext2'), 'entity'=>X)
1838
                    if (isset($value['data']) && is_array($value['data'])) {
1839
                        $newvalue = json_encode($value['data']);
1840
                        if (isset($value['entity'])) {
1841
                            $entity = $value['entity'];
1842
                        }
1843
                    } elseif (isset($value['data']) && !is_array($value['data'])) {
1844
                        $newvalue = $value['data'];
1845
                        if (isset($value['entity'])) {
1846
                            $entity = $value['entity'];
1847
                        }
1848
                    } else { // when hook is declared with syntax 'hook'=>array('hookcontext1','hookcontext2',...)
1849
                        $newvalue = json_encode($value);
1850
                    }
1851
                }
1852
1853
                if (!empty($newvalue)) {
1854
                    $sql = "INSERT INTO " . MAIN_DB_PREFIX . "const (";
1855
                    $sql .= "name";
1856
                    $sql .= ", type";
1857
                    $sql .= ", value";
1858
                    $sql .= ", note";
1859
                    $sql .= ", visible";
1860
                    $sql .= ", entity";
1861
                    $sql .= ")";
1862
                    $sql .= " VALUES (";
1863
                    $sql .= " " . $this->db->encrypt($this->const_name . "_" . strtoupper($key), 1);
1864
                    $sql .= ", 'chaine'";
1865
                    $sql .= ", " . $this->db->encrypt($newvalue, 1);
1866
                    $sql .= ", null";
1867
                    $sql .= ", '0'";
1868
                    $sql .= ", " . ((int)$entity);
1869
                    $sql .= ")";
1870
1871
                    dol_syslog(get_class($this) . "::insert_module_parts for key=" . $this->const_name . "_" . strtoupper($key), LOG_DEBUG);
1872
1873
                    $resql = $this->db->query($sql, 1);
1874
                    if (!$resql) {
1875
                        if ($this->db->lasterrno() != 'DB_ERROR_RECORD_ALREADY_EXISTS') {
1876
                            $error++;
1877
                            $this->error = $this->db->lasterror();
1878
                        } else {
1879
                            dol_syslog(get_class($this) . "::insert_module_parts for " . $this->const_name . "_" . strtoupper($key) . " Record already exists.", LOG_WARNING);
1880
                        }
1881
                    }
1882
                }
1883
            }
1884
        }
1885
        return $error;
1886
    }
1887
1888
1889
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1890
1891
    /**
1892
     * Adds constants
1893
     *
1894
     * @return int Error count (0 if OK)
1895
     */
1896
    public function insert_const()
1897
    {
1898
        // phpcs:enable
1899
        global $conf;
1900
1901
        $err = 0;
1902
1903
        if (empty($this->const)) {
1904
            return 0;
1905
        }
1906
1907
        dol_syslog(__METHOD__, LOG_DEBUG);
1908
1909
        foreach ($this->const as $key => $value) {
1910
            $name = $this->const[$key][0];
1911
            $type = $this->const[$key][1];
1912
            $val = $this->const[$key][2];
1913
            $note = isset($this->const[$key][3]) ? $this->const[$key][3] : '';
1914
            $visible = isset($this->const[$key][4]) ? $this->const[$key][4] : 0;
1915
            $entity = (!empty($this->const[$key][5]) && $this->const[$key][5] != 'current') ? 0 : $conf->entity;
1916
1917
            // Clean
1918
            if (empty($visible)) {
1919
                $visible = '0';
1920
            }
1921
            if (empty($val) && $val != '0') {
1922
                $val = '';
1923
            }
1924
1925
            $sql = "SELECT count(*) as nb";
1926
            $sql .= " FROM " . MAIN_DB_PREFIX . "const";
1927
            $sql .= " WHERE " . $this->db->decrypt('name') . " = '" . $this->db->escape($name) . "'";
1928
            $sql .= " AND entity = " . ((int)$entity);
1929
1930
            $result = $this->db->query($sql);
1931
            if ($result) {
1932
                $row = $this->db->fetch_row($result);
1933
1934
                if ($row[0] == 0) {   // If not found
1935
                    $sql = "INSERT INTO " . MAIN_DB_PREFIX . "const (name,type,value,note,visible,entity)";
1936
                    $sql .= " VALUES (";
1937
                    $sql .= $this->db->encrypt($name);
1938
                    $sql .= ",'" . $this->db->escape($type) . "'";
1939
                    $sql .= "," . (($val != '') ? $this->db->encrypt($val) : "''");
1940
                    $sql .= "," . ($note ? "'" . $this->db->escape($note) . "'" : "null");
1941
                    $sql .= ",'" . $this->db->escape($visible) . "'";
1942
                    $sql .= "," . $entity;
1943
                    $sql .= ")";
1944
1945
                    if (!$this->db->query($sql)) {
1946
                        $err++;
1947
                    }
1948
                } else {
1949
                    dol_syslog(__METHOD__ . " constant '" . $name . "' already exists", LOG_DEBUG);
1950
                }
1951
            } else {
1952
                $err++;
1953
            }
1954
        }
1955
1956
        return $err;
1957
    }
1958
1959
1960
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1961
1962
    /**
1963
     * Adds boxes
1964
     *
1965
     * @param string $option Options when disabling module ('newboxdefonly'=insert only boxes definition)
1966
     *
1967
     * @return int             Error count (0 if OK)
1968
     */
1969
    public function insert_boxes($option = '')
1970
    {
1971
        // phpcs:enable
1972
1973
        global $conf;
1974
1975
        $err = 0;
1976
1977
        if (is_array($this->boxes)) {
1978
            dol_syslog(get_class($this) . "::insert_boxes", LOG_DEBUG);
1979
1980
            $pos_name = InfoBox::getListOfPagesForBoxes();
1981
1982
            foreach ($this->boxes as $key => $value) {
1983
                $file = isset($this->boxes[$key]['file']) ? $this->boxes[$key]['file'] : '';
1984
                $note = isset($this->boxes[$key]['note']) ? $this->boxes[$key]['note'] : '';
1985
                $enabledbydefaulton = isset($this->boxes[$key]['enabledbydefaulton']) ? $this->boxes[$key]['enabledbydefaulton'] : 'Home';
1986
1987
                if (empty($file)) {
1988
                    $file = isset($this->boxes[$key][1]) ? $this->boxes[$key][1] : ''; // For backward compatibility
1989
                }
1990
                if (empty($note)) {
1991
                    $note = isset($this->boxes[$key][2]) ? $this->boxes[$key][2] : ''; // For backward compatibility
1992
                }
1993
1994
                // Search if boxes def already present
1995
                $sql = "SELECT count(*) as nb FROM " . MAIN_DB_PREFIX . "boxes_def";
1996
                $sql .= " WHERE file = '" . $this->db->escape($file) . "'";
1997
                $sql .= " AND entity = " . $conf->entity;
1998
                if ($note) {
1999
                    $sql .= " AND note ='" . $this->db->escape($note) . "'";
2000
                }
2001
2002
                $result = $this->db->query($sql);
2003
                if ($result) {
2004
                    $obj = $this->db->fetch_object($result);
2005
                    if ($obj->nb == 0) {
2006
                        $this->db->begin();
2007
2008
                        if (!$err) {
2009
                            $sql = "INSERT INTO " . MAIN_DB_PREFIX . "boxes_def (file, entity, note)";
2010
                            $sql .= " VALUES ('" . $this->db->escape($file) . "', ";
2011
                            $sql .= $conf->entity . ", ";
2012
                            $sql .= $note ? "'" . $this->db->escape($note) . "'" : "null";
2013
                            $sql .= ")";
2014
2015
                            dol_syslog(get_class($this) . "::insert_boxes", LOG_DEBUG);
2016
                            $resql = $this->db->query($sql);
2017
                            if (!$resql) {
2018
                                $err++;
2019
                            }
2020
                        }
2021
                        if (!$err && !preg_match('/newboxdefonly/', $option)) {
2022
                            $lastid = $this->db->last_insert_id(MAIN_DB_PREFIX . "boxes_def", "rowid");
2023
2024
                            foreach ($pos_name as $key2 => $val2) {
2025
                                //print 'key2='.$key2.'-val2='.$val2."<br>\n";
2026
                                if ($enabledbydefaulton && $val2 != $enabledbydefaulton) {
2027
                                    continue; // Not enabled by default onto this page.
2028
                                }
2029
2030
                                $sql = "INSERT INTO " . MAIN_DB_PREFIX . "boxes (box_id, position, box_order, fk_user, entity)";
2031
                                $sql .= " VALUES (" . ((int)$lastid) . ", " . ((int)$key2) . ", '0', 0, " . ((int)$conf->entity) . ")";
2032
2033
                                dol_syslog(get_class($this) . "::insert_boxes onto page " . $key2 . "=" . $val2, LOG_DEBUG);
2034
                                $resql = $this->db->query($sql);
2035
                                if (!$resql) {
2036
                                    $err++;
2037
                                }
2038
                            }
2039
                        }
2040
2041
                        if (!$err) {
2042
                            $this->db->commit();
2043
                        } else {
2044
                            $this->error = $this->db->lasterror();
2045
                            $this->db->rollback();
2046
                        }
2047
                    }
2048
                    // else box already registered into database
2049
                } else {
2050
                    $this->error = $this->db->lasterror();
2051
                    $err++;
2052
                }
2053
            }
2054
        }
2055
2056
        return $err;
2057
    }
2058
2059
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2060
2061
    /**
2062
     * Adds cronjobs
2063
     *
2064
     * @return int             Error count (0 if OK)
2065
     */
2066
    public function insert_cronjobs()
2067
    {
2068
        // phpcs:enable
2069
2070
        global $conf;
2071
2072
        $err = 0;
2073
2074
        if (is_array($this->cronjobs)) {
2075
            dol_syslog(get_class($this) . "::insert_cronjobs", LOG_DEBUG);
2076
2077
            foreach ($this->cronjobs as $key => $value) {
2078
                $entity = isset($this->cronjobs[$key]['entity']) ? $this->cronjobs[$key]['entity'] : $conf->entity;
2079
                $label = isset($this->cronjobs[$key]['label']) ? $this->cronjobs[$key]['label'] : '';
2080
                $jobtype = isset($this->cronjobs[$key]['jobtype']) ? $this->cronjobs[$key]['jobtype'] : '';
2081
                $class = isset($this->cronjobs[$key]['class']) ? $this->cronjobs[$key]['class'] : '';
2082
                $objectname = isset($this->cronjobs[$key]['objectname']) ? $this->cronjobs[$key]['objectname'] : '';
2083
                $method = isset($this->cronjobs[$key]['method']) ? $this->cronjobs[$key]['method'] : '';
2084
                $command = isset($this->cronjobs[$key]['command']) ? $this->cronjobs[$key]['command'] : '';
2085
                $parameters = isset($this->cronjobs[$key]['parameters']) ? $this->cronjobs[$key]['parameters'] : '';
2086
                $comment = isset($this->cronjobs[$key]['comment']) ? $this->cronjobs[$key]['comment'] : '';
2087
                $frequency = isset($this->cronjobs[$key]['frequency']) ? $this->cronjobs[$key]['frequency'] : '';
2088
                $unitfrequency = isset($this->cronjobs[$key]['unitfrequency']) ? $this->cronjobs[$key]['unitfrequency'] : '';
2089
                $priority = isset($this->cronjobs[$key]['priority']) ? $this->cronjobs[$key]['priority'] : '';
2090
                $datestart = isset($this->cronjobs[$key]['datestart']) ? $this->cronjobs[$key]['datestart'] : '';
2091
                $dateend = isset($this->cronjobs[$key]['dateend']) ? $this->cronjobs[$key]['dateend'] : '';
2092
                $status = isset($this->cronjobs[$key]['status']) ? $this->cronjobs[$key]['status'] : '';
2093
                $test = isset($this->cronjobs[$key]['test']) ? $this->cronjobs[$key]['test'] : ''; // Line must be enabled or not (so visible or not)
2094
2095
                // Search if cron entry already present
2096
                $sql = "SELECT count(*) as nb FROM " . MAIN_DB_PREFIX . "cronjob";
2097
                //$sql .= " WHERE module_name = '".$this->db->escape(empty($this->rights_class) ?strtolower($this->name) : $this->rights_class)."'";
2098
                $sql .= " WHERE label = '" . $this->db->escape($label) . "'";
2099
                /*if ($class) {
2100
                    $sql .= " AND classesname = '".$this->db->escape($class)."'";
2101
                }
2102
                if ($objectname) {
2103
                    $sql .= " AND objectname = '".$this->db->escape($objectname)."'";
2104
                }
2105
                if ($method) {
2106
                    $sql .= " AND methodename = '".$this->db->escape($method)."'";
2107
                }
2108
                if ($command) {
2109
                    $sql .= " AND command = '".$this->db->escape($command)."'";
2110
                }
2111
                if ($parameters) {
2112
                    $sql .= " AND params = '".$this->db->escape($parameters)."'";
2113
                }*/
2114
                $sql .= " AND entity = " . ((int)$entity); // Must be exact entity
2115
2116
                $now = dol_now();
2117
2118
                $result = $this->db->query($sql);
2119
                if ($result) {
2120
                    $obj = $this->db->fetch_object($result);
2121
                    if ($obj->nb == 0) {
2122
                        $this->db->begin();
2123
2124
                        if (!$err) {
2125
                            $sql = "INSERT INTO " . MAIN_DB_PREFIX . "cronjob (module_name, datec, datestart, dateend, label, jobtype, classesname, objectname, methodename, command, params, note,";
2126
                            if (is_int($frequency)) {
2127
                                $sql .= ' frequency,';
2128
                            }
2129
                            if (is_int($unitfrequency)) {
2130
                                $sql .= ' unitfrequency,';
2131
                            }
2132
                            if (is_int($priority)) {
2133
                                $sql .= ' priority,';
2134
                            }
2135
                            if (is_int($status)) {
2136
                                $sql .= ' status,';
2137
                            }
2138
                            $sql .= " entity, test)";
2139
                            $sql .= " VALUES (";
2140
                            $sql .= "'" . $this->db->escape(empty($this->rights_class) ? strtolower($this->name) : $this->rights_class) . "', ";
2141
                            $sql .= "'" . $this->db->idate($now) . "', ";
2142
                            $sql .= ($datestart ? "'" . $this->db->idate($datestart) . "'" : "'" . $this->db->idate($now) . "'") . ", ";
2143
                            $sql .= ($dateend ? "'" . $this->db->idate($dateend) . "'" : "NULL") . ", ";
2144
                            $sql .= "'" . $this->db->escape($label) . "', ";
2145
                            $sql .= "'" . $this->db->escape($jobtype) . "', ";
2146
                            $sql .= ($class ? "'" . $this->db->escape($class) . "'" : "null") . ",";
2147
                            $sql .= ($objectname ? "'" . $this->db->escape($objectname) . "'" : "null") . ",";
2148
                            $sql .= ($method ? "'" . $this->db->escape($method) . "'" : "null") . ",";
2149
                            $sql .= ($command ? "'" . $this->db->escape($command) . "'" : "null") . ",";
2150
                            $sql .= ($parameters ? "'" . $this->db->escape($parameters) . "'" : "null") . ",";
2151
                            $sql .= ($comment ? "'" . $this->db->escape($comment) . "'" : "null") . ",";
2152
                            if (is_int($frequency)) {
2153
                                $sql .= "'" . $this->db->escape($frequency) . "', ";
2154
                            }
2155
                            if (is_int($unitfrequency)) {
2156
                                $sql .= "'" . $this->db->escape($unitfrequency) . "', ";
2157
                            }
2158
                            if (is_int($priority)) {
2159
                                $sql .= "'" . $this->db->escape($priority) . "', ";
2160
                            }
2161
                            if (is_int($status)) {
2162
                                $sql .= ((int)$status) . ", ";
2163
                            }
2164
                            $sql .= $entity . ",";
2165
                            $sql .= "'" . $this->db->escape($test) . "'";
2166
                            $sql .= ")";
2167
2168
                            $resql = $this->db->query($sql);
2169
                            if (!$resql) {
2170
                                $err++;
2171
                            }
2172
                        }
2173
2174
                        if (!$err) {
2175
                            $this->db->commit();
2176
                        } else {
2177
                            $this->error = $this->db->lasterror();
2178
                            $this->db->rollback();
2179
                        }
2180
                    }
2181
                    // else box already registered into database
2182
                } else {
2183
                    $this->error = $this->db->lasterror();
2184
                    $err++;
2185
                }
2186
            }
2187
        }
2188
2189
        return $err;
2190
    }
2191
2192
2193
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2194
2195
    /**
2196
     * Adds menu entries
2197
     *
2198
     * @return int     Error count (0 if OK)
2199
     */
2200
    public function insert_menus()
2201
    {
2202
        // phpcs:enable
2203
        global $conf, $user;
2204
2205
        if (!is_array($this->menu) || empty($this->menu)) {
2206
            return 0;
2207
        }
2208
2209
        include_once DOL_DOCUMENT_ROOT . '/core/class/menubase.class.php';
2210
2211
        dol_syslog(get_class($this) . "::insert_menus", LOG_DEBUG);
2212
2213
        $err = 0;
2214
2215
        // Common module
2216
        $entity = ((!empty($this->always_enabled) || !empty($this->core_enabled)) ? 0 : $conf->entity);
2217
2218
        $this->db->begin();
2219
2220
        foreach ($this->menu as $key => $value) {
2221
            $menu = new Menubase($this->db);
2222
            $menu->menu_handler = 'all';
2223
2224
            //$menu->module=strtolower($this->name);    TODO When right_class will be same than module name
2225
            $menu->module = (empty($this->rights_class) ? strtolower($this->name) : $this->rights_class);
2226
2227
            if (!$this->menu[$key]['fk_menu']) {
2228
                $menu->fk_menu = 0;
2229
            } else {
2230
                $foundparent = 0;
2231
                $fk_parent = $this->menu[$key]['fk_menu'];
2232
                $reg = array();
2233
                if (preg_match('/^r=/', $fk_parent)) {    // old deprecated method
2234
                    $fk_parent = str_replace('r=', '', $fk_parent);
2235
                    if (isset($this->menu[$fk_parent]['rowid'])) {
2236
                        $menu->fk_menu = $this->menu[$fk_parent]['rowid'];
2237
                        $foundparent = 1;
2238
                    }
2239
                } elseif (preg_match('/^fk_mainmenu=([a-zA-Z0-9_]+),fk_leftmenu=([a-zA-Z0-9_]+)$/', $fk_parent, $reg)) {
2240
                    $menu->fk_menu = -1;
2241
                    $menu->fk_mainmenu = $reg[1];
2242
                    $menu->fk_leftmenu = $reg[2];
2243
                    $foundparent = 1;
2244
                } elseif (preg_match('/^fk_mainmenu=([a-zA-Z0-9_]+)$/', $fk_parent, $reg)) {
2245
                    $menu->fk_menu = -1;
2246
                    $menu->fk_mainmenu = $reg[1];
2247
                    $menu->fk_leftmenu = '';
2248
                    $foundparent = 1;
2249
                }
2250
                if (!$foundparent) {
2251
                    $this->error = "ErrorBadDefinitionOfMenuArrayInModuleDescriptor";
2252
                    dol_syslog(get_class($this) . "::insert_menus " . $this->error . " " . $this->menu[$key]['fk_menu'], LOG_ERR);
2253
                    $err++;
2254
                }
2255
            }
2256
            $menu->type = $this->menu[$key]['type'];
2257
            $menu->mainmenu = isset($this->menu[$key]['mainmenu']) ? $this->menu[$key]['mainmenu'] : (isset($menu->fk_mainmenu) ? $menu->fk_mainmenu : '');
2258
            $menu->leftmenu = isset($this->menu[$key]['leftmenu']) ? $this->menu[$key]['leftmenu'] : '';
2259
            $menu->title = $this->menu[$key]['titre'];
2260
            $menu->prefix = isset($this->menu[$key]['prefix']) ? $this->menu[$key]['prefix'] : '';
2261
            $menu->url = $this->menu[$key]['url'];
2262
            $menu->langs = isset($this->menu[$key]['langs']) ? $this->menu[$key]['langs'] : '';
2263
            $menu->position = $this->menu[$key]['position'];
2264
            $menu->perms = $this->menu[$key]['perms'];
2265
            $menu->target = isset($this->menu[$key]['target']) ? $this->menu[$key]['target'] : '';
2266
            $menu->user = $this->menu[$key]['user'];
2267
            $menu->enabled = isset($this->menu[$key]['enabled']) ? $this->menu[$key]['enabled'] : 0;
2268
            $menu->position = $this->menu[$key]['position'];
2269
            $menu->entity = $entity;
2270
2271
            if (!$err) {
2272
                $result = $menu->create($user); // Save menu entry into table llx_menu
2273
                if ($result > 0) {
2274
                    $this->menu[$key]['rowid'] = $result;
2275
                } else {
2276
                    $this->error = $menu->error;
2277
                    dol_syslog(get_class($this) . '::insert_menus result=' . $result . " " . $this->error, LOG_ERR);
2278
                    $err++;
2279
                    break;
2280
                }
2281
            }
2282
        }
2283
2284
        if (!$err) {
2285
            $this->db->commit();
2286
        } else {
2287
            dol_syslog(get_class($this) . "::insert_menus " . $this->error, LOG_ERR);
2288
            $this->db->rollback();
2289
        }
2290
2291
        return $err;
2292
    }
2293
2294
2295
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2296
2297
    /**
2298
     * Creates directories
2299
     *
2300
     * @return int Error count (0 if OK)
2301
     */
2302
    public function create_dirs()
2303
    {
2304
        // phpcs:enable
2305
        global $langs, $conf;
2306
2307
        $err = 0;
2308
        $name = '';
2309
2310
        if (isset($this->dirs) && is_array($this->dirs)) {
2311
            foreach ($this->dirs as $key => $value) {
2312
                $addtodatabase = 0;
2313
2314
                if (!is_array($value)) {
2315
                    $dir = $value; // Default simple mode
2316
                } else {
2317
                    $constname = $this->const_name . "_DIR_";
2318
                    $dir = $this->dirs[$key][1];
2319
                    $addtodatabase = empty($this->dirs[$key][2]) ? '' : $this->dirs[$key][2]; // Create constante in llx_const
2320
                    $subname = empty($this->dirs[$key][3]) ? '' : strtoupper($this->dirs[$key][3]); // Add submodule name (ex: $conf->module->submodule->dir_output)
2321
                    $forcename = empty($this->dirs[$key][4]) ? '' : strtoupper($this->dirs[$key][4]); // Change the module name if different
2322
2323
                    if (!empty($forcename)) {
2324
                        $constname = 'MAIN_MODULE_' . $forcename . "_DIR_";
2325
                    }
2326
                    if (!empty($subname)) {
2327
                        $constname = $constname . $subname . "_";
2328
                    }
2329
2330
                    $name = $constname . strtoupper($this->dirs[$key][0]);
2331
                }
2332
2333
                // Define directory full path ($dir must start with "/")
2334
                if (!getDolGlobalString('MAIN_MODULE_MULTICOMPANY') || $conf->entity == 1) {
2335
                    $fulldir = DOL_DATA_ROOT . $dir;
2336
                } else {
2337
                    $fulldir = DOL_DATA_ROOT . "/" . $conf->entity . $dir;
2338
                }
2339
                // Create dir if it does not exists
2340
                if (!empty($fulldir) && !file_exists($fulldir)) {
2341
                    if (dol_mkdir($fulldir, DOL_DATA_ROOT) < 0) {
2342
                        $this->error = $langs->trans("ErrorCanNotCreateDir", $fulldir);
2343
                        dol_syslog(get_class($this) . "::_init " . $this->error, LOG_ERR);
2344
                        $err++;
2345
                    }
2346
                }
2347
2348
                // Define the constant in database if requested (not the default mode)
2349
                if (!empty($addtodatabase) && !empty($name)) {
2350
                    $result = $this->insert_dirs($name, $dir);
2351
                    if ($result) {
2352
                        $err++;
2353
                    }
2354
                }
2355
            }
2356
        }
2357
2358
        return $err;
2359
    }
2360
2361
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2362
2363
    /**
2364
     * Adds directories definitions
2365
     *
2366
     * @param string $name Name
2367
     * @param string $dir Directory
2368
     *
2369
     * @return int             Error count (0 if OK)
2370
     */
2371
    public function insert_dirs($name, $dir)
2372
    {
2373
        // phpcs:enable
2374
        global $conf;
2375
2376
        $err = 0;
2377
2378
        $sql = "SELECT count(*)";
2379
        $sql .= " FROM " . MAIN_DB_PREFIX . "const";
2380
        $sql .= " WHERE " . $this->db->decrypt('name') . " = '" . $this->db->escape($name) . "'";
2381
        $sql .= " AND entity = " . $conf->entity;
2382
2383
        dol_syslog(get_class($this) . "::insert_dirs", LOG_DEBUG);
2384
        $result = $this->db->query($sql);
2385
        if ($result) {
2386
            $row = $this->db->fetch_row($result);
2387
2388
            if ($row[0] == 0) {
2389
                $sql = "INSERT INTO " . MAIN_DB_PREFIX . "const (name, type, value, note, visible, entity)";
2390
                $sql .= " VALUES (" . $this->db->encrypt($name) . ", 'chaine', " . $this->db->encrypt($dir) . ", '" . $this->db->escape("Directory for module " . $this->name) . "', '0', " . ((int)$conf->entity) . ")";
2391
2392
                dol_syslog(get_class($this) . "::insert_dirs", LOG_DEBUG);
2393
                $this->db->query($sql);
2394
            }
2395
        } else {
2396
            $this->error = $this->db->lasterror();
2397
            $err++;
2398
        }
2399
2400
        return $err;
2401
    }
2402
2403
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2404
2405
    /**
2406
     * Return Kanban view of a module
2407
     *
2408
     * @param string $codeenabledisable HTML code for button to enable/disable module
2409
     * @param string $codetoconfig HTML code to go to config page
2410
     * @return  string                          HTML code of Kanban view
2411
     */
2412
    public function getKanbanView($codeenabledisable = '', $codetoconfig = '')
2413
    {
2414
        global $langs;
2415
2416
        // Define imginfo
2417
        $imginfo = "info";
2418
        if ($this->isCoreOrExternalModule() == 'external') {
2419
            $imginfo = "info_black";
2420
        }
2421
2422
        $const_name = 'MAIN_MODULE_' . strtoupper(preg_replace('/^mod/i', '', get_class($this)));
2423
2424
        $version = $this->getVersion(0);
2425
        $versiontrans = '';
2426
        if (preg_match('/development/i', $version)) {
2427
            $versiontrans .= 'warning';
2428
        }
2429
        if (preg_match('/experimental/i', $version)) {
2430
            $versiontrans .= 'warning';
2431
        }
2432
        if (preg_match('/deprecated/i', $version)) {
2433
            $versiontrans .= 'warning';
2434
        }
2435
2436
        $return = '
2437
    	<div class="box-flex-item info-box-module'
2438
            . (getDolGlobalString($const_name) ? '' : ' --disabled')
2439
            . ($this->isCoreOrExternalModule() == 'external' ? ' --external' : '')
2440
            . ($this->needUpdate ? ' --need-update' : '')
2441
            . '">
2442
	    <div class="info-box info-box-sm info-box-module">
2443
	    <div class="info-box-icon' . (!getDolGlobalString($const_name) ? '' : ' info-box-icon-module-enabled' . ($versiontrans ? ' info-box-icon-module-warning' : '')) . '">';
2444
2445
        $alttext = '';
2446
        //if (is_array($objMod->need_dolibarr_version)) $alttext.=($alttext?' - ':'').'Dolibarr >= '.join('.',$objMod->need_dolibarr_version);
2447
        //if (is_array($objMod->phpmin)) $alttext.=($alttext?' - ':'').'PHP >= '.join('.',$objMod->phpmin);
2448
        if (!empty($this->picto)) {
2449
            if (preg_match('/^\//i', $this->picto)) {
2450
                $return .= img_picto($alttext, $this->picto, 'class="inline-block valignmiddle"', 1);
2451
            } else {
2452
                $return .= img_object($alttext, $this->picto, 'class="inline-block valignmiddle"');
2453
            }
2454
        } else {
2455
            $return .= img_object($alttext, 'generic', 'class="inline-block valignmiddle"');
2456
        }
2457
2458
        if ($this->isCoreOrExternalModule() == 'external' || preg_match('/development|experimental|deprecated/i', $version)) {
2459
            $versionTitle = $langs->trans("Version") . ' ' . $this->getVersion(1);
2460
            if ($this->needUpdate) {
2461
                $versionTitle .= '<br>' . $langs->trans('ModuleUpdateAvailable') . ' : ' . $this->lastVersion;
2462
            }
2463
2464
            $return .= '<span class="info-box-icon-version' . ($versiontrans ? ' ' . $versiontrans : '') . ' classfortooltip" title="' . dol_escape_js($versionTitle) . '" >';
2465
            $return .= $this->getVersion(1);
2466
            $return .= '</span>';
2467
        }
2468
2469
        $return .= '</div>
2470
	    <div class="info-box-content info-box-text-module' . (!getDolGlobalString($const_name) ? '' : ' info-box-module-enabled' . ($versiontrans ? ' info-box-content-warning' : '')) . '">
2471
	    <span class="info-box-title">' . $this->getName() . '</span>
2472
	    <span class="info-box-desc twolinesmax opacitymedium" title="' . dol_escape_htmltag($this->getDesc()) . '">' . nl2br($this->getDesc()) . '</span>';
2473
2474
        $return .= '<div class="valignmiddle inline-block info-box-more">';
2475
        //if ($versiontrans) print img_warning($langs->trans("Version").' '.$this->getVersion(1)).' ';
2476
        $return .= '<a class="valignmiddle inline-block" href="javascript:document_preview(\'' . constant('BASE_URL') . '/admin/modulehelp.php?id=' . ((int)$this->numero) . '\',\'text/html\',\'' . dol_escape_js($langs->trans("Module")) . '\')">' . img_picto(($this->isCoreOrExternalModule() == 'external' ? $langs->trans("ExternalModule") . ' - ' : '') . $langs->trans("ClickToShowDescription"), $imginfo) . '</a>';
2477
        $return .= '</div><br>';
2478
2479
        $return .= '<div class="valignmiddle inline-block info-box-actions">';
2480
        $return .= '<div class="valignmiddle inline-block info-box-setup">';
2481
        $return .= $codetoconfig;
2482
        $return .= '</div>';
2483
        $return .= '<div class="valignmiddle inline-block marginleftonly marginrightonly">';
2484
        $return .= $codeenabledisable;
2485
        $return .= '</div>';
2486
        $return .= '</div>';
2487
2488
        $return .= '
2489
	    </div><!-- /.info-box-content -->
2490
	    </div><!-- /.info-box -->
2491
	    </div>';
2492
2493
        return $return;
2494
    }
2495
2496
    /**
2497
     * Tells if module is core or external.
2498
     * 'dolibarr' and 'dolibarr_deprecated' is core
2499
     * 'experimental' and 'development' is core
2500
     *
2501
     * @return string  'core', 'external' or 'unknown'
2502
     */
2503
    public function isCoreOrExternalModule()
2504
    {
2505
        if ($this->version == 'dolibarr' || $this->version == 'dolibarr_deprecated') {
2506
            return 'core';
2507
        }
2508
        if (!empty($this->version) && !in_array($this->version, array('experimental', 'development'))) {
2509
            return 'external';
2510
        }
2511
        if (!empty($this->editor_name) || !empty($this->editor_url)) {
2512
            return 'external';
2513
        }
2514
        if ($this->numero >= 100000) {
2515
            return 'external';
2516
        }
2517
        return 'unknown';
2518
    }
2519
2520
    /**
2521
     * Gives module version (translated if param $translated is on)
2522
     * For 'experimental' modules, gives 'experimental' translation
2523
     * For 'dolibarr' modules, gives Dolibarr version
2524
     *
2525
     * @param int $translated 1=Special version keys are translated, 0=Special version keys are not translated
2526
     * @return string                       Module version
2527
     */
2528
    public function getVersion($translated = 1)
2529
    {
2530
        global $langs;
2531
        $langs->load("admin");
2532
2533
        $ret = '';
2534
2535
        $newversion = preg_replace('/_deprecated/', '', $this->version);
2536
        if ($newversion == 'experimental') {
2537
            $ret = ($translated ? $langs->transnoentitiesnoconv("VersionExperimental") : $newversion);
2538
        } elseif ($newversion == 'development') {
2539
            $ret = ($translated ? $langs->transnoentitiesnoconv("VersionDevelopment") : $newversion);
2540
        } elseif ($newversion == 'dolibarr') {
2541
            $ret = DOL_VERSION;
2542
        } elseif ($newversion) {
2543
            $ret = $newversion;
2544
        } else {
2545
            $ret = ($translated ? $langs->transnoentitiesnoconv("VersionUnknown") : 'unknown');
2546
        }
2547
2548
        if (preg_match('/_deprecated/', $this->version)) {
2549
            $ret .= ($translated ? ' (' . $langs->transnoentitiesnoconv("Deprecated") . ')' : $this->version);
2550
        }
2551
        return $ret;
2552
    }
2553
2554
    /**
2555
     * Gives the translated module name if translation exists in admin.lang or into language files of module.
2556
     * Otherwise return the module key name.
2557
     *
2558
     * @return string  Translated module name
2559
     */
2560
    public function getName()
2561
    {
2562
        global $langs;
2563
        $langs->load("admin");
2564
2565
        if ($langs->transnoentitiesnoconv("Module" . $this->numero . "Name") != "Module" . $this->numero . "Name") {
2566
            // If module name translation exists
2567
            return $langs->transnoentitiesnoconv("Module" . $this->numero . "Name");
2568
        } else {
2569
            // If module name translation using it's unique id does not exist, we try to use its name to find translation
2570
            if (is_array($this->langfiles)) {
2571
                foreach ($this->langfiles as $val) {
2572
                    if ($val) {
2573
                        $langs->load($val);
2574
                    }
2575
                }
2576
            }
2577
2578
            if ($langs->trans("Module" . $this->name . "Name") != "Module" . $this->name . "Name") {
2579
                // If module name translation exists
2580
                return $langs->transnoentitiesnoconv("Module" . $this->name . "Name");
2581
            }
2582
2583
            // Last chance with simple label
2584
            return $langs->transnoentitiesnoconv($this->name);
2585
        }
2586
    }
2587
2588
    /**
2589
     * Gives the translated module description if translation exists in admin.lang or the default module description
2590
     *
2591
     * @return string  Translated module description
2592
     */
2593
    public function getDesc()
2594
    {
2595
        global $langs;
2596
        $langs->load("admin");
2597
2598
        if ($langs->transnoentitiesnoconv("Module" . $this->numero . "Desc") != "Module" . $this->numero . "Desc") {
2599
            // If module description translation exists
2600
            return $langs->transnoentitiesnoconv("Module" . $this->numero . "Desc");
2601
        } else {
2602
            // If module description translation does not exist using its unique id, we can use its name to find translation
2603
            if (is_array($this->langfiles)) {
2604
                foreach ($this->langfiles as $val) {
2605
                    if ($val) {
2606
                        $langs->load($val);
2607
                    }
2608
                }
2609
            }
2610
2611
            if ($langs->transnoentitiesnoconv("Module" . $this->name . "Desc") != "Module" . $this->name . "Desc") {
2612
                // If module name translation exists
2613
                return $langs->trans("Module" . $this->name . "Desc");
2614
            }
2615
2616
            // Last chance with simple label
2617
            return $langs->trans($this->description);
2618
        }
2619
    }
2620
2621
    /**
2622
     * Check for module update
2623
     * TODO : store results for $this->url_last_version and $this->needUpdate
2624
     * Add a cron task to monitor for updates
2625
     *
2626
     * @return int Return integer <0 if Error, 0 == no update needed,  >0 if need update
2627
     */
2628
    public function checkForUpdate()
2629
    {
2630
        require_once constant('DOL_DOCUMENT_ROOT') . '/core/lib/geturl.lib.php';
2631
        if (!empty($this->url_last_version)) {
2632
            $lastVersion = getURLContent($this->url_last_version, 'GET', '', 1, array(), array('http', 'https'), 0);    // Accept http or https links on external remote server only
2633
            if (isset($lastVersion['content']) && strlen($lastVersion['content']) < 30) {
2634
                // Security warning :  be careful with remote data content, the module editor could be hacked (or evil) so limit to a-z A-Z 0-9 _ . -
2635
                $this->lastVersion = preg_replace("/[^a-zA-Z0-9_\.\-]+/", "", $lastVersion['content']);
2636
                if (version_compare($this->lastVersion, $this->version) > 0) {
2637
                    $this->needUpdate = true;
2638
                    return 1;
2639
                } else {
2640
                    $this->needUpdate = false;
2641
                    return 0;
2642
                }
2643
            } else {
2644
                return -1;
2645
            }
2646
        }
2647
        return 0;
2648
    }
2649
2650
    /**
2651
     * Create tables and keys required by module:
2652
     * - Files table.sql or table-module.sql with create table instructions
2653
     * - Then table.key.sql or table-module.key.sql with create keys instructions
2654
     * - Then data_xxx.sql (usually provided by external modules only)
2655
     * - Then update_xxx.sql (usually provided by external modules only)
2656
     * Files must be stored in subdirectory 'tables' or 'data' into directory $reldir (Example: '/install/mysql/' or '/module/sql/')
2657
     * This function may also be called by :
2658
     * - _load_tables('/install/mysql/', 'modulename') into the this->init() of core module descriptors.
2659
     * - _load_tables('/mymodule/sql/') into the this->init() of external module descriptors.
2660
     *
2661
     * @param string $reldir Relative directory where to scan files. Example: '/install/mysql/' or '/module/sql/'
2662
     * @param string $onlywithsuffix Only with the defined suffix
2663
     * @return  int<0,1>                        Return integer <=0 if KO, >0 if OK
2664
     */
2665
    protected function _load_tables($reldir, $onlywithsuffix = '')
2666
    {
2667
        // phpcs:enable
2668
        global $conf;
2669
2670
        $error = 0;
2671
        $dirfound = 0;
2672
        $ok = 1;
2673
2674
        if (empty($reldir)) {
2675
            return 1;
2676
        }
2677
2678
        include_once DOL_DOCUMENT_ROOT . '/core/lib/admin.lib.php';
2679
2680
        foreach ($conf->file->dol_document_root as $dirroot) {
2681
            if ($ok == 1) {
2682
                $dirsql = $dirroot . $reldir;
2683
                $ok = 0;
2684
2685
                // We will loop on xxx/, xxx/tables/, xxx/data/
2686
                $listofsubdir = array('', 'tables/', 'data/');
2687
                if ($this->db->type == 'pgsql') {
2688
                    $listofsubdir[] = '../pgsql/functions/';
2689
                }
2690
2691
                foreach ($listofsubdir as $subdir) {
2692
                    $dir = $dirsql . $subdir;
2693
2694
                    if (!is_dir($dir)) {
2695
                        continue;
2696
                    }
2697
                    $handle = opendir($dir); // Dir may not exists
2698
2699
                    if (is_resource($handle)) {
2700
                        $dirfound++;
2701
2702
                        // Run llx_mytable.sql files, then llx_mytable_*.sql
2703
                        $files = array();
2704
                        while (($file = readdir($handle)) !== false) {
2705
                            $files[] = $file;
2706
                        }
2707
                        sort($files);
2708
                        foreach ($files as $file) {
2709
                            if ($onlywithsuffix) {
2710
                                if (!preg_match('/\-' . preg_quote($onlywithsuffix, '/') . '\./i', $file)) {
2711
                                    //print 'File '.$file.' does not match suffix '.$onlywithsuffix.' so it is discarded<br>'."\n";
2712
                                    continue;
2713
                                } else {
2714
                                    //print 'File '.$file.' match suffix '.$onlywithsuffix.' so we keep it<br>'."\n";
2715
                                }
2716
                            }
2717
                            if (preg_match('/\.sql$/i', $file) && !preg_match('/\.key\.sql$/i', $file) && substr($file, 0, 4) == 'llx_') {
2718
                                $result = run_sql($dir . $file, !getDolGlobalString('MAIN_DISPLAY_SQL_INSTALL_LOG') ? 1 : 0, '', 1);
2719
                                if ($result <= 0) {
2720
                                    $error++;
2721
                                }
2722
                            }
2723
                        }
2724
2725
                        rewinddir($handle);
2726
2727
                        // Run llx_mytable.key.sql files (Must be done after llx_mytable.sql) then then llx_mytable_*.key.sql
2728
                        $files = array();
2729
                        while (($file = readdir($handle)) !== false) {
2730
                            $files[] = $file;
2731
                        }
2732
                        sort($files);
2733
                        foreach ($files as $file) {
2734
                            if ($onlywithsuffix) {
2735
                                if (!preg_match('/\-' . preg_quote($onlywithsuffix, '/') . '\./i', $file)) {
2736
                                    //print 'File '.$file.' does not match suffix '.$onlywithsuffix.' so it is discarded<br>'."\n";
2737
                                    continue;
2738
                                } else {
2739
                                    //print 'File '.$file.' match suffix '.$onlywithsuffix.' so we keep it<br>'."\n";
2740
                                }
2741
                            }
2742
                            if (preg_match('/\.key\.sql$/i', $file) && substr($file, 0, 4) == 'llx_') {
2743
                                $result = run_sql($dir . $file, !getDolGlobalString('MAIN_DISPLAY_SQL_INSTALL_LOG') ? 1 : 0, '', 1);
2744
                                if ($result <= 0) {
2745
                                    $error++;
2746
                                }
2747
                            }
2748
                        }
2749
2750
                        rewinddir($handle);
2751
2752
                        // Run functions-xxx.sql files (Must be done after llx_mytable.key.sql)
2753
                        $files = array();
2754
                        while (($file = readdir($handle)) !== false) {
2755
                            $files[] = $file;
2756
                        }
2757
                        sort($files);
2758
                        foreach ($files as $file) {
2759
                            if ($onlywithsuffix) {
2760
                                if (!preg_match('/\-' . preg_quote($onlywithsuffix, '/') . '\./i', $file)) {
2761
                                    //print 'File '.$file.' does not match suffix '.$onlywithsuffix.' so it is discarded<br>'."\n";
2762
                                    continue;
2763
                                } else {
2764
                                    //print 'File '.$file.' match suffix '.$onlywithsuffix.' so we keep it<br>'."\n";
2765
                                }
2766
                            }
2767
                            if (preg_match('/\.sql$/i', $file) && !preg_match('/\.key\.sql$/i', $file) && substr($file, 0, 9) == 'functions') {
2768
                                $result = run_sql($dir . $file, !getDolGlobalString('MAIN_DISPLAY_SQL_INSTALL_LOG') ? 1 : 0, '', 1);
2769
                                if ($result <= 0) {
2770
                                    $error++;
2771
                                }
2772
                            }
2773
                        }
2774
2775
                        rewinddir($handle);
2776
2777
                        // Run data_xxx.sql files (Must be done after llx_mytable.key.sql)
2778
                        $files = array();
2779
                        while (($file = readdir($handle)) !== false) {
2780
                            $files[] = $file;
2781
                        }
2782
                        sort($files);
2783
                        foreach ($files as $file) {
2784
                            if ($onlywithsuffix) {
2785
                                if (!preg_match('/\-' . preg_quote($onlywithsuffix, '/') . '\./i', $file)) {
2786
                                    //print 'File '.$file.' does not match suffix '.$onlywithsuffix.' so it is discarded<br>'."\n";
2787
                                    continue;
2788
                                } else {
2789
                                    //print 'File '.$file.' match suffix '.$onlywithsuffix.' so we keep it<br>'."\n";
2790
                                }
2791
                            }
2792
                            if (preg_match('/\.sql$/i', $file) && !preg_match('/\.key\.sql$/i', $file) && substr($file, 0, 4) == 'data') {
2793
                                $result = run_sql($dir . $file, !getDolGlobalString('MAIN_DISPLAY_SQL_INSTALL_LOG') ? 1 : 0, '', 1);
2794
                                if ($result <= 0) {
2795
                                    $error++;
2796
                                }
2797
                            }
2798
                        }
2799
2800
                        rewinddir($handle);
2801
2802
                        // Run update_xxx.sql files
2803
                        $files = array();
2804
                        while (($file = readdir($handle)) !== false) {
2805
                            $files[] = $file;
2806
                        }
2807
                        sort($files);
2808
                        foreach ($files as $file) {
2809
                            if ($onlywithsuffix) {
2810
                                if (!preg_match('/\-' . preg_quote($onlywithsuffix, '/') . '\./i', $file)) {
2811
                                    //print 'File '.$file.' does not match suffix '.$onlywithsuffix.' so it is discarded<br>'."\n";
2812
                                    continue;
2813
                                } else {
2814
                                    //print 'File '.$file.' match suffix '.$onlywithsuffix.' so we keep it<br>'."\n";
2815
                                }
2816
                            }
2817
                            if (preg_match('/\.sql$/i', $file) && !preg_match('/\.key\.sql$/i', $file) && substr($file, 0, 6) == 'update') {
2818
                                $result = run_sql($dir . $file, !getDolGlobalString('MAIN_DISPLAY_SQL_INSTALL_LOG') ? 1 : 0, '', 1);
2819
                                if ($result <= 0) {
2820
                                    $error++;
2821
                                }
2822
                            }
2823
                        }
2824
2825
                        closedir($handle);
2826
                    }
2827
                }
2828
2829
                if ($error == 0) {
2830
                    $ok = 1;
2831
                }
2832
            }
2833
        }
2834
2835
        if (!$dirfound) {
2836
            dol_syslog("A module wants to load sql files from " . $reldir . " but this directory was not found.", LOG_WARNING);
2837
        }
2838
        return $ok;
2839
    }
2840
2841
    /**
2842
     * Helper method to declare dictionaries one at a time (rather than declaring dictionaries property by property).
2843
     *
2844
     * @param array $dictionaryArray Array describing one dictionary. Keys are:
2845
     *                               'name',        table name (without prefix)
2846
     *                               'lib',         dictionary label
2847
     *                               'sql',         query for select
2848
     *                               'sqlsort',     sort order
2849
     *                               'field',       comma-separated list of fields to select
2850
     *                               'fieldvalue',  list of columns used for editing existing rows
2851
     *                               'fieldinsert', list of columns used for inserting new rows
2852
     *                               'rowid',       name of the technical ID (primary key) column, usually 'rowid'
2853
     *                               'cond',        condition for the dictionary to be shown / active
2854
     *                               'help',        optional array of translation keys by column for tooltips
2855
     *                               'fieldcheck'   (appears to be unused)
2856
     * @param string $langs Optional translation file to include (appears to be unused)
2857
     * @return void
2858
     */
2859
    protected function declareNewDictionary($dictionaryArray, $langs = '')
2860
    {
2861
        $fields = array('name', 'lib', 'sql', 'sqlsort', 'field', 'fieldvalue', 'fieldinsert', 'rowid', 'cond', 'help', 'fieldcheck');
2862
2863
        foreach ($fields as $field) {
2864
            if (!empty($dictionaryArray[$field])) {
2865
                $this->dictionaries['tab' . $field][] = $dictionaryArray[$field];
2866
            }
2867
        }
2868
        if ($langs && !in_array($langs, $this->dictionaries[$langs])) $this->dictionaries['langs'][] = $langs;
2869
    }
2870
}
2871