Skip to content

Commit 674a4d3

Browse files
committed
fix: proxy images for shared access
1 parent 6cf31e2 commit 674a4d3

17 files changed

Lines changed: 574 additions & 94 deletions

lib/Controller/ChecklistController.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,7 @@ public function uploadItemImage(int $houseId, int $listId, int $itemId): DataRes
419419
$original = (string)($data['name'] ?? 'image.jpg');
420420
$fileId = $this->images->uploadForUser($uid, $houseId, $original, $bytes);
421421

422-
$updated = $this->lists->updateItem($itemId, ['imageFileId' => $fileId]);
422+
$updated = $this->lists->updateItem($itemId, ['imageFileId' => $fileId, 'imageUploadedBy' => $uid]);
423423
return new DataResponse($updated->jsonSerialize());
424424
});
425425
}
@@ -447,7 +447,7 @@ public function clearItemImage(int $houseId, int $listId, int $itemId): DataResp
447447
if ($item->getListId() !== $listId) {
448448
throw new NotFoundException('Item does not belong to this list');
449449
}
450-
$updated = $this->lists->updateItem($itemId, ['imageFileId' => null]);
450+
$updated = $this->lists->updateItem($itemId, ['imageFileId' => null, 'imageUploadedBy' => null]);
451451
return new DataResponse($updated->jsonSerialize());
452452
});
453453
}

lib/Controller/ImageController.php

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
// SPDX-FileCopyrightText: Chen Asraf <contact@casraf.dev>
6+
// SPDX-License-Identifier: AGPL-3.0-or-later
7+
8+
namespace OCA\Pantry\Controller;
9+
10+
use OCA\Pantry\Db\PhotoMapper;
11+
use OCA\Pantry\Exception\ForbiddenException;
12+
use OCA\Pantry\Exception\NotFoundException;
13+
use OCA\Pantry\Service\HouseAuthService;
14+
use OCP\AppFramework\Http;
15+
use OCP\AppFramework\Http\Attribute\ApiRoute;
16+
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
17+
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
18+
use OCP\AppFramework\Http\DataResponse;
19+
use OCP\AppFramework\Http\FileDisplayResponse;
20+
use OCP\AppFramework\OCSController;
21+
use OCP\Files\File;
22+
use OCP\Files\IRootFolder;
23+
use OCP\IPreview;
24+
use OCP\IRequest;
25+
use OCP\IUserSession;
26+
27+
/**
28+
* Serve images from the owner's storage so any house member can view them,
29+
* regardless of Nextcloud sharing settings.
30+
*/
31+
final class ImageController extends OCSController {
32+
use TranslatesDomainExceptions;
33+
34+
public function __construct(
35+
string $appName,
36+
IRequest $request,
37+
private HouseAuthService $auth,
38+
private PhotoMapper $photoMapper,
39+
private IRootFolder $rootFolder,
40+
private IPreview $previewManager,
41+
private IUserSession $userSession,
42+
) {
43+
parent::__construct($appName, $request);
44+
}
45+
46+
/**
47+
* Serve a photo board image preview
48+
*
49+
* @param int $houseId House id.
50+
* @param int $photoId Photo record id.
51+
* @param int $size Preview size (longest edge).
52+
*
53+
* @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|DataResponse<Http::STATUS_NOT_FOUND, array{error: string}, array{}>
54+
*
55+
* 200: Preview returned
56+
* 404: Image not found
57+
*/
58+
#[ApiRoute(verb: 'GET', url: '/api/houses/{houseId}/photos/{photoId}/preview')]
59+
#[NoAdminRequired]
60+
#[NoCSRFRequired]
61+
public function photoPreview(int $houseId, int $photoId, int $size = 300): FileDisplayResponse|DataResponse {
62+
return $this->runAction(function () use ($houseId, $photoId, $size) {
63+
$this->auth->requireMember($houseId, $this->requireUid());
64+
65+
$photo = $this->photoMapper->findById($photoId);
66+
if ($photo->getHouseId() !== $houseId) {
67+
throw new NotFoundException('Photo does not belong to this house');
68+
}
69+
70+
return $this->servePreview($photo->getUploadedBy(), $photo->getFileId(), $size);
71+
});
72+
}
73+
74+
/**
75+
* Serve a checklist item image preview
76+
*
77+
* @param int $houseId House id.
78+
* @param int $fileId Nextcloud file id.
79+
* @param string $owner File owner uid.
80+
* @param int $size Preview size (longest edge).
81+
*
82+
* @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|DataResponse<Http::STATUS_NOT_FOUND, array{error: string}, array{}>
83+
*
84+
* 200: Preview returned
85+
* 404: Image not found
86+
*/
87+
#[ApiRoute(verb: 'GET', url: '/api/houses/{houseId}/image-preview')]
88+
#[NoAdminRequired]
89+
#[NoCSRFRequired]
90+
public function itemImagePreview(int $houseId, int $fileId, string $owner, int $size = 300): FileDisplayResponse|DataResponse {
91+
return $this->runAction(function () use ($houseId, $fileId, $owner, $size) {
92+
$this->auth->requireMember($houseId, $this->requireUid());
93+
94+
return $this->servePreview($owner, $fileId, $size);
95+
});
96+
}
97+
98+
private function servePreview(string $ownerUid, int $fileId, int $size): FileDisplayResponse|DataResponse {
99+
$size = max(16, min($size, 2048));
100+
101+
$userFolder = $this->rootFolder->getUserFolder($ownerUid);
102+
$nodes = $userFolder->getById($fileId);
103+
if (empty($nodes)) {
104+
return new DataResponse(['error' => 'File not found'], Http::STATUS_NOT_FOUND);
105+
}
106+
107+
$file = $nodes[0];
108+
if (!$file instanceof File) {
109+
return new DataResponse(['error' => 'Not a file'], Http::STATUS_NOT_FOUND);
110+
}
111+
112+
if ($this->previewManager->isAvailable($file)) {
113+
$preview = $this->previewManager->getPreview($file, $size, $size);
114+
$resp = new FileDisplayResponse($preview, Http::STATUS_OK, [
115+
'Content-Type' => $preview->getMimeType(),
116+
]);
117+
} else {
118+
$resp = new FileDisplayResponse($file, Http::STATUS_OK, [
119+
'Content-Type' => $file->getMimeType(),
120+
]);
121+
}
122+
$resp->cacheFor(3600);
123+
return $resp;
124+
}
125+
126+
private function requireUid(): string {
127+
$user = $this->userSession->getUser();
128+
if ($user === null) {
129+
throw new ForbiddenException('Not authenticated');
130+
}
131+
return $user->getUID();
132+
}
133+
}

