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
62 changes: 62 additions & 0 deletions htdocs/categories/class/categorie.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,14 @@ public function create($user, $notrigger = 0)
}
$this->fk_parent = ($this->fk_parent != "" ? intval($this->fk_parent) : 0);

// Check for circular reference: a descendant cannot become our parent
if ($this->fk_parent > 0 && $this->isDescendant($this->fk_parent)) {
$this->error = $langs->trans("ImpossibleAddCat", $this->label);
$this->error .= " : ".$langs->trans("ErrorCategoryParentIsDescendant");
dol_syslog($this->error, LOG_WARNING);
return -5;
}

if ($this->already_exists()) {
$this->error = $langs->trans("ImpossibleAddCat", $this->label);
$this->error .= " : ".$langs->trans("CategoryExistsAtSameLevel");
Expand Down Expand Up @@ -660,6 +668,13 @@ public function update(User $user, $notrigger = 0)
$this->fk_parent = ($this->fk_parent != "" ? intval($this->fk_parent) : 0);
$this->visible = ($this->visible != "" ? intval($this->visible) : 0);

// Check for circular reference: a descendant cannot become our parent
if ($this->fk_parent > 0 && $this->isDescendant($this->fk_parent)) {
$this->error = $langs->trans("ImpossibleUpdateCat");
$this->error .= " : ".$langs->trans("ErrorCategoryParentIsDescendant");
return -5;
}

if ($this->already_exists()) {
$this->error = $langs->trans("ImpossibleUpdateCat");
$this->error .= " : ".$langs->trans("CategoryExistsAtSameLevel");
Expand Down Expand Up @@ -1525,6 +1540,53 @@ public function already_exists()
}
}

/**
* Check if a category is a descendant of the current category.
* A descendant cannot become the parent of its ancestor (circular reference).
*
* @param int $category_id ID of the category to check
* @return bool True if category_id is a descendant of this category
*/
public function isDescendant($category_id)
{
if ($category_id <= 0 || empty($this->id)) {
return false;
}

// Direct self-reference
if ($category_id == $this->id) {
return true;
}

// Walk up the ancestors of category_id to check if we find $this->id
$current = $category_id;
$visited = array(); // Protection against already corrupted DB

while ($current > 0) {
$sql = "SELECT fk_parent FROM ".MAIN_DB_PREFIX."categorie WHERE rowid = ".((int) $current);
$resql = $this->db->query($sql);

if ($resql && $this->db->num_rows($resql) > 0) {
$obj = $this->db->fetch_object($resql);
$parent = (int) $obj->fk_parent;

if ($parent == $this->id) {
return true; // Found ourselves in ancestors = it's our descendant
}

if (isset($visited[$parent])) {
break; // Already visited = corrupted DB, stop to avoid infinite loop
}
$visited[$current] = true;
$current = $parent;
} else {
break;
}
}

return false;
}


// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
/**
Expand Down
1 change: 1 addition & 0 deletions htdocs/langs/en_US/categories.lang
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ ClassifyInCategory=Add to tag/category
RemoveCategory=Remove category
NotCategorized=Without tag/category
CategoryExistsAtSameLevel=This category already exists with this ref
ErrorCategoryParentIsDescendant=A category cannot have one of its descendants as parent (circular reference)
ContentsVisibleByAllShort=Contents visible by all
ContentsNotVisibleByAllShort=Contents not visible by all
DeleteCategory=Delete tag/category
Expand Down
1 change: 1 addition & 0 deletions htdocs/langs/fr_FR/categories.lang
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ ClassifyInCategory=Ajouter tag/catégorie
RemoveCategory=Supprimer la catégorie
NotCategorized=Sans tag/catégorie
CategoryExistsAtSameLevel=Ce tag existe déjà avec cette référence
ErrorCategoryParentIsDescendant=Une catégorie ne peut pas avoir un de ses descendants comme parent (référence circulaire)
ContentsVisibleByAllShort=Contenu visible par tous
ContentsNotVisibleByAllShort=Contenu non visible par tous
DeleteCategory=Effacer le(a) libellé/catégorie
Expand Down
Loading