Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions htdocs/admin/perms.php
Original file line number Diff line number Diff line change
Expand Up @@ -146,14 +146,20 @@
print '</tr>'."\n";

//print "xx".$conf->global->MAIN_USE_ADVANCED_PERMS;
$sql = "SELECT r.id, r.libelle as label, r.module, r.perms, r.subperms, r.module_position, r.bydefault";
$sql = "SELECT r.id, r.libelle as label, r.module, r.module_origin, r.perms, r.subperms, r.module_position, r.bydefault";
$sql .= " FROM ".MAIN_DB_PREFIX."rights_def as r";
$sql .= " WHERE r.libelle NOT LIKE 'tou%'"; // On ignore droits "tous"
$sql .= " AND r.entity = ".((int) $entity);
if (!getDolGlobalString('MAIN_USE_ADVANCED_PERMS')) {
$sql .= " AND r.perms NOT LIKE '%_advance'"; // Hide advanced perms if option is not enabled
}
$sql .= " ORDER BY r.family_position, r.module_position, r.module, r.id";
// sort_order (see DolibarrModules::KEY_SORT_ORDER) lets a right filed into another module's
// section via module_origin sort right after a given native right of that module (its id)
// instead of always trailing the whole group; falls back to id when unset (default 0). The
// native right being anchored to must always sort before the right(s) anchored onto it,
// regardless of which one has the numerically larger id, hence the explicit CASE discriminant
// before the final id tiebreak.
$sql .= " ORDER BY r.family_position, r.module_position, r.module, (CASE WHEN r.sort_order > 0 THEN r.sort_order ELSE r.id END), (CASE WHEN r.sort_order > 0 THEN 1 ELSE 0 END), r.id";

