Skip to content

Commit efa5632

Browse files
authored
Merge pull request #51458 from nextcloud/fix/fix-public-download-activity
Fix public download activity
2 parents c4ac979 + da9b6e3 commit efa5632

File tree

11 files changed

+298
-106
lines changed

11 files changed

+298
-106
lines changed

.github/workflows/integration-sqlite.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ jobs:
7373

7474
php-versions: ['8.1']
7575
spreed-versions: ['main']
76+
activity-versions: ['master']
7677

7778
services:
7879
redis:
@@ -104,6 +105,14 @@ jobs:
104105
path: apps/spreed
105106
ref: ${{ matrix.spreed-versions }}
106107

108+
- name: Checkout Activity app
109+
if: ${{ matrix.test-suite == 'sharing_features' }}
110+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
111+
with:
112+
repository: nextcloud/activity
113+
path: apps/activity
114+
ref: ${{ matrix.activity-versions }}
115+
107116
- name: Set up php ${{ matrix.php-versions }}
108117
uses: shivammathur/setup-php@9e72090525849c5e82e596468b86eb55e9cc5401 #v2.32.0
109118
with:

apps/files_sharing/composer/composer/autoload_classmap.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
'OCA\\Files_Sharing\\ISharedMountPoint' => $baseDir . '/../lib/ISharedMountPoint.php',
6060
'OCA\\Files_Sharing\\ISharedStorage' => $baseDir . '/../lib/ISharedStorage.php',
6161
'OCA\\Files_Sharing\\Listener\\BeforeDirectFileDownloadListener' => $baseDir . '/../lib/Listener/BeforeDirectFileDownloadListener.php',
62+
'OCA\\Files_Sharing\\Listener\\BeforeNodeReadListener' => $baseDir . '/../lib/Listener/BeforeNodeReadListener.php',
6263
'OCA\\Files_Sharing\\Listener\\BeforeZipCreatedListener' => $baseDir . '/../lib/Listener/BeforeZipCreatedListener.php',
6364
'OCA\\Files_Sharing\\Listener\\LoadAdditionalListener' => $baseDir . '/../lib/Listener/LoadAdditionalListener.php',
6465
'OCA\\Files_Sharing\\Listener\\LoadPublicFileRequestAuthListener' => $baseDir . '/../lib/Listener/LoadPublicFileRequestAuthListener.php',

apps/files_sharing/composer/composer/autoload_static.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ class ComposerStaticInitFiles_Sharing
7474
'OCA\\Files_Sharing\\ISharedMountPoint' => __DIR__ . '/..' . '/../lib/ISharedMountPoint.php',
7575
'OCA\\Files_Sharing\\ISharedStorage' => __DIR__ . '/..' . '/../lib/ISharedStorage.php',
7676
'OCA\\Files_Sharing\\Listener\\BeforeDirectFileDownloadListener' => __DIR__ . '/..' . '/../lib/Listener/BeforeDirectFileDownloadListener.php',
77+
'OCA\\Files_Sharing\\Listener\\BeforeNodeReadListener' => __DIR__ . '/..' . '/../lib/Listener/BeforeNodeReadListener.php',
7778
'OCA\\Files_Sharing\\Listener\\BeforeZipCreatedListener' => __DIR__ . '/..' . '/../lib/Listener/BeforeZipCreatedListener.php',
7879
'OCA\\Files_Sharing\\Listener\\LoadAdditionalListener' => __DIR__ . '/..' . '/../lib/Listener/LoadAdditionalListener.php',
7980
'OCA\\Files_Sharing\\Listener\\LoadPublicFileRequestAuthListener' => __DIR__ . '/..' . '/../lib/Listener/LoadPublicFileRequestAuthListener.php',