lib/Db/ChecklistItem.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
* @method void setNextDueAt(?int $nextDueAt)
3333
* @method int|null getImageFileId()
3434
* @method void setImageFileId(?int $imageFileId)
35+
* @method string|null getImageUploadedBy()
36+
* @method void setImageUploadedBy(?string $imageUploadedBy)
3537
* @method int getSortOrder()
3638
* @method void setSortOrder(int $sortOrder)
3739
* @method int getCreatedAt()
@@ -51,6 +53,7 @@ class ChecklistItem extends Entity implements \JsonSerializable {
5153
protected bool $repeatFromCompletion = false;
5254
protected ?int $nextDueAt = null;
5355
protected ?int $imageFileId = null;
56+
protected ?string $imageUploadedBy = null;
5457
protected int $sortOrder = 0;
5558
protected int $createdAt = 0;
5659
protected int $updatedAt = 0;
@@ -88,6 +91,7 @@ public function jsonSerialize(): array {
8891
'repeatFromCompletion' => $this->repeatFromCompletion,
8992
'nextDueAt' => $this->nextDueAt,
9093
'imageFileId' => $this->imageFileId,
94+
'imageUploadedBy' => $this->imageUploadedBy,
9195
'sortOrder' => $this->sortOrder,
9296
'createdAt' => $this->createdAt,
9397
'updatedAt' => $this->updatedAt,

lib/Migration/Version1Date20260405000000.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,10 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt
220220
'notnull' => false,
221221
'length' => 20,
222222
]);
223+
$table->addColumn('image_uploaded_by', Types::STRING, [
224+
'notnull' => false,
225+
'length' => 64,
226+
]);
223227
$table->addColumn('sort_order', Types::INTEGER, [
224228
'notnull' => true,
225229
'default' => 0,

lib/ResponseDefinitions.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
* repeatFromCompletion: bool,
5252
* nextDueAt: int|null,
5353
* imageFileId: int|null,
54+
* imageUploadedBy: string|null,
5455
* sortOrder: int,
5556
* createdAt: int,
5657
* updatedAt: int,

lib/Service/ChecklistService.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,10 @@ public function updateItem(int $itemId, array $patch): ChecklistItem {
193193
if (array_key_exists('imageFileId', $patch)) {
194194
$item->setImageFileId($this->intOrNull($patch['imageFileId']));
195195
}
196+
if (array_key_exists('imageUploadedBy', $patch)) {
197+
$v = $patch['imageUploadedBy'];
198+
$item->setImageUploadedBy(is_string($v) && $v !== '' ? $v : null);
199+
}
196200
// If already done and rrule or mode changed, recompute next due.
197201
if ($item->getDone() && $item->getRrule() !== null
198202
&& (array_key_exists('rrule', $patch) || array_key_exists('repeatFromCompletion', $patch))) {

0 commit comments

Comments
 (0)