$result = $db->query($sql);
if ($result) {
Expand Down Expand Up @@ -264,6 +270,15 @@

// Permission and tick
$permlabel = (getDolGlobalString('MAIN_USE_ADVANCED_PERMS') && ($langs->trans("PermissionAdvanced".$obj->id) != "PermissionAdvanced".$obj->id) ? $langs->trans("PermissionAdvanced".$obj->id) : (($langs->trans("Permission".$obj->id) != "Permission".$obj->id) ? $langs->trans("Permission".$obj->id) : $langs->trans($obj->label)));

// This right is declared by another module (module_origin) but filed into this module's
// section for display (KEY_MODULE): show a small badge so it is not mistaken for a native
// right of this module.
if (!empty($obj->module_origin) && $obj->module_origin != $obj->module && !empty($modules[$obj->module_origin])) {
$permoriginmod = $modules[$obj->module_origin];
$permoriginpicto = ($permoriginmod->picto ? $permoriginmod->picto : 'generic');
$permlabel = img_picto($langs->trans("RightProvidedByModule", $permoriginmod->getName()), $permoriginpicto, 'class="paddingrightonly"').$permlabel;
}
print '<td>';
print $permlabel;
if ($langs->trans("Permission".$obj->id.'b') != "Permission".$obj->id.'b') {
Expand All @@ -283,7 +298,10 @@
if ($user->admin) {
print '<td class="right">';
$htmltext = $langs->trans("ID").': '.$obj->id;
$htmltext .= '<br>'.$langs->trans("Permission").': user->hasRight(\''.dol_escape_htmltag($obj->module).'\', \''.dol_escape_htmltag($obj->perms).'\''.($obj->subperms ? ', \''.dol_escape_htmltag($obj->subperms).'\'' : '').')';
// hasRight() is actually checked against module_origin when set, not the display
// module column, see User::loadRights().
$htmltextmodule = (!empty($obj->module_origin) ? $obj->module_origin : $obj->module);
$htmltext .= '<br>'.$langs->trans("Permission").': user->hasRight(\''.dol_escape_htmltag($htmltextmodule).'\', \''.dol_escape_htmltag($obj->perms).'\''.($obj->subperms ? ', \''.dol_escape_htmltag($obj->subperms).'\'' : '').')';
print $form->textwithpicto('', $htmltext);
//print '<span class="opacitymedium">'.$obj->id.'</span>';
print '</td>';
Expand Down
85 changes: 81 additions & 4 deletions htdocs/core/modules/DolibarrModules.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,12 @@ class DolibarrModules // Can not be abstract, because we need to instantiate it
const KEY_SECOND_LEVEL = 5;
const KEY_MODULE = 6;
const KEY_ENABLED = 7;
const KEY_SORT_ORDER = 8;

/**
* @var array<string,array{family:string,position:int}> Cache of family/position looked up by rights_class, used by getModuleInfoByRightsClass(). Shared across all module instances for the duration of the request.
*/
protected static $keyModuleInfoCache = array();

/**
* @var array<array{commentgroup?:string,mainmenu:string,leftmenu:string,langs:string,enabled:int|string,target:string,titre:string,user:int,fk_menu:string,fk_parent:string,url:string,position:int,positionfull:int|string,perms:string,type:string}>|int<1,1> Module menu entries (1 means the menu entries are not declared into module descriptor but are hardcoded into menu manager)
Expand Down Expand Up @@ -2016,10 +2022,6 @@ public function insert_permissions($reinitadminperms = 0, $force_entity = null,
$r_perms = $this->rights[$key][self::KEY_FIRST_LEVEL] ?? '';
$r_subperms = $this->rights[$key][self::KEY_SECOND_LEVEL] ?? '';

$r_module_position = $this->getModulePosition();
$r_family = $this->family;
$r_family_position = 0;

// KEY_FIRST_LEVEL (perms) must not be empty
if (empty($r_perms)) {
continue;
Expand All @@ -2038,6 +2040,26 @@ public function insert_permissions($reinitadminperms = 0, $force_entity = null,
$r_module_origin = (empty($this->rights_class) ? strtolower($this->name) : $this->rights_class);
}

if (!empty($r_module_origin) && $r_module !== $r_module_origin) {
// This right is filed under a different module's section of the permission
// grid (KEY_MODULE) than the one declaring it: use that target module's own
// family/position so it appears grouped with its native rights instead of
// opening a second, misplaced section for the same module.
$r_targetmoduleinfo = $this->getModuleInfoByRightsClass($r_module);
$r_module_position = $r_targetmoduleinfo['position'];
$r_family = $r_targetmoduleinfo['family'];
} else {
$r_module_position = $this->getModulePosition();
$r_family = $this->family;
}
$r_family_position = 0;

// optional fine sort key inside the module/family group (default 0 means: fall
// back to id, same as before this key existed). A right filed into another
// module's section via KEY_MODULE can set this to the id of the native right it
// should be sorted right after.
$r_sort_order = $this->rights[$key][self::KEY_SORT_ORDER] ?? 0;

// condition to show or hide a user right (default: 1) (eg isModEnabled('anothermodule') or ($conf->global->MAIN_FEATURES_LEVEL > 0) or etc..)
$r_enabled = $this->rights[$key][self::KEY_ENABLED] ?? '1';

Expand All @@ -2059,6 +2081,7 @@ public function insert_permissions($reinitadminperms = 0, $force_entity = null,
$sql .= ", module_position"; // Not that module_position can be fixed eynamically when accessing page user/perms.php
$sql .= ", family";
$sql .= ", family_position";
$sql .= ", sort_order";
$sql .= ", type"; // Not used yet
$sql .= ", bydefault";
$sql .= ", perms";
Expand All @@ -2073,6 +2096,7 @@ public function insert_permissions($reinitadminperms = 0, $force_entity = null,
$sql .= ", '".$this->db->escape((string) $r_module_position)."'";
$sql .= ", '".$this->db->escape($r_family)."'";
$sql .= ", '".$this->db->escape((string) $r_family_position)."'";
$sql .= ", ".((int) $r_sort_order);
$sql .= ", '".$this->db->escape($r_type)."'"; // Not used yet
$sql .= ", ".((int) $r_default);
$sql .= ", '".$this->db->escape($r_perms)."'";
Expand Down Expand Up @@ -2142,6 +2166,59 @@ public function insert_permissions($reinitadminperms = 0, $force_entity = null,
return $err;
}

/**
* Look up the family and module_position of another module by its rights_class, so a right
* filed under that module via KEY_MODULE can share its family/position and be grouped with
* that module's native rights instead of opening a second, misplaced section on the same
* permission grid page (see insert_permissions()). Result is cached per rights_class for the
* duration of the request since this scans every module descriptor found on disk. Falls back
* to this module's own family/position if no module with that rights_class is found.
*
* @param string $rightsclass rights_class of the target module (value used as KEY_MODULE)
* @return array{family:string,position:int} family and module_position of the target module
*/
protected function getModuleInfoByRightsClass($rightsclass)
{
global $db;

if (isset(self::$keyModuleInfoCache[$rightsclass])) {
return self::$keyModuleInfoCache[$rightsclass];
}

// Fallback: if no module with this rights_class is found (typo, or module removed from disk),
// still register something under its own family/position rather than leaving it undefined.
$result = array('family' => $this->family, 'position' => (int) $this->getModulePosition());

$modulesdir = dolGetModulesDirs();
foreach ($modulesdir as $dir) {
$handle = @opendir(dol_osencode($dir));
if (is_resource($handle)) {
while (($file = readdir($handle)) !== false) {
if (is_readable($dir.$file) && substr($file, 0, 3) == 'mod' && substr($file, dol_strlen($file) - 10) == '.class.php') {
$modName = substr($file, 0, dol_strlen($file) - 10);
if ($modName && $modName != get_class($this)) {
include_once $dir.$file;
if (class_exists($modName)) {
'@phan-var-force class-string<DolibarrModules> $modName';
$objMod = new $modName($db);
'@phan-var-force DolibarrModules $objMod';
if (!empty($objMod->rights_class) && $objMod->rights_class === $rightsclass) {
$result = array('family' => $objMod->family, 'position' => (int) $objMod->getModulePosition());
break 2;
}
}
}
}
}
closedir($handle);
}
}

self::$keyModuleInfoCache[$rightsclass] = $result;

return $result;
}


// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
/**
Expand Down
4 changes: 4 additions & 0 deletions htdocs/install/mysql/migration/24.0.0-25.0.0.sql
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ ALTER TABLE llx_product ADD INDEX idx_product_entity_tobuy (entity, tobuy);
ALTER TABLE llx_product ADD INDEX idx_product_datec (datec);
ALTER TABLE llx_product ADD INDEX idx_product_tms (tms);

-- Optional fine sort key for rights_def, used by rights filed into another module's
-- section via module_origin (KEY_MODULE) to sort next to a given native right of that module
ALTER TABLE llx_rights_def ADD COLUMN sort_order integer DEFAULT 0 NOT NULL AFTER family_position;




Expand Down
1 change: 1 addition & 0 deletions htdocs/install/mysql/tables/llx_rights_def.sql
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ create table llx_rights_def
module_position integer DEFAULT 0 NOT NULL,
family varchar(64) NULL,
family_position integer DEFAULT 0 NOT NULL,
sort_order integer DEFAULT 0 NOT NULL, -- optional fine sort key inside the module/family group; when 0 (default), sorting falls back to id (unchanged behavior). Used by rights filed into another module's section via module_origin to sort next to a given native right (set to that right's id).
perms varchar(50),
subperms varchar(50),
type varchar(1), -- deprecated
Expand Down
1 change: 1 addition & 0 deletions htdocs/langs/en_US/users.lang
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ UserCard=User card
GroupCard=Group card
Permission=Permission
Permissions=Permissions
RightProvidedByModule=Right provided by module: %s
EditPassword=Edit password
SendNewPassword=Regenerate and send password
SendNewPasswordLink=Send link to reset password
Expand Down
14 changes: 14 additions & 0 deletions htdocs/modulebuilder/template/core/modules/modMyModule.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,20 @@ public function __construct($db)
$this->rights[$r][4] = 'myobject';
$this->rights[$r][5] = 'delete'; // In php code, permission will be checked by test if ($user->hasRight('mymodule', 'myobject', 'delete'))
$r++;
// Example of a permission filed into another module's section of the permission grid
// (KEY_MODULE, index 6) instead of this module's own, so it appears grouped with that
// module's native rights. It is still checked with hasRight('mymodule', ...) (module_origin,
// filled in automatically, is what hasRight() actually resolves against, not KEY_MODULE).
// KEY_SORT_ORDER (index 8) is optional and, when set to the id of a specific native right
// of the target module, sorts this permission right after it instead of trailing the whole
// section (falls back to sorting by this permission's own id when left unset).
$this->rights[$r][0] = $this->numero . sprintf("%02d", ($o * 10) + 4); // Permission id (must not be already used)
$this->rights[$r][1] = 'Read objects of MyModule from the third party card'; // Permission label
$this->rights[$r][4] = 'myobject';
$this->rights[$r][5] = 'readfromthirdparty';
$this->rights[$r][6] = 'societe'; // KEY_MODULE: rights_class of the target module (here, thirdparties)
$this->rights[$r][8] = 121; // KEY_SORT_ORDER: id of the native thirdparty right to sort after (here, "Read third parties")
$r++;
*/
/* END MODULEBUILDER PERMISSIONS */

Expand Down
13 changes: 9 additions & 4 deletions htdocs/user/class/user.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -1419,7 +1419,7 @@ public function loadRights($moduletag = '', $forcereload = 0)

if (!$alreadyloaded) {
// First user permissions
$sql = "SELECT DISTINCT r.module, r.perms, r.subperms";
$sql = "SELECT DISTINCT r.module, r.module_origin, r.perms, r.subperms";
$sql .= " FROM ".$this->db->prefix()."user_rights as ur,";
$sql .= " ".$this->db->prefix()."rights_def as r";
$sql .= " WHERE r.id = ur.fk_id";
Expand Down Expand Up @@ -1451,7 +1451,12 @@ public function loadRights($moduletag = '', $forcereload = 0)
$obj = $this->db->fetch_object($resql);

if ($obj) {
$module = $obj->module;
// module_origin (set only when the right was declared by another module
// via KEY_MODULE, to be filed into a foreign module's section of the
// permission grid) is the namespace actually used to check the right with
// hasRight(), so the declaring module keeps control of it regardless of
// which module's section it is grouped under for display.
$module = (!empty($obj->module_origin) ? $obj->module_origin : $obj->module);
$perms = $obj->perms;
$subperms = $obj->subperms;

Expand Down Expand Up @@ -1483,7 +1488,7 @@ public function loadRights($moduletag = '', $forcereload = 0)
}

// Now permissions of groups
$sql = "SELECT DISTINCT r.module, r.perms, r.subperms, r.entity";
$sql = "SELECT DISTINCT r.module, r.module_origin, r.perms, r.subperms, r.entity";
$sql .= " FROM ".$this->db->prefix()."usergroup_rights as gr,";
$sql .= " ".$this->db->prefix()."usergroup_user as gu,";
$sql .= " ".$this->db->prefix()."rights_def as r";
Expand Down Expand Up @@ -1524,7 +1529,7 @@ public function loadRights($moduletag = '', $forcereload = 0)
$obj = $this->db->fetch_object($resql);

if ($obj) {
$module = $obj->module;
$module = (!empty($obj->module_origin) ? $obj->module_origin : $obj->module);
$perms = $obj->perms;
$subperms = $obj->subperms;

Expand Down
4 changes: 2 additions & 2 deletions htdocs/user/class/usergroup.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -620,7 +620,7 @@ public function loadRights($moduletag = '')
}

// Load permission from group
$sql = "SELECT r.module, r.perms, r.subperms ";
$sql = "SELECT r.module, r.module_origin, r.perms, r.subperms ";
$sql .= " FROM ".$this->db->prefix()."usergroup_rights as u, ".$this->db->prefix()."rights_def as r";
$sql .= " WHERE r.id = u.fk_id";
$sql .= " AND r.entity = ".((int) $conf->entity);
Expand All @@ -640,7 +640,7 @@ public function loadRights($moduletag = '')
$obj = $this->db->fetch_object($resql);

if ($obj) {
$module = $obj->module;
$module = (!empty($obj->module_origin) ? $obj->module_origin : $obj->module);
$perms = $obj->perms;
$subperms = $obj->subperms;

Expand Down
26 changes: 23 additions & 3 deletions htdocs/user/group/perms.php
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@
print '</tr>'."\n";

// Get list of all permissions
$sql = "SELECT r.id, r.libelle as label, r.module, r.perms, r.subperms, r.module_position, r.bydefault, r.family, r.family_position";
$sql = "SELECT r.id, r.libelle as label, r.module, r.module_origin, r.perms, r.subperms, r.module_position, r.bydefault, r.family, r.family_position, r.sort_order";
$sql .= " FROM ".MAIN_DB_PREFIX."rights_def as r";
$sql .= " WHERE r.libelle NOT LIKE 'tou%'"; // We ignore permission "tous les tiers". Why ?
$sql .= " AND r.entity = ".((int) $entity);
Expand Down Expand Up @@ -405,7 +405,15 @@
}
}

$obj->position = $obj->family_position.'_'.$obj->module_position.'_'.$obj->id;
// sort_order (set by rights filed into another module's section via module_origin,
// see DolibarrModules::KEY_SORT_ORDER) lets such a right sort right after a given
// native right of that module (its id) instead of always trailing the whole group.
// The native right being anchored to (sort_order=0, falls back to its own id) must
// always sort before the right(s) anchored onto it, regardless of which one has the
// numerically larger id, hence the explicit 0/1 discriminant before the final id tiebreak.
$hassortorder = !empty($obj->sort_order);
$sortkey = ($hassortorder ? $obj->sort_order : $obj->id);
$obj->position = $obj->family_position.'_'.$obj->module_position.'_'.sprintf('%010d', $sortkey).'_'.($hassortorder ? '1' : '0').'_'.sprintf('%010d', $obj->id);

$arrayofpermission[$i] = $obj;
$i++;
Expand Down Expand Up @@ -518,6 +526,15 @@

$permlabel = (getDolGlobalString('MAIN_USE_ADVANCED_PERMS') && ($langs->trans("PermissionAdvanced".$obj->id) != "PermissionAdvanced".$obj->id) ? $langs->trans("PermissionAdvanced".$obj->id) : (($langs->trans("Permission".$obj->id) != "Permission".$obj->id) ? $langs->trans("Permission".$obj->id) : $langs->trans($obj->label)));

// This right is declared by another module (module_origin) but filed into this module's
// section for display (KEY_MODULE): show a small badge so it is not mistaken for a native
// right of this module.
if (!empty($obj->module_origin) && $obj->module_origin != $obj->module && !empty($modules[$obj->module_origin])) {
$permoriginmod = $modules[$obj->module_origin];
$permoriginpicto = ($permoriginmod->picto ? $permoriginmod->picto : 'generic');
$permlabel = img_picto($langs->trans("RightProvidedByModule", $permoriginmod->getName()), $permoriginpicto, 'class="paddingrightonly"').$permlabel;
}

print '<!-- '.$obj->module.'->'.$obj->perms.($obj->subperms ? '->'.$obj->subperms : '').' -->'."\n";
print '<tr class="oddeven trtohide_'.$obj->module.'"'.(!$isexpanded ? ' style="display:none"' : '').'>';

Expand Down Expand Up @@ -584,7 +601,10 @@
if ($user->admin) {
print '<td class="right">';
$htmltext = $langs->trans("ID").': '.$obj->id;
$htmltext .= '<br>'.$langs->trans("Permission").': user->hasRight(\''.dol_escape_htmltag($obj->module).'\', \''.dol_escape_htmltag($obj->perms).'\''.($obj->subperms ? ', \''.dol_escape_htmltag($obj->subperms).'\'' : '').')';
// hasRight() is actually checked against module_origin when set, not the display
// module column, see User::loadRights().
$htmltextmodule = (!empty($obj->module_origin) ? $obj->module_origin : $obj->module);
$htmltext .= '<br>'.$langs->trans("Permission").': user->hasRight(\''.dol_escape_htmltag($htmltextmodule).'\', \''.dol_escape_htmltag($obj->perms).'\''.($obj->subperms ? ', \''.dol_escape_htmltag($obj->subperms).'\'' : '').')';
print $form->textwithpicto('', $htmltext, 1, 'help', 'inline-block marginrightonly');
//print '<span class="opacitymedium">'.$obj->id.'</span>';
print '</td>';
Expand Down
Loading
Loading