apps/files_sharing/lib/AppInfo/Application.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use OCA\Files_Sharing\External\MountProvider as ExternalMountProvider;
1717
use OCA\Files_Sharing\Helper;
1818
use OCA\Files_Sharing\Listener\BeforeDirectFileDownloadListener;
19+
use OCA\Files_Sharing\Listener\BeforeNodeReadListener;
1920
use OCA\Files_Sharing\Listener\BeforeZipCreatedListener;
2021
use OCA\Files_Sharing\Listener\LoadAdditionalListener;
2122
use OCA\Files_Sharing\Listener\LoadPublicFileRequestAuthListener;
@@ -42,6 +43,7 @@
4243
use OCP\Files\Config\IMountProviderCollection;
4344
use OCP\Files\Events\BeforeDirectFileDownloadEvent;
4445
use OCP\Files\Events\BeforeZipCreatedEvent;
46+
use OCP\Files\Events\Node\BeforeNodeReadEvent;
4547
use OCP\Group\Events\GroupChangedEvent;
4648
use OCP\Group\Events\GroupDeletedEvent;
4749
use OCP\Group\Events\UserAddedEvent;
@@ -94,9 +96,13 @@ function () use ($c) {
9496
$context->registerEventListener(ShareCreatedEvent::class, UserShareAcceptanceListener::class);
9597
$context->registerEventListener(UserAddedEvent::class, UserAddedToGroupListener::class);
9698

97-
// Handle download events for view only checks
98-
$context->registerEventListener(BeforeZipCreatedEvent::class, BeforeZipCreatedListener::class);
99-
$context->registerEventListener(BeforeDirectFileDownloadEvent::class, BeforeDirectFileDownloadListener::class);
99+
// Publish activity for public download
100+
$context->registerEventListener(BeforeNodeReadEvent::class, BeforeNodeReadListener::class);
101+
$context->registerEventListener(BeforeZipCreatedEvent::class, BeforeNodeReadListener::class);
102+
103+
// Handle download events for view only checks. Priority higher than 0 to run early.
104+
$context->registerEventListener(BeforeZipCreatedEvent::class, BeforeZipCreatedListener::class, 5);
105+
$context->registerEventListener(BeforeDirectFileDownloadEvent::class, BeforeDirectFileDownloadListener::class, 5);
100106

101107
// File request auth
102108
$context->registerEventListener(BeforeTemplateRenderedEvent::class, LoadPublicFileRequestAuthListener::class);

apps/files_sharing/lib/Controller/ShareController.php

Lines changed: 5 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
use OC\Security\CSP\ContentSecurityPolicy;
1010
use OCA\DAV\Connector\Sabre\PublicAuth;
1111
use OCA\FederatedFileSharing\FederatedShareProvider;
12-
use OCA\Files_Sharing\Activity\Providers\Downloads;
1312
use OCA\Files_Sharing\Event\BeforeTemplateRenderedEvent;
1413
use OCA\Files_Sharing\Event\ShareLinkAccessedEvent;
1514
use OCP\Accounts\IAccountManager;
@@ -28,7 +27,6 @@
2827
use OCP\Files\File;
2928
use OCP\Files\Folder;
3029
use OCP\Files\IRootFolder;
31-
use OCP\Files\Node;
3230
use OCP\Files\NotFoundException;
3331
use OCP\HintException;
3432
use OCP\IConfig;
@@ -368,15 +366,9 @@ public function downloadShare($token, $files = null, $path = '') {
368366
throw new NotFoundException();
369367
}
370368

371-
// Single file share
372-
if ($share->getNode() instanceof File) {
373-
// Single file download
374-
$this->singleFileDownloaded($share, $share->getNode());
375-
}
376-
// Directory share
377-
else {
378-
/** @var Folder $node */
379-
$node = $share->getNode();
369+
$node = $share->getNode();
370+
if ($node instanceof Folder) {
371+
// Directory share
380372

381373
// Try to get the path
382374
if ($path !== '') {
@@ -391,22 +383,10 @@ public function downloadShare($token, $files = null, $path = '') {
391383

392384
if ($node instanceof Folder) {
393385
if ($files === null || $files === '') {
394-
// The folder is downloaded
395-
$this->singleFileDownloaded($share, $share->getNode());
396-
} else {
397-
$fileList = json_decode($files);
398-
// in case we get only a single file
399-
if (!is_array($fileList)) {
400-
$fileList = [$fileList];
401-
}
402-
foreach ($fileList as $file) {
403-
$subNode = $node->get($file);
404-
$this->singleFileDownloaded($share, $subNode);
386+
if ($share->getHideDownload()) {
387+
throw new NotFoundException('Downloading a folder');
405388
}
406389
}
407-
} else {
408-
// Single file download
409-
$this->singleFileDownloaded($share, $share->getNode());
410390
}
411391
}
412392

@@ -419,77 +399,4 @@ public function downloadShare($token, $files = null, $path = '') {
419399
}
420400
return new RedirectResponse($this->urlGenerator->getAbsoluteURL($davUrl));
421401
}
422-
423-
/**
424-
* create activity if a single file was downloaded from a link share
425-
*
426-
* @param Share\IShare $share
427-
* @throws NotFoundException when trying to download a folder of a "hide download" share
428-
*/
429-
protected function singleFileDownloaded(IShare $share, Node $node) {
430-
if ($share->getHideDownload() && $node instanceof Folder) {
431-
throw new NotFoundException('Downloading a folder');
432-
}
433-
434-
$fileId = $node->getId();
435-
436-
$userFolder = $this->rootFolder->getUserFolder($share->getSharedBy());
437-
$userNode = $userFolder->getFirstNodeById($fileId);
438-
$ownerFolder = $this->rootFolder->getUserFolder($share->getShareOwner());
439-
$userPath = $userFolder->getRelativePath($userNode->getPath());
440-
$ownerPath = $ownerFolder->getRelativePath($node->getPath());
441-
$remoteAddress = $this->request->getRemoteAddress();
442-
$dateTime = new \DateTime();
443-
$dateTime = $dateTime->format('Y-m-d H');
444-
$remoteAddressHash = md5($dateTime . '-' . $remoteAddress);
445-
446-
$parameters = [$userPath];
447-
448-
if ($share->getShareType() === IShare::TYPE_EMAIL) {
449-
if ($node instanceof File) {
450-
$subject = Downloads::SUBJECT_SHARED_FILE_BY_EMAIL_DOWNLOADED;
451-
} else {
452-
$subject = Downloads::SUBJECT_SHARED_FOLDER_BY_EMAIL_DOWNLOADED;
453-
}
454-
$parameters[] = $share->getSharedWith();
455-
} else {
456-
if ($node instanceof File) {
457-
$subject = Downloads::SUBJECT_PUBLIC_SHARED_FILE_DOWNLOADED;
458-
$parameters[] = $remoteAddressHash;
459-
} else {
460-
$subject = Downloads::SUBJECT_PUBLIC_SHARED_FOLDER_DOWNLOADED;
461-
$parameters[] = $remoteAddressHash;
462-
}
463-
}
464-
465-
$this->publishActivity($subject, $parameters, $share->getSharedBy(), $fileId, $userPath);
466-
467-
if ($share->getShareOwner() !== $share->getSharedBy()) {
468-
$parameters[0] = $ownerPath;
469-
$this->publishActivity($subject, $parameters, $share->getShareOwner(), $fileId, $ownerPath);
470-
}
471-
}
472-
473-
/**
474-
* publish activity
475-
*
476-
* @param string $subject
477-
* @param array $parameters
478-
* @param string $affectedUser
479-
* @param int $fileId
480-
* @param string $filePath
481-
*/
482-
protected function publishActivity($subject,
483-
array $parameters,
484-
$affectedUser,
485-
$fileId,
486-
$filePath) {
487-
$event = $this->activityManager->generateEvent();
488-
$event->setApp('files_sharing')
489-
->setType('public_links')
490-
->setSubject($subject, $parameters)
491-
->setAffectedUser($affectedUser)
492-
->setObject('files', $fileId, $filePath);
493-
$this->activityManager->publish($event);
494-
}
495402
}
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\Files_Sharing\Listener;
11+
12+
use OCA\Files_Sharing\Activity\Providers\Downloads;
13+
use OCP\EventDispatcher\Event;
14+
use OCP\EventDispatcher\IEventListener;
15+
use OCP\Files\Events\BeforeZipCreatedEvent;
16+
use OCP\Files\Events\Node\BeforeNodeReadEvent;
17+
use OCP\Files\File;
18+
use OCP\Files\Folder;
19+
use OCP\Files\IRootFolder;
20+
use OCP\Files\NotFoundException;
21+
use OCP\Files\Storage\ISharedStorage;
22+
use OCP\ICache;
23+
use OCP\ICacheFactory;
24+
use OCP\IRequest;
25+
use OCP\ISession;
26+
use OCP\Share\IShare;
27+
28+
/**
29+
* @template-implements IEventListener<BeforeNodeReadEvent|BeforeZipCreatedEvent|Event>
30+
*/
31+
class BeforeNodeReadListener implements IEventListener {
32+
private ICache $cache;
33+
34+
public function __construct(
35+
private ISession $session,
36+
private IRootFolder $rootFolder,
37+
private \OCP\Activity\IManager $activityManager,
38+
private IRequest $request,
39+
ICacheFactory $cacheFactory,
40+
) {
41+
$this->cache = $cacheFactory->createDistributed('files_sharing_activity_events');
42+
}
43+
44+
public function handle(Event $event): void {
45+
if ($event instanceof BeforeZipCreatedEvent) {
46+
$this->handleBeforeZipCreatedEvent($event);
47+
} elseif ($event instanceof BeforeNodeReadEvent) {
48+
$this->handleBeforeNodeReadEvent($event);
49+
}
50+
}
51+
52+
public function handleBeforeZipCreatedEvent(BeforeZipCreatedEvent $event): void {
53+
$files = $event->getFiles();
54+
if (count($files) !== 0) {
55+
/* No need to do anything, activity will be triggered for each file in the zip by the BeforeNodeReadEvent */
56+
return;
57+
}
58+
59+
$node = $event->getFolder();
60+
if (!($node instanceof Folder)) {
61+
return;
62+
}
63+
64+
try {
65+
$storage = $node->getStorage();
66+
} catch (NotFoundException) {
67+
return;
68+
}
69+
70+
if (!$storage->instanceOfStorage(ISharedStorage::class)) {
71+
return;
72+
}
73+
74+
/** @var ISharedStorage $storage */
75+
$share = $storage->getShare();
76+
77+
if (!in_array($share->getShareType(), [IShare::TYPE_EMAIL, IShare::TYPE_LINK])) {
78+
return;
79+
}
80+
81+
/* Cache that that folder download activity was published */
82+
$this->cache->set($this->request->getId(), $node->getPath(), 3600);
83+
84+
$this->singleFileDownloaded($share, $node);
85+
}
86+
87+
public function handleBeforeNodeReadEvent(BeforeNodeReadEvent $event): void {
88+
$node = $event->getNode();
89+
if (!($node instanceof File)) {
90+
return;
91+
}
92+
93+
try {
94+
$storage = $node->getStorage();
95+
} catch (NotFoundException) {
96+
return;
97+
}
98+
99+
if (!$storage->instanceOfStorage(ISharedStorage::class)) {
100+
return;
101+
}
102+
103+
/** @var ISharedStorage $storage */
104+
$share = $storage->getShare();
105+
106+
if (!in_array($share->getShareType(), [IShare::TYPE_EMAIL, IShare::TYPE_LINK])) {
107+
return;
108+
}
109+
110+
$path = $this->cache->get($this->request->getId());
111+
if (is_string($path) && str_starts_with($node->getPath(), $path)) {
112+
/* An activity was published for a containing folder already */
113+
return;
114+
}
115+
116+
/* Avoid publishing several activities for one video playing */
117+
$cacheKey = $node->getId() . $node->getPath() . $this->session->getId();
118+
if (($this->request->getHeader('range') !== '') && ($this->cache->get($cacheKey) === 'true')) {
119+
/* This is a range request and an activity for the same file was published in the same session */
120+
return;
121+
}
122+
$this->cache->set($cacheKey, 'true', 3600);
123+
124+
$this->singleFileDownloaded($share, $node);
125+
}
126+
127+
/**
128+
* create activity if a single file or folder was downloaded from a link share
129+
*/
130+
protected function singleFileDownloaded(IShare $share, File|Folder $node): void {
131+
$fileId = $node->getId();
132+
133+
$userFolder = $this->rootFolder->getUserFolder($share->getSharedBy());
134+
$userNode = $userFolder->getFirstNodeById($fileId);
135+
$ownerFolder = $this->rootFolder->getUserFolder($share->getShareOwner());
136+
$userPath = $userFolder->getRelativePath($userNode?->getPath() ?? '') ?? '';
137+
$ownerPath = $ownerFolder->getRelativePath($node->getPath()) ?? '';
138+
139+
$parameters = [$userPath];
140+
141+
if ($share->getShareType() === IShare::TYPE_EMAIL) {
142+
if ($node instanceof File) {
143+
$subject = Downloads::SUBJECT_SHARED_FILE_BY_EMAIL_DOWNLOADED;
144+
} else {
145+
$subject = Downloads::SUBJECT_SHARED_FOLDER_BY_EMAIL_DOWNLOADED;
146+
}
147+
$parameters[] = $share->getSharedWith();
148+
} elseif ($share->getShareType() === IShare::TYPE_LINK) {
149+
if ($node instanceof File) {
150+
$subject = Downloads::SUBJECT_PUBLIC_SHARED_FILE_DOWNLOADED;
151+
} else {
152+
$subject = Downloads::SUBJECT_PUBLIC_SHARED_FOLDER_DOWNLOADED;
153+
}
154+
$remoteAddress = $this->request->getRemoteAddress();
155+
$dateTime = new \DateTime();
156+
$dateTime = $dateTime->format('Y-m-d H');
157+
$remoteAddressHash = md5($dateTime . '-' . $remoteAddress);
158+
$parameters[] = $remoteAddressHash;
159+
} else {
160+
return;
161+
}
162+
163+
$this->publishActivity($subject, $parameters, $share->getSharedBy(), $fileId, $userPath);
164+
165+
if ($share->getShareOwner() !== $share->getSharedBy()) {
166+
$parameters[0] = $ownerPath;
167+
$this->publishActivity($subject, $parameters, $share->getShareOwner(), $fileId, $ownerPath);
168+
}
169+
}
170+
171+
/**
172+
* publish activity
173+
*/
174+
protected function publishActivity(
175+
string $subject,
176+
array $parameters,
177+
string $affectedUser,
178+
int $fileId,
179+
string $filePath,
180+
): void {
181+
$event = $this->activityManager->generateEvent();
182+
$event->setApp('files_sharing')
183+
->setType('public_links')
184+
->setSubject($subject, $parameters)
185+
->setAffectedUser($affectedUser)
186+
->setObject('files', $fileId, $filePath);
187+
$this->activityManager->publish($event);
188+
}
189+
}

0 commit comments

Comments
 (0)