Skip to content

Commit 0b3108e

Browse files
Merge pull request #7170 from christianbeeznest/fixes-updates199
Course: Fix document import hierarchy and hide certificates system folder
2 parents 35a819f + c276235 commit 0b3108e

File tree

7 files changed

+311
-38
lines changed

7 files changed

+311
-38
lines changed

public/main/inc/lib/document.lib.php

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3263,6 +3263,9 @@ public static function addDocument(
32633263

32643264
// Document already exists
32653265
if (null !== $document) {
3266+
// Keep the contextual tree consistent: set ResourceLink.parent when needed.
3267+
self::syncResourceLinkParentForContext($document, $parentResource, $courseEntity, $session, $group);
3268+
32663269
return $document;
32673270
}
32683271

@@ -3281,6 +3284,9 @@ public static function addDocument(
32813284
$em->persist($document);
32823285
$em->flush();
32833286

3287+
// Ensure contextual hierarchy (course/session/group) uses ResourceLink.parent.
3288+
self::syncResourceLinkParentForContext($document, $parentResource, $courseEntity, $session, $group);
3289+
32843290
$repo = Container::getDocumentRepository();
32853291
if (!empty($content)) {
32863292
$repo->addFileFromString($document, $title, 'text/html', $content, true);
@@ -3318,4 +3324,108 @@ public static function addDocument(
33183324

33193325
return false;
33203326
}
3327+
3328+
private static function syncResourceLinkParentForContext(
3329+
CDocument $child,
3330+
$parentResource,
3331+
$courseEntity,
3332+
$session,
3333+
$group
3334+
): void {
3335+
// Only set a parent link when the parent is another document (folder).
3336+
if (!$parentResource instanceof CDocument) {
3337+
return; // Root items must keep rl.parent = NULL
3338+
}
3339+
3340+
if (!method_exists($child, 'getResourceNode') || !method_exists($parentResource, 'getResourceNode')) {
3341+
return;
3342+
}
3343+
3344+
$childNode = $child->getResourceNode();
3345+
$parentNode = $parentResource->getResourceNode();
3346+
3347+
if (!$childNode instanceof ResourceNode || !$parentNode instanceof ResourceNode) {
3348+
return;
3349+
}
3350+
3351+
try {
3352+
$em = Database::getManager();
3353+
3354+
$childLink = self::findResourceLinkForContext($em, $childNode, $courseEntity, $session, $group);
3355+
$parentLink = self::findResourceLinkForContext($em, $parentNode, $courseEntity, $session, $group);
3356+
3357+
if (!$childLink instanceof ResourceLink || !$parentLink instanceof ResourceLink) {
3358+
// If a link is missing, we don't hard-fail the import.
3359+
error_log('[IMPORT] ResourceLink not found for context when syncing parent.', [
3360+
'childNodeId' => method_exists($childNode, 'getId') ? $childNode->getId() : null,
3361+
'parentNodeId' => method_exists($parentNode, 'getId') ? $parentNode->getId() : null,
3362+
]);
3363+
3364+
return;
3365+
}
3366+
3367+
$currentParent = method_exists($childLink, 'getParent') ? $childLink->getParent() : null;
3368+
3369+
// Avoid useless flushes
3370+
if ($currentParent && method_exists($currentParent, 'getId') && method_exists($parentLink, 'getId')) {
3371+
if ((int) $currentParent->getId() === (int) $parentLink->getId()) {
3372+
return;
3373+
}
3374+
}
3375+
3376+
$childLink->setParent($parentLink);
3377+
$em->persist($childLink);
3378+
$em->flush();
3379+
} catch (\Throwable $e) {
3380+
error_log('[IMPORT] Failed to sync ResourceLink.parent for context: '.$e->getMessage());
3381+
}
3382+
}
3383+
3384+
private static function findResourceLinkForContext(
3385+
$em,
3386+
ResourceNode $node,
3387+
$courseEntity,
3388+
$session,
3389+
$group
3390+
): ?ResourceLink {
3391+
try {
3392+
$qb = $em->createQueryBuilder()
3393+
->select('rl')
3394+
->from(ResourceLink::class, 'rl')
3395+
->andWhere('rl.resourceNode = :node')
3396+
->setParameter('node', $node);
3397+
3398+
// Course context
3399+
if ($courseEntity) {
3400+
$qb->andWhere('rl.course = :course')->setParameter('course', $courseEntity);
3401+
} else {
3402+
$qb->andWhere('rl.course IS NULL');
3403+
}
3404+
3405+
// Session context
3406+
if ($session) {
3407+
$qb->andWhere('rl.session = :session')->setParameter('session', $session);
3408+
} else {
3409+
$qb->andWhere('rl.session IS NULL');
3410+
}
3411+
3412+
// Group context
3413+
if ($group) {
3414+
$qb->andWhere('rl.group = :group')->setParameter('group', $group);
3415+
} else {
3416+
$qb->andWhere('rl.group IS NULL');
3417+
}
3418+
3419+
$qb->setMaxResults(1);
3420+
3421+
/** @var ResourceLink|null $link */
3422+
$link = $qb->getQuery()->getOneOrNullResult();
3423+
3424+
return $link;
3425+
} catch (\Throwable $e) {
3426+
error_log('[IMPORT] Failed to find ResourceLink for context: '.$e->getMessage());
3427+
3428+
return null;
3429+
}
3430+
}
33213431
}

src/CoreBundle/Controller/Api/UpdateVisibilityDocument.php

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,27 +13,37 @@
1313
use Chamilo\CourseBundle\Repository\CDocumentRepository;
1414
use Doctrine\ORM\EntityManagerInterface;
1515
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
16+
use Symfony\Component\HttpFoundation\RequestStack;
1617
use Symfony\Component\HttpKernel\Attribute\AsController;
18+
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
1719

1820
#[AsController]
1921
class UpdateVisibilityDocument extends AbstractController
2022
{
2123
public function __construct(
2224
private readonly CidReqHelper $cidReqHelper,
2325
private readonly EntityManagerInterface $em,
26+
private readonly RequestStack $requestStack,
2427
) {}
2528

2629
public function __invoke(CDocument $document, CDocumentRepository $repo): CDocument
2730
{
28-
$course = $this->cidReqHelper->getCourseEntity();
29-
$session = $this->cidReqHelper->getSessionEntity();
31+
$request = $this->requestStack->getCurrentRequest();
3032

31-
if ($course) {
32-
$course = $this->em->getRepository(Course::class)->find($course->getId());
33-
}
33+
$cid = $request?->query->getInt('cid', 0) ?? 0;
34+
$sid = $request?->query->getInt('sid', 0) ?? 0;
35+
36+
$courseFromHelper = $this->cidReqHelper->getCourseEntity();
37+
$sessionFromHelper = $this->cidReqHelper->getSessionEntity();
38+
39+
$courseId = $courseFromHelper?->getId() ?? $cid;
40+
$sessionId = $sessionFromHelper?->getId() ?? $sid;
41+
42+
$course = $courseId > 0 ? $this->em->getRepository(Course::class)->find($courseId) : null;
43+
$session = $sessionId > 0 ? $this->em->getRepository(Session::class)->find($sessionId) : null;
3444

35-
if ($session) {
36-
$session = $this->em->getRepository(Session::class)->find($session->getId());
45+
if (null === $course) {
46+
throw new BadRequestHttpException('Course context is required to toggle visibility.');
3747
}
3848

3949
$repo->toggleVisibilityPublishedDraft($document, $course, $session);

src/CoreBundle/Controller/CourseMaintenanceController.php

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,9 @@ public function importUpload(int $node, Request $req): JsonResponse
7777
return $this->json(['error' => 'Invalid upload'], 400);
7878
}
7979

80-
$maxBytes = 1024 * 1024 * 512;
81-
if ($file->getSize() > $maxBytes) {
80+
$maxBytes = self::getPhpUploadLimitBytes();
81+
$fileSize = (int) ($file->getSize() ?? 0);
82+
if ($maxBytes > 0 && $fileSize > $maxBytes) {
8283
return $this->json(['error' => 'File too large'], 413);
8384
}
8485

@@ -4009,4 +4010,61 @@ private function sanitizePhpGraph(mixed $value): mixed
40094010

40104011
return $value;
40114012
}
4013+
4014+
private static function getPhpUploadLimitBytes(): int
4015+
{
4016+
// Use the strictest PHP limit (min of upload_max_filesize and post_max_size).
4017+
$limits = [];
4018+
4019+
$u = ini_get('upload_max_filesize');
4020+
if (is_string($u)) {
4021+
$limits[] = self::iniSizeToBytes($u);
4022+
}
4023+
4024+
$p = ini_get('post_max_size');
4025+
if (is_string($p)) {
4026+
$limits[] = self::iniSizeToBytes($p);
4027+
}
4028+
4029+
// Keep only positive limits. If both are 0, treat as "no limit".
4030+
$limits = array_values(array_filter($limits, static fn (int $v): bool => $v > 0));
4031+
4032+
return empty($limits) ? 0 : min($limits);
4033+
}
4034+
4035+
private static function iniSizeToBytes(string $val): int
4036+
{
4037+
// Parses values like "2G", "512M", "900K", "1048576".
4038+
$val = trim($val);
4039+
if ($val === '') {
4040+
return 0;
4041+
}
4042+
if ($val === '0') {
4043+
return 0; // "no limit" for upload/post.
4044+
}
4045+
4046+
if (!preg_match('/^([0-9]+(?:\.[0-9]+)?)\s*([kmgt])?b?$/i', $val, $m)) {
4047+
return (int) $val;
4048+
}
4049+
4050+
$num = (float) $m[1];
4051+
$unit = strtolower((string) ($m[2] ?? ''));
4052+
4053+
switch ($unit) {
4054+
case 't':
4055+
$num *= 1024;
4056+
// no break
4057+
case 'g':
4058+
$num *= 1024;
4059+
// no break
4060+
case 'm':
4061+
$num *= 1024;
4062+
// no break
4063+
case 'k':
4064+
$num *= 1024;
4065+
break;
4066+
}
4067+
4068+
return (int) round($num);
4069+
}
40124070
}

src/CoreBundle/Helpers/ChamiloHelper.php

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -130,15 +130,13 @@ public static function getPlatformLogoPath(
130130
}
131131

132132
$originalLogoPath = $themeDir.'images/header-logo.png';
133-
if ('true' === $svgIcons) {
134-
$originalLogoPathSVG = $themeDir.'images/header-logo.svg';
135-
if (file_exists(api_get_path(SYS_CSS_PATH).$originalLogoPathSVG)) {
136-
if ($getSysPath) {
137-
return api_get_path(SYS_CSS_PATH).$originalLogoPathSVG;
138-
}
139-
140-
return api_get_path(WEB_CSS_PATH).$originalLogoPathSVG;
133+
$originalLogoPathSVG = $themeDir.'images/header-logo.svg';
134+
if (file_exists(api_get_path(SYS_CSS_PATH).$originalLogoPathSVG)) {
135+
if ($getSysPath) {
136+
return api_get_path(SYS_CSS_PATH).$originalLogoPathSVG;
141137
}
138+
139+
return api_get_path(WEB_CSS_PATH).$originalLogoPathSVG;
142140
}
143141

144142
if (file_exists(api_get_path(SYS_CSS_PATH).$originalLogoPath)) {
@@ -860,11 +858,13 @@ public static function buildUrlMapForHtmlFromPackage(
860858
): array {
861859
$byRel = [];
862860
$byBase = [];
861+
$iidByRel = [];
862+
$iidByBase = [];
863863

864864
$DBG = $dbg ?: static function ($m, $c = []): void { /* no-op */ };
865865

866866
// src|href pointing to …/courses/<dir>/document/... (host optional)
867-
$depRegex = '/(?P<attr>src|href)\s*=\s*["\'](?P<full>(?:(?P<scheme>https?:)?\/\/[^"\']+)?(?P<path>\/courses\/[^\/]+\/document\/[^"\']+\.[a-z0-9]{1,8}))["\']/i';
867+
$depRegex = '/(?P<attr>src|href)\s*=\s*["\'](?P<full>(?:(?P<scheme>https?:)?\/\/[^"\']+)?(?P<path>\/(?:app\/)?courses\/[^\/]+\/document\/[^"\']+\.[a-z0-9]{1,8}))["\']/i';
868868

869869
if (!preg_match_all($depRegex, $html, $mm) || empty($mm['full'])) {
870870
return ['byRel' => $byRel, 'byBase' => $byBase];
@@ -873,14 +873,19 @@ public static function buildUrlMapForHtmlFromPackage(
873873
// Normalize a full URL to a "document/..." relative path inside the package
874874
$toRel = static function (string $full) use ($courseDir): string {
875875
$urlPath = parse_url(html_entity_decode($full, ENT_QUOTES | ENT_HTML5), PHP_URL_PATH) ?: $full;
876-
$urlPath = preg_replace('#^/courses/([^/]+)/#i', '/courses/'.$courseDir.'/', $urlPath);
877-
$rel = preg_replace('#^/courses/'.preg_quote($courseDir, '#').'/#i', '', $urlPath) ?: $urlPath;
876+
$urlPath = preg_replace('#^/(?:app/)?courses/([^/]+)/#i', '/courses/'.$courseDir.'/', $urlPath);
877+
$rel = preg_replace('#^/(?:app/)?courses/'.preg_quote($courseDir, '#').'/#i', '', $urlPath) ?: $urlPath;
878878

879879
return ltrim($rel, '/'); // "document/..."
880880
};
881881

882882
foreach ($mm['full'] as $fullUrl) {
883883
$rel = $toRel($fullUrl); // e.g. "document/img.png"
884+
// Do not auto-create HTML files here (they are handled by the main import loop).
885+
$ext = strtolower(pathinfo($rel, PATHINFO_EXTENSION));
886+
if (in_array($ext, ['html', 'htm'], true)) {
887+
continue;
888+
}
884889
if (!str_starts_with($rel, 'document/')) {
885890
continue;
886891
} // STRICT: only /document/*
@@ -933,6 +938,10 @@ public static function buildUrlMapForHtmlFromPackage(
933938
if ($url) {
934939
$byRel[$rel] = $url;
935940
$byBase[$basename] = $byBase[$basename] ?: $url;
941+
942+
$iidByRel[$rel] = (int) $existsIid;
943+
$iidByBase[$basename] = $iidByBase[$basename] ?? (int) $existsIid;
944+
936945
$DBG('helper.dep.reuse', ['rel' => $rel, 'iid' => $existsIid, 'url' => $url]);
937946
}
938947
}
@@ -971,6 +980,8 @@ public static function buildUrlMapForHtmlFromPackage(
971980
);
972981
$iid = method_exists($entity, 'getIid') ? $entity->getIid() : 0;
973982
$url = $docRepo->getResourceFileUrl($entity);
983+
$iidByRel[$rel] = (int) $iid;
984+
$iidByBase[$basename] = $iidByBase[$basename] ?? (int) $iid;
974985

975986
$DBG('helper.dep.created', ['rel' => $rel, 'iid' => $iid, 'url' => $url]);
976987

@@ -985,7 +996,12 @@ public static function buildUrlMapForHtmlFromPackage(
985996

986997
$byBase = array_filter($byBase);
987998

988-
return ['byRel' => $byRel, 'byBase' => $byBase];
999+
return [
1000+
'byRel' => $byRel,
1001+
'byBase' => $byBase,
1002+
'iidByRel' => $iidByRel,
1003+
'iidByBase' => $iidByBase,
1004+
];
9891005
}
9901006

9911007
/**
@@ -1004,7 +1020,7 @@ public static function rewriteLegacyCourseUrlsWithMap(
10041020
$replaced = 0;
10051021
$misses = 0;
10061022

1007-
$pattern = '/(?P<attr>src|href)\s*=\s*["\'](?P<full>(?:(?P<scheme>https?:)?\/\/[^"\']+)?(?P<path>\/courses\/(?P<dir>[^\/]+)\/document\/[^"\']+\.[a-z0-9]{1,8}))["\']/i';
1023+
$pattern = '/(?P<attr>src|href)\s*=\s*["\'](?P<full>(?:(?P<scheme>https?:)?\/\/[^"\']+)?(?P<path>\/(?:app\/)?courses\/(?P<dir>[^\/]+)\/document\/[^"\']+\.[a-z0-9]{1,8}))["\']/i';
10081024

10091025
$html = preg_replace_callback($pattern, function ($m) use ($courseDir, $urlMapByRel, $urlMapByBase, &$replaced, &$misses) {
10101026
$attr = $m['attr'];
@@ -1015,11 +1031,19 @@ public static function rewriteLegacyCourseUrlsWithMap(
10151031
// Normalize to current course directory
10161032
$effectivePath = $path;
10171033
if (0 !== strcasecmp($matchDir, $courseDir)) {
1018-
$effectivePath = preg_replace('#^/courses/'.preg_quote($matchDir, '#').'/#i', '/courses/'.$courseDir.'/', $path) ?: $path;
1034+
$effectivePath = preg_replace(
1035+
'#^/(?:app/)?courses/'.preg_quote($matchDir, '#').'/#i',
1036+
'/courses/'.$courseDir.'/',
1037+
$path
1038+
) ?: $path;
10191039
}
10201040

1021-
// "document/...."
1022-
$relInPackage = preg_replace('#^/courses/'.preg_quote($courseDir, '#').'/#i', '', $effectivePath) ?: $effectivePath;
1041+
$relInPackage = preg_replace(
1042+
'#^/(?:app/)?courses/'.preg_quote($courseDir, '#').'/#i',
1043+
'',
1044+
$effectivePath
1045+
) ?: $effectivePath;
1046+
10231047
$relInPackage = ltrim($relInPackage, '/'); // document/...
10241048

10251049
// 1) exact rel match

0 commit comments

Comments
 (0)