Skip to content

Commit 690f510

Browse files
committed
Merge remote-tracking branch 'origin/master'
# Conflicts: # src/CoreBundle/Security/Authorization/Voter/ResourceNodeVoter.php
2 parents acad212 + 577984b commit 690f510

File tree

4 files changed

+22879
-10148
lines changed

4 files changed

+22879
-10148
lines changed

src/CoreBundle/Helpers/PageHelper.php

Lines changed: 85 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,34 @@
1212
use Chamilo\CoreBundle\Entity\User;
1313
use Chamilo\CoreBundle\Repository\PageCategoryRepository;
1414
use Chamilo\CoreBundle\Repository\PageRepository;
15+
use Chamilo\CoreBundle\Repository\SysAnnouncementRepository;
16+
use Symfony\Component\Security\Core\User\UserInterface;
1517

1618
class PageHelper
1719
{
1820
protected PageRepository $pageRepository;
1921
protected PageCategoryRepository $pageCategoryRepository;
2022

21-
public function __construct(PageRepository $pageRepository, PageCategoryRepository $pageCategoryRepository)
22-
{
23+
/**
24+
* Repository used to read system announcements (platform news).
25+
*/
26+
protected SysAnnouncementRepository $sysAnnouncementRepository;
27+
28+
/**
29+
* Helper used to retrieve the current AccessUrl.
30+
*/
31+
protected AccessUrlHelper $accessUrlHelper;
32+
33+
public function __construct(
34+
PageRepository $pageRepository,
35+
PageCategoryRepository $pageCategoryRepository,
36+
SysAnnouncementRepository $sysAnnouncementRepository,
37+
AccessUrlHelper $accessUrlHelper
38+
) {
2339
$this->pageRepository = $pageRepository;
2440
$this->pageCategoryRepository = $pageCategoryRepository;
41+
$this->sysAnnouncementRepository = $sysAnnouncementRepository;
42+
$this->accessUrlHelper = $accessUrlHelper;
2543
}
2644

2745
public function createDefaultPages(User $user, AccessUrl $url, string $locale): bool
@@ -99,8 +117,7 @@ public function createDefaultPages(User $user, AccessUrl $url, string $locale):
99117

100118
$this->pageCategoryRepository->update($footerPrivateCategory);
101119

102-
// Categories for extra content in admin blocks
103-
120+
// Categories for extra content in admin blocks.
104121
foreach (self::getCategoriesForAdminBlocks() as $nameBlock) {
105122
$usersAdminBlock = (new PageCategory())
106123
->setTitle($nameBlock)
@@ -142,4 +159,68 @@ public static function getCategoriesForAdminBlocks(): array
142159
'block-admin-chamilo',
143160
];
144161
}
162+
163+
/**
164+
* Checks if a document file URL is effectively exposed through a visible system announcement.
165+
*
166+
* This centralizes the logic used by different parts of the platform (e.g. voters, controllers)
167+
* to decide if a file coming from personal files can be considered "public" because it is
168+
* embedded inside a system announcement that is visible to the current user.
169+
*
170+
* @param string $pathInfo Full request path (e.g. /r/document/files/{uuid}/view)
171+
* @param string|null $identifier File identifier extracted from the URL (usually a UUID)
172+
* @param UserInterface|null $user Current user, or null to behave as anonymous
173+
* @param string $locale Current locale used to fetch announcements
174+
*/
175+
public function isFilePathExposedByVisibleAnnouncement(
176+
string $pathInfo,
177+
?string $identifier,
178+
?UserInterface $user,
179+
string $locale
180+
): bool {
181+
// Only relax security for the document file viewer route.
182+
if ('' === $pathInfo || !str_contains($pathInfo, '/r/document/files/')) {
183+
return false;
184+
}
185+
186+
// Normalize user: if no authenticated user is provided, behave as anonymous.
187+
if (null === $user) {
188+
$anon = new User();
189+
$anon->setRoles(['ROLE_ANONYMOUS']);
190+
$user = $anon;
191+
}
192+
193+
$accessUrl = $this->accessUrlHelper->getCurrent();
194+
195+
// Fetch announcements that are visible for the given user, URL and locale.
196+
$announcements = $this->sysAnnouncementRepository->getAnnouncements(
197+
$user,
198+
$accessUrl,
199+
$locale
200+
);
201+
202+
foreach ($announcements as $item) {
203+
$content = '';
204+
205+
if (\is_array($item)) {
206+
$content = (string) ($item['content'] ?? '');
207+
} elseif (\is_object($item) && method_exists($item, 'getContent')) {
208+
$content = (string) $item->getContent();
209+
}
210+
211+
if ('' === $content) {
212+
continue;
213+
}
214+
215+
// Check if the announcement HTML contains the viewer path or the identifier.
216+
if (
217+
str_contains($content, $pathInfo)
218+
|| ($identifier && str_contains($content, $identifier))
219+
) {
220+
return true;
221+
}
222+
}
223+
224+
return false;
225+
}
145226
}

