Skip to content

Commit 577984b

Browse files
Merge pull request #7134 from christianbeeznest/fixes-updates194
Internal: Allow files embedded in public announcements to be visible for anonymous users
2 parents 2f54b46 + 2c390b1 commit 577984b

File tree

2 files changed

+134
-5
lines changed

2 files changed

+134
-5
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: 49 additions & 1 deletion
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\Settings\SettingsManager;
1516
use Chamilo\CourseBundle\Entity\CDocument;
1617
use Chamilo\CourseBundle\Entity\CGroup;
@@ -51,7 +52,8 @@ public function __construct(
5152
private Security $security,
5253
private RequestStack $requestStack,
5354
private SettingsManager $settingsManager,
54-
private EntityManagerInterface $entityManager
55+
private EntityManagerInterface $entityManager,
56+
private PageHelper $pageHelper,
5557
) {}
5658

5759
public static function getReaderMask(): int
@@ -117,6 +119,11 @@ protected function voteOnAttribute(string $attribute, $subject, TokenInterface $
117119
return true;
118120
}
119121

122+
// Special case: allow file assets that are embedded inside a visible system announcement.
123+
if (self::VIEW === $attribute && $this->isAnnouncementFileVisibleForCurrentRequest($resourceNode, $token)) {
124+
return true;
125+
}
126+
120127
// @todo
121128
switch ($attribute) {
122129
case self::VIEW:
@@ -516,6 +523,47 @@ protected function voteOnAttribute(string $attribute, $subject, TokenInterface $
516523
return false;
517524
}
518525

526+
/**
527+
* Checks if the current request is viewing a document file that is embedded
528+
* inside a visible system announcement, delegating the heavy logic to PageHelper.
529+
*/
530+
private function isAnnouncementFileVisibleForCurrentRequest(ResourceNode $resourceNode, TokenInterface $token): bool
531+
{
532+
$type = $resourceNode->getResourceType()?->getTitle();
533+
if ('files' !== $type) {
534+
return false;
535+
}
536+
537+
$request = $this->requestStack->getCurrentRequest();
538+
if (null === $request) {
539+
return false;
540+
}
541+
542+
$pathInfo = (string) $request->getPathInfo();
543+
if ('' === $pathInfo) {
544+
return false;
545+
}
546+
547+
// Extract file identifier from /r/document/files/{identifier}/view.
548+
$segments = explode('/', trim($pathInfo, '/'));
549+
$identifier = null;
550+
if (\count($segments) >= 4) {
551+
// ... /r/document/files/{identifier}/view
552+
$identifier = $segments[\count($segments) - 2] ?? null;
553+
}
554+
555+
$userFromToken = $token->getUser();
556+
$user = $userFromToken instanceof UserInterface ? $userFromToken : null;
557+
$locale = $request->getLocale();
558+
559+
return $this->pageHelper->isFilePathExposedByVisibleAnnouncement(
560+
$pathInfo,
561+
\is_string($identifier) ? $identifier : null,
562+
$user,
563+
$locale
564+
);
565+
}
566+
519567
private function isBlogResource(ResourceNode $node): bool
520568
{
521569
$type = $node->getResourceType()?->getTitle();

0 commit comments

Comments
 (0)