Skip to content
Merged
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
115 changes: 68 additions & 47 deletions main/inc/lib/link.lib.php
Original file line number Diff line number Diff line change
Expand Up @@ -383,22 +383,14 @@ public static function addlinkcategory($type)

/**
* Used to delete a link or a category.
*
* @author Patrick Cool <[email protected]>, Ghent University
*
* @param int $id
* @param string $type The type of item to delete
*
* @return bool
*/
public static function deletelinkcategory($id, $type)
public static function deletelinkcategory(int $id, string $type, $courseId = null, bool $removeContentFromDb = false): bool
{
$courseInfo = api_get_course_info();
$tbl_link = Database::get_course_table(TABLE_LINK);
$tbl_categories = Database::get_course_table(TABLE_LINK_CATEGORY);

$course_id = $courseInfo['real_id'];
$id = intval($id);
$courseInfo = api_get_course_info_by_id($courseId);
$tblLink = Database::get_course_table(TABLE_LINK);
$tblCategories = Database::get_course_table(TABLE_LINK_CATEGORY);
$tblItemProperty = Database::get_course_table(TABLE_ITEM_PROPERTY);
$courseIdReal = $courseInfo['real_id'];

if (empty($id)) {
return false;
Expand All @@ -407,45 +399,74 @@ public static function deletelinkcategory($id, $type)
$result = false;
switch ($type) {
case 'link':
// -> Items are no longer physically deleted,
// but the visibility is set to 2 (in item_property).
// This will make a restore function possible for the platform administrator.
$sql = "UPDATE $tbl_link SET on_homepage='0'
WHERE c_id = $course_id AND id='".$id."'";
Database::query($sql);
if ($removeContentFromDb) {
// Hard delete: Remove from both c_link and item_property
$sql = "DELETE FROM $tblItemProperty
WHERE c_id = $courseIdReal AND ref = $id AND tool = '".TOOL_LINK."'";
Database::query($sql);

api_item_property_update(
$courseInfo,
TOOL_LINK,
$id,
'delete',
api_get_user_id()
);
self::delete_link_from_search_engine(api_get_course_id(), $id);
Skill::deleteSkillsFromItem($id, ITEM_TYPE_LINK);
Display::addFlash(Display::return_message(get_lang('LinkDeleted')));
$result = true;
$sql = "DELETE FROM $tblLink
WHERE c_id = $courseIdReal AND id = $id";
Database::query($sql);

self::delete_link_from_search_engine(api_get_course_id(), $id);
Skill::deleteSkillsFromItem($id, ITEM_TYPE_LINK);
$result = true;
} else {
// Soft delete: Update visibility in item_property
$sql = "UPDATE $tblLink SET on_homepage='0'
WHERE c_id = $courseIdReal AND id='$id'";
Database::query($sql);

api_item_property_update(
$courseInfo,
TOOL_LINK,
$id,
'delete',
api_get_user_id()
);
self::delete_link_from_search_engine(api_get_course_id(), $id);
Skill::deleteSkillsFromItem($id, ITEM_TYPE_LINK);
Display::addFlash(Display::return_message(get_lang('LinkDeleted')));
$result = true;
}
break;
case 'category':
// First we delete the category itself and afterwards all the links of this category.
$sql = "DELETE FROM ".$tbl_categories."
WHERE c_id = $course_id AND id='".$id."'";
Database::query($sql);
if ($removeContentFromDb) {
// Hard delete: Remove category and its links
$sql = "DELETE FROM $tblCategories
WHERE c_id = $courseIdReal AND id = $id";
Database::query($sql);

$sql = "DELETE FROM ".$tbl_link."
WHERE c_id = $course_id AND category_id='".$id."'";
Database::query($sql);
$sql = "DELETE FROM $tblLink
WHERE c_id = $courseIdReal AND category_id = $id";
Database::query($sql);

api_item_property_update(
$courseInfo,
TOOL_LINK_CATEGORY,
$id,
'delete',
api_get_user_id()
);
$sql = "DELETE FROM $tblItemProperty
WHERE c_id = $courseIdReal AND ref = $id AND tool = '".TOOL_LINK_CATEGORY."'";
Database::query($sql);
$result = true;
} else {
// Soft delete: Update visibility in item_property
$sql = "DELETE FROM $tblCategories
WHERE c_id = $courseIdReal AND id = $id";
Database::query($sql);

$sql = "DELETE FROM $tblLink
WHERE c_id = $courseIdReal AND category_id = $id";
Database::query($sql);

api_item_property_update(
$courseInfo,
TOOL_LINK_CATEGORY,
$id,
'delete',
api_get_user_id()
);

Display::addFlash(Display::return_message(get_lang('CategoryDeleted')));
$result = true;
Display::addFlash(Display::return_message(get_lang('CategoryDeleted')));
$result = true;
}
break;
}

Expand Down
2 changes: 1 addition & 1 deletion main/link/link.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ function check_url(id, url) {
Event::event_access_tool(TOOL_LINK);

/* Action Handling */
$id = isset($_REQUEST['id']) ? $_REQUEST['id'] : null;
$id = isset($_REQUEST['id']) ? (int) $_REQUEST['id'] : null;
$scope = isset($_REQUEST['scope']) ? $_REQUEST['scope'] : null;
$show = isset($_REQUEST['show']) && in_array(trim($_REQUEST['show']), ['all', 'none']) ? $_REQUEST['show'] : 'all';
$categoryId = isset($_REQUEST['category_id']) ? (int) $_REQUEST['category_id'] : '';
Expand Down
140 changes: 140 additions & 0 deletions tests/scripts/delete_duplicate_links.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<?php
/* For licensing terms, see /license.txt */
/**
* This script removes duplicated links.
* It identifies duplicate links by URL,
* makes sure no usage is associated with the duplicated link, and
* that the duplicated link is not used in a learning path.
* A duplicated link will generally match the following criteria:
* - same URL field as the original
* - same title
* - same category_id
* - same on_homepage field
* - same target
* - same course, same session (otherwise considered a different link, a voluntary copy)
* - each have entries in c_item_property because it was created legitimately
* Possible duplicates can be found with a query like:
* SELECT iid, c_id, session_id, url, title, description, target, on_homepage FROM c_link WHERE c_id = 470 AND url like '%\__.%' ORDER BY url, title;
* This script should be located inside the tests/scripts/ folder to work.
* It can be run more than one time as it will only ever affect duplicate
* links.
* If you have a very large number of links, we recommend you temporarily
* comment out the api_item_property_update() calls in
* Link::deletelinkcategory() (which deletes a link *or* a category).
* Chances are there is not even a registry of those links there in the
* first place (they were probably duplicated through a short/broken process) and
* this is where most of the time is spent during deletion.
* @author Yannick Warnier <[email protected]>
*/
exit; //remove this line to execute from the command line

use ChamiloSession as Session;

ini_set('memory_limit', '256M');

if (PHP_SAPI !== 'cli') {
die('This script can only be executed from the command line');
}

require_once __DIR__.'/../../main/inc/global.inc.php';

$debug = true;
$_user['user_id'] = 1;
Session::write('_user', $_user);

echo "[" . time() . "] Querying courses\n";
$sql = "SELECT id, code FROM course ORDER BY id";

$resCourse = Database::query($sql);
if ($resCourse === false) {
exit("Could not find any course\n");
}
$countCourses = Database::num_rows($resCourse);
echo "[" . time() . "] Found $countCourses courses\n";

$duplicatesCount = 0;
$originalsCount = 0;
$deletedCount = 0;
$itemsInLP = 0;

// Iterate through each course
while ($course = Database::fetch_assoc($resCourse)) {
if (empty($course['id'])) {
continue; // Skip invalid course IDs
}

$sql2 = "SELECT iid, url, title, description, category_id, on_homepage, target, session_id
FROM c_link
WHERE c_id = " . $course['id'] . "
AND (session_id = 0 OR session_id IS NULL)
ORDER BY url, title, iid";
$res2 = Database::query($sql2);

if ($res2 === false) {
die("Error querying links in course code " . $course['code'] . "\n");
}

$links = [];
while ($item = Database::fetch_assoc($res2)) {
$links[] = $item;
}

// Track processed duplicates to avoid redundant operations
$processedDuplicates = [];

foreach ($links as $key => $original) {
$originalsCount++;
foreach ($links as $key2 => $duplicate) {
if (
$key !== $key2 &&
!in_array($duplicate['iid'], $processedDuplicates) &&
$original['url'] === $duplicate['url'] &&
$original['title'] === $duplicate['title'] &&
$original['description'] === $duplicate['description'] &&
$original['category_id'] === $duplicate['category_id'] &&
$original['on_homepage'] === $duplicate['on_homepage'] &&
$original['target'] === $duplicate['target'] &&
$original['session_id'] === $duplicate['session_id'] &&
$original['iid'] < $duplicate['iid']
) {
$duplicatesCount++;
if ($debug) {
echo "\nDuplicate found in Course ID: " . $course['id'] . "\n";
echo "Original IID=" . $original['iid'] . ", Duplicate IID=" . $duplicate['iid'] . "\n";
}

// Check if duplicate exists in c_lp_item
$checkSql = "SELECT COUNT(*) as count FROM c_lp_item
WHERE ref = " . $duplicate['iid'] . "
AND c_id = " . $course['id'] . "
AND item_type = 'link'";
$checkResult = Database::query($checkSql);
$row = Database::fetch_assoc($checkResult);

if ($row['count'] > 0) {
$itemsInLP++;
if ($debug) {
echo "Duplicate in learning path: IID=" . $duplicate['iid'] . " (Original IID=" . $original['iid'] . ")\n";
}
continue; // Skip duplicates in learning paths
}

// Delete the duplicate
Link::deletelinkcategory($duplicate['iid'], 'link', $course['id'], true);
$deletedCount++;
$processedDuplicates[] = $duplicate['iid']; // Mark as processed
if ($debug) {
echo "Deleted Duplicate IID=" . $duplicate['iid'] . "\n";
}
}
}
}
}

// Summary
echo "\nSummary:\n";
echo "- Total duplicates detected: $duplicatesCount\n";
echo "- Duplicates ignored (in learning paths): $itemsInLP\n";
echo "- Duplicates deleted: $deletedCount\n";

echo "[" . time() . "] Process complete.\n";
Loading