Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file removed apps/profile/.noopenapi
Empty file.
1 change: 1 addition & 0 deletions apps/profile/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
return array(
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
'OCA\\Profile\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php',
'OCA\\Profile\\Controller\\ProfileApiController' => $baseDir . '/../lib/Controller/ProfileApiController.php',
'OCA\\Profile\\Controller\\ProfilePageController' => $baseDir . '/../lib/Controller/ProfilePageController.php',
'OCA\\Profile\\Listener\\LoadAdditionalEntriesListener' => $baseDir . '/../lib/Listener/LoadAdditionalEntriesListener.php',
'OCA\\Profile\\Listener\\ProfilePickerReferenceListener' => $baseDir . '/../lib/Listener/ProfilePickerReferenceListener.php',
Expand Down
1 change: 1 addition & 0 deletions apps/profile/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class ComposerStaticInitProfile
public static $classMap = array (
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'OCA\\Profile\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php',
'OCA\\Profile\\Controller\\ProfileApiController' => __DIR__ . '/..' . '/../lib/Controller/ProfileApiController.php',
'OCA\\Profile\\Controller\\ProfilePageController' => __DIR__ . '/..' . '/../lib/Controller/ProfilePageController.php',
'OCA\\Profile\\Listener\\LoadAdditionalEntriesListener' => __DIR__ . '/..' . '/../lib/Listener/LoadAdditionalEntriesListener.php',
'OCA\\Profile\\Listener\\ProfilePickerReferenceListener' => __DIR__ . '/..' . '/../lib/Listener/ProfilePickerReferenceListener.php',
Expand Down
185 changes: 185 additions & 0 deletions apps/profile/lib/Controller/ProfileApiController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Profile\Controller;

use OCP\App\IAppManager;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\ApiRoute;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCS\OCSNotFoundException;
use OCP\AppFramework\OCSController;
use OCP\Calendar\ICalendarQuery;
use OCP\Calendar\IManager;
use OCP\Files\File;
use OCP\IDateTimeFormatter;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\Share\IManager as IShareManager;
use OCP\Share\IShare;

/**
* @psalm-import-type ProfileSharedResource from \OCA\Profile\ResponseDefinitions
*/
#[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
class ProfileApiController extends OCSController {
public function __construct(
string $appName,
IRequest $request,
private IUserSession $userSession,
private IUserManager $userManager,
private IManager $calendarManager,
private IShareManager $shareManager,
private IDateTimeFormatter $formatter,
private IURLGenerator $urlGenerator,
private IAppManager $appManager,
) {
parent::__construct($appName, $request);
}

/**
* Get resources shared between the current user and the specified user.
*
* @param string $userId - The user ID of the user to get shared resources with.
* @return DataResponse<Http::STATUS_OK, list<ProfileSharedResource>, array{}>
* @throws OCSNotFoundException - The specified user does not exist.
*
* 200: The shared resources between the current user and the specified user.
* 404: The specified user does not exist.
*/
#[NoCSRFRequired]
#[NoAdminRequired]
#[ApiRoute(verb: 'GET', url: '/api/v1/resources/{userId}')]
public function getResources(string $userId): DataResponse {
$user = $this->userManager->get($userId);
if (!$user) {
throw new OCSNotFoundException();
}

$files = $this->getSharedNodes($userId);
$events = $this->getSharedCalendarEvents($userId);

$entries = array_values(array_merge($files, $events));
return new DataResponse($entries);
}

/**
* Get all upcoming events shared between a user and the current user.
*
* If the calendar app is disabled for the current user no events will be returned.
*
* @param string $userId - The user ID of the user to get shared events with.
* @return list<ProfileSharedResource>
*/
private function getSharedCalendarEvents(string $userId) {
if (!$this->appManager->isEnabledForUser('calendar', $this->userSession->getUser())) {
return [];
}

$mePrincipal = 'principals/users/' . $this->userSession->getUser()->getUID();

$query = $this->calendarManager->newQuery($mePrincipal);
$query->setSearchPattern($userId);
$query->addType('VEVENT');
$query->addSearchProperty(ICalendarQuery::SEARCH_PROPERTY_ATTENDEE);
$query->addSearchProperty(ICalendarQuery::SEARCH_PROPERTY_ORGANIZER);
$now = new \DateTimeImmutable('now');
$query->setTimerangeStart($now->modify('-1 hour'));
$query->setLimit(9);

$events = $this->calendarManager->searchForPrincipal($query);
$result = [];
foreach ($events as $event) {
if (isset($event['objects'][0]['STATUS']) && $event['objects'][0]['STATUS'][0] === 'CANCELLED') {
continue;
}

$end = $event['objects'][0]['DTEND'][0];
if ($now->diff($end)->invert === 1) {
// already ended, skip
continue;
}

$calendarUid = $event['objects'][0]['UID'][0];
if (isset($event['RECURRENCE-ID'])) {
$recurrenceId = $event['RECURRENCE-ID'][0];
$href = $this->urlGenerator->linkToRouteAbsolute('calendar.object.indexuid.recurrenceId', ['uid' => $calendarUid, 'recurrenceId' => $recurrenceId]);
} else {
$href = $this->urlGenerator->linkToRouteAbsolute('calendar.object.indexuid', ['uid' => $calendarUid]);
}

$start = \DateTime::createFromImmutable($event['objects'][0]['DTSTART'][0]);
$result[] = [
'label' => $event['objects'][0]['SUMMARY'][0],
'text' => $this->formatter->formatTimeSpan($start, \DateTime::createFromImmutable($now)),
'href' => $href,
'img' => $this->urlGenerator->getAbsoluteURL($this->appManager->getAppIcon('calendar')),
];
}
return $result;
}

/**
* @return ProfileSharedResource[]
*/
private function getSharedNodes(string $userId): array {
$outgoingShares = [];
$offset = 0;
while (count($outgoingShares) < 5) {
$shares = $this->shareManager->getSharesBy($userId, IShare::TYPE_USER, limit: 50, offset: $offset);
$outgoingShares = array_merge($outgoingShares, array_filter($shares, fn ($share) => $share->getSharedWith() === $this->userSession->getUser()->getUID()));
$offset += 50;
if (count($shares) < 50) {
break;
}
}

$incomingShares = [];
$offset = 0;
while (count($incomingShares) < 5) {
$shares = $this->shareManager->getSharesBy($this->userSession->getUser()->getUID(), IShare::TYPE_USER, limit: 50, offset: $offset);
$incomingShares = array_merge($incomingShares, array_filter($shares, fn ($share) => $share->getSharedWith() === $userId));
$offset += 50;
if (count($shares) < 50) {
break;
}
}

$shares = array_slice(array_merge($outgoingShares, $incomingShares), 0, 5);
usort($shares, fn ($a, $b) => $a->getNode()->getMTime() <=> $b->getNode()->getMTime());
$files = [];
foreach ($shares as $share) {
$node = $share->getNode();
// Preview endpoint only serves files; folders need the mime icon directly.
if ($node instanceof File) {
$img = $this->urlGenerator->linkToRouteAbsolute('core.Preview.getPreviewByFileId', [
'fileId' => $node->getId(),
'mimeFallback' => true,
]);
} else {
$img = $this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath('core', 'filetypes/folder.svg'));
}
$files[] = [
'label' => $node->getName(),
'text' => $this->formatter->formatTimeSpan($node->getMTime()),
'href' => $this->urlGenerator->linkToRouteAbsolute('files.view.index', [
'dir' => $node->getParent()->getPath(),
'fileid' => $node->getId(),
]),
'img' => $img,
];
}
return $files;
}
}
21 changes: 21 additions & 0 deletions apps/profile/lib/ResponseDefinitions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Profile;

/**
* @psalm-type ProfileSharedResource array{
* label: string,
* text: string,
* href: string,
* img: string
* }
*/
class ResponseDefinitions {
}
Loading
Loading