diff --git a/public/main/gradebook/gradebook_display_certificate.php b/public/main/gradebook/gradebook_display_certificate.php
index cc3d0c7b26f..a6741c4ba10 100644
--- a/public/main/gradebook/gradebook_display_certificate.php
+++ b/public/main/gradebook/gradebook_display_certificate.php
@@ -336,43 +336,109 @@ function confirmation() {
echo '
'.get_lang('Date').' : '.api_convert_and_format_date($valueCertificate['created_at']).' | ';
echo '';
- // Normalize and availability checks
- $pathRaw = isset($valueCertificate['path_certificate']) ? (string) $valueCertificate['path_certificate'] : '';
- $path = ltrim($pathRaw, '/'); // ensure no leading slash
- $hasPath = $path !== '';
- $hash = $hasPath ? pathinfo($path, PATHINFO_FILENAME) : '';
-
- // Admin can bypass publish flag for visibility
+ /**
+ * Resource-first per-course certificate resolution.
+ * - First: try to resolve a Resource certificate for (cat_id, user_id).
+ * - Never fallback to the user's general/custom certificate here.
+ * - If no Resource exists, use legacy path_certificate (HTML/PDF in /certificates/).
+ * - Keep publish flag behavior; platform admins bypass publish.
+ */
$isPublished = !empty($valueCertificate['publish']) || api_is_platform_admin();
- $isAvailable = $hasPath && $isPublished;
- // Build URLs only if available
- $htmlUrl = $isAvailable ? api_get_path(WEB_PATH).'certificates/'.$hash.'.html' : '';
- $pdfUrl = $isAvailable ? api_get_path(WEB_PATH).'certificates/'.$hash.'.pdf' : '';
+ $certRepo = Container::getGradeBookCertificateRepository();
+ $router = null;
+ try {
+ $router = Container::getRouter(); // Might not exist in some installs; guarded below.
+ } catch (\Throwable $e) {
+ // Non-fatal. We'll just skip route generation if router is not available.
+ }
+
+ $htmlUrl = '';
+ $pdfUrl = '';
+ $isAvailable = false;
+
+ // Try to resolve the per-category (course/session) resource
+ try {
+ $entity = $certRepo->getCertificateByUserId(
+ $categoryId === 0 ? null : (int) $categoryId,
+ (int) $value['user_id']
+ );
+
+ if ($entity && $entity->hasResourceNode()) {
+ // HTML is served through the Resource layer (secured, hashed filename)
+ $htmlUrl = (string) $certRepo->getResourceFileUrl($entity);
+ $isAvailable = $isPublished && $htmlUrl !== '';
+
+ // PDF is served by your Symfony controller (update the route name/params if needed)
+ if ($router && $isAvailable) {
+ // Attempt 1: route by certificateId (common signature)
+ try {
+ $pdfUrl = $router->generate('gradebook_certificate_pdf', [
+ 'certificateId' => (int) $valueCertificate['id'],
+ ]);
+ } catch (\Throwable $e1) {
+ // Attempt 2: route by userId+catId (alternative signature)
+ try {
+ $pdfUrl = $router->generate('gradebook_certificate_pdf', [
+ 'userId' => (int) $value['user_id'],
+ 'catId' => (int) $categoryId,
+ ]);
+ } catch (\Throwable $e2) {
+ // Route not found or wrong signature: leave $pdfUrl empty.
+ error_log('[gradebook_display_certificate] PDF route resolution failed: '.$e2->getMessage());
+ }
+ }
+ }
+ }
+ } catch (\Throwable $e) {
+ error_log('[gradebook_display_certificate] resource resolve error: '.$e->getMessage());
+ }
+
+ // Legacy per-course fallback ONLY if no resource is available.
+ // IMPORTANT: do NOT fallback to general/custom certificate on this screen.
+ if (!$isAvailable) {
+ $pathRaw = isset($valueCertificate['path_certificate']) ? (string) $valueCertificate['path_certificate'] : '';
+ $path = ltrim($pathRaw, '/'); // normalize: remove leading slash if present
+ $hasPath = $path !== '';
+ $hash = $hasPath ? pathinfo($path, PATHINFO_FILENAME) : '';
+
+ $isAvailable = $hasPath && $isPublished;
+
+ if ($isAvailable) {
+ $htmlUrl = api_get_path(WEB_PATH).'certificates/'.$hash.'.html';
+ $pdfUrl = api_get_path(WEB_PATH).'certificates/'.$hash.'.pdf';
+ }
+ }
- // HTML certificate button/link
+ // Render buttons (enabled/disabled) preserving existing UI
if ($isAvailable) {
+ // HTML certificate button/link
echo Display::url(
get_lang('Certificate'),
$htmlUrl,
['target' => '_blank', 'class' => 'btn btn--plain']
);
+
+ // PDF download icon/link (only if we have a URL)
+ if (!empty($pdfUrl)) {
+ echo Display::url(
+ Display::getMdiIcon(ActionIcon::EXPORT_PDF, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Download')),
+ $pdfUrl,
+ ['target' => '_blank', 'title' => 'Download PDF certificate']
+ );
+ } else {
+ // Route not available: show disabled icon with a clear tooltip
+ echo '';
+ }
} else {
- // Disabled button with clear message
+ // Disabled HTML button
echo '';
- }
- echo PHP_EOL;
- // PDF download button/link (mdi icon)
- if ($isAvailable) {
- echo Display::url(
- Display::getMdiIcon(ActionIcon::EXPORT_PDF, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Download')),
- $pdfUrl,
- ['target' => '_blank', 'title' => 'Download PDF certificate']
- );
- } else {
- // Disabled icon with tooltip
+ // Disabled PDF icon
echo ' |