Merge pull request #6031 from christianbeeznest/escuela-22323

Internal: Fix duplicate links handling with improved LP checks and deletion logic - refs BT#22323
pull/6033/merge
Yannick Warnier 8 months ago committed by GitHub
commit 13dbffa620
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 115
      main/inc/lib/link.lib.php
  2. 2
      main/link/link.php
  3. 140
      tests/scripts/delete_duplicate_links.php

@ -383,22 +383,14 @@ class Link extends Model
/**
* Used to delete a link or a category.
*
* @author Patrick Cool <patrick.cool@UGent.be>, 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;
@ -407,45 +399,74 @@ class Link extends Model
$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;
}

@ -88,7 +88,7 @@ if ($action === 'editlink') {
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'] : '';

@ -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 <yannick.warnier@beeznest.com>
*/
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…
Cancel
Save