src/CoreBundle/Security/Authorization/Voter/ResourceNodeVoter.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use Chamilo\CoreBundle\Entity\ResourceNode;
1212
use Chamilo\CoreBundle\Entity\ResourceRight;
1313
use Chamilo\CoreBundle\Entity\Session;
14+
use Chamilo\CoreBundle\Helpers\PageHelper;
1415
use Chamilo\CoreBundle\Helpers\ResourceAclHelper;
1516
use Chamilo\CoreBundle\Settings\SettingsManager;
1617
use Chamilo\CourseBundle\Entity\CDocument;
@@ -53,6 +54,7 @@ public function __construct(
5354
private RequestStack $requestStack,
5455
private SettingsManager $settingsManager,
5556
private EntityManagerInterface $entityManager,
57+
private PageHelper $pageHelper,
5658
private readonly ResourceAclHelper $resourceAclHelper,
5759
) {}
5860

@@ -119,6 +121,11 @@ protected function voteOnAttribute(string $attribute, $subject, TokenInterface $
119121
return true;
120122
}
121123

124+
// Special case: allow file assets that are embedded inside a visible system announcement.
125+
if (self::VIEW === $attribute && $this->isAnnouncementFileVisibleForCurrentRequest($resourceNode, $token)) {
126+
return true;
127+
}
128+
122129
// @todo
123130
switch ($attribute) {
124131
case self::VIEW:
@@ -450,6 +457,47 @@ protected function voteOnAttribute(string $attribute, $subject, TokenInterface $
450457
return $this->resourceAclHelper->isAllowed($attribute, $link, $rights, $allowAnonsToView);
451458
}
452459

460+
/**
461+
* Checks if the current request is viewing a document file that is embedded
462+
* inside a visible system announcement, delegating the heavy logic to PageHelper.
463+
*/
464+
private function isAnnouncementFileVisibleForCurrentRequest(ResourceNode $resourceNode, TokenInterface $token): bool
465+
{
466+
$type = $resourceNode->getResourceType()?->getTitle();
467+
if ('files' !== $type) {
468+
return false;
469+
}
470+
471+
$request = $this->requestStack->getCurrentRequest();
472+
if (null === $request) {
473+
return false;
474+
}
475+
476+
$pathInfo = (string) $request->getPathInfo();
477+
if ('' === $pathInfo) {
478+
return false;
479+
}
480+
481+
// Extract file identifier from /r/document/files/{identifier}/view.
482+
$segments = explode('/', trim($pathInfo, '/'));
483+
$identifier = null;
484+
if (\count($segments) >= 4) {
485+
// ... /r/document/files/{identifier}/view
486+
$identifier = $segments[\count($segments) - 2] ?? null;
487+
}
488+
489+
$userFromToken = $token->getUser();
490+
$user = $userFromToken instanceof UserInterface ? $userFromToken : null;
491+
$locale = $request->getLocale();
492+
493+
return $this->pageHelper->isFilePathExposedByVisibleAnnouncement(
494+
$pathInfo,
495+
\is_string($identifier) ? $identifier : null,
496+
$user,
497+
$locale
498+
);
499+
}
500+
453501
private function isBlogResource(ResourceNode $node): bool
454502
{
455503
$type = $node->getResourceType()?->getTitle();

0 commit comments

Comments
 (0)