diff --git a/apps/cloud_federation_api/lib/Capabilities.php b/apps/cloud_federation_api/lib/Capabilities.php index 91fd52192155c..0eb981865528a 100644 --- a/apps/cloud_federation_api/lib/Capabilities.php +++ b/apps/cloud_federation_api/lib/Capabilities.php @@ -3,6 +3,7 @@ * @copyright Copyright (c) 2017 Bjoern Schiessle * * @author Bjoern Schiessle + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -36,6 +37,22 @@ public function __construct(IURLGenerator $urlGenerator) { /** * Function an app uses to return the capabilities + * + * @return array{ + * ocm: array{ + * enabled: bool, + * apiVersion: string, + * endPoint: string, + * resourceTypes: array{ + * name: string, + * shareTypes: string[], + * protocols: array{ + * webdav: string, + * }, + * }[], + * }, + * } + * @since 8.2.0 */ public function getCapabilities() { $url = $this->urlGenerator->linkToRouteAbsolute('cloud_federation_api.requesthandlercontroller.addShare'); diff --git a/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php b/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php index ef77f2fa317de..7cc029fbba337 100644 --- a/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php +++ b/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php @@ -5,6 +5,7 @@ * @author Bjoern Schiessle * @author Christoph Wurst * @author Roeland Jago Douma + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -25,6 +26,7 @@ namespace OCA\CloudFederationAPI\Controller; use OCA\CloudFederationAPI\Config; +use OCA\CloudFederationAPI\ResponseDefinitions; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; use OCP\AppFramework\Http\JSONResponse; @@ -44,11 +46,13 @@ use Psr\Log\LoggerInterface; /** - * Class RequestHandlerController - * - * handle API between different Cloud instances + * Open-Cloud-Mesh-API * * @package OCA\CloudFederationAPI\Controller + * + * @psalm-import-type CloudFederationApiAddShare from ResponseDefinitions + * @psalm-import-type CloudFederationApiValidationError from ResponseDefinitions + * @psalm-import-type CloudFederationApiError from ResponseDefinitions */ class RequestHandlerController extends Controller { @@ -100,44 +104,42 @@ public function __construct($appName, } /** - * add share + * Add share * * @NoCSRFRequired * @PublicPage * @BruteForceProtection(action=receiveFederatedShare) * - * @param string $shareWith - * @param string $name resource name (e.g. document.odt) - * @param string $description share description (optional) - * @param string $providerId resource UID on the provider side - * @param string $owner provider specific UID of the user who owns the resource - * @param string $ownerDisplayName display name of the user who shared the item - * @param string $sharedBy provider specific UID of the user who shared the resource - * @param string $sharedByDisplayName display name of the user who shared the resource - * @param array $protocol (e,.g. ['name' => 'webdav', 'options' => ['username' => 'john', 'permissions' => 31]]) - * @param string $shareType ('group' or 'user' share) - * @param $resourceType ('file', 'calendar',...) - * @return Http\DataResponse|JSONResponse + * @param string $shareWith The user who the share will be shared with + * @param string $name The resource name (e.g. document.odt) + * @param string|null $description Share description + * @param string $providerId Resource UID on the provider side + * @param string $owner Provider specific UID of the user who owns the resource + * @param string|null $ownerDisplayName Display name of the user who shared the item + * @param string|null $sharedBy Provider specific UID of the user who shared the resource + * @param string|null $sharedByDisplayName Display name of the user who shared the resource + * @param array{name: string[], options: array} $protocol e,.g. ['name' => 'webdav', 'options' => ['username' => 'john', 'permissions' => 31]] + * @param string $shareType 'group' or 'user' share + * @param string $resourceType 'file', 'calendar',... * - * Example: curl -H "Content-Type: application/json" -X POST -d '{"shareWith":"admin1@serve1","name":"welcome server2.txt","description":"desc","providerId":"2","owner":"admin2@http://localhost/server2","ownerDisplayName":"admin2 display","shareType":"user","resourceType":"file","protocol":{"name":"webdav","options":{"sharedSecret":"secret","permissions":"webdav-property"}}}' http://localhost/server/index.php/ocm/shares + * @return JSONResponse|JSONResponse|JSONResponse + * 201: The notification was successfully received. The display name of the recipient might be returned in the body + * 400: Bad request due to invalid parameters, e.g. when `shareWith` is not found or required properties are missing + * 501: Share type or the resource type is not supported */ - public function addShare($shareWith, $name, $description, $providerId, $owner, $ownerDisplayName, $sharedBy, $sharedByDisplayName, $protocol, $shareType, $resourceType) { + public function addShare(string $shareWith, string $name, ?string $description, string $providerId, string $owner, ?string $ownerDisplayName, ?string $sharedBy, ?string $sharedByDisplayName, array $protocol, string $shareType, string $resourceType): JSONResponse { // check if all required parameters are set - if ($shareWith === null || - $name === null || - $providerId === null || - $owner === null || - $resourceType === null || - $shareType === null || - !is_array($protocol) || - !isset($protocol['name']) || + if (!isset($protocol['name']) || !isset($protocol['options']) || !is_array($protocol['options']) || !isset($protocol['options']['sharedSecret']) ) { return new JSONResponse( - ['message' => 'Missing arguments'], + [ + 'message' => 'Missing arguments', + 'validationErrors' => [], + ], Http::STATUS_BAD_REQUEST ); } @@ -158,7 +160,10 @@ public function addShare($shareWith, $name, $description, $providerId, $owner, $ if (!$this->userManager->userExists($shareWith)) { $response = new JSONResponse( - ['message' => 'User "' . $shareWith . '" does not exists at ' . $this->urlGenerator->getBaseUrl()], + [ + 'message' => 'User "' . $shareWith . '" does not exists at ' . $this->urlGenerator->getBaseUrl(), + 'validationErrors' => [], + ], Http::STATUS_BAD_REQUEST ); $response->throttle(); @@ -169,7 +174,10 @@ public function addShare($shareWith, $name, $description, $providerId, $owner, $ if ($shareType === 'group') { if (!$this->groupManager->groupExists($shareWith)) { $response = new JSONResponse( - ['message' => 'Group "' . $shareWith . '" does not exists at ' . $this->urlGenerator->getBaseUrl()], + [ + 'message' => 'Group "' . $shareWith . '" does not exists at ' . $this->urlGenerator->getBaseUrl(), + 'validationErrors' => [], + ], Http::STATUS_BAD_REQUEST ); $response->throttle(); @@ -192,20 +200,18 @@ public function addShare($shareWith, $name, $description, $providerId, $owner, $ $share = $this->factory->getCloudFederationShare($shareWith, $name, $description, $providerId, $owner, $ownerDisplayName, $sharedBy, $sharedByDisplayName, '', $shareType, $resourceType); $share->setProtocol($protocol); $provider->shareReceived($share); - } catch (ProviderDoesNotExistsException $e) { + } catch (ProviderDoesNotExistsException|ProviderCouldNotAddShareException $e) { return new JSONResponse( ['message' => $e->getMessage()], Http::STATUS_NOT_IMPLEMENTED ); - } catch (ProviderCouldNotAddShareException $e) { - return new JSONResponse( - ['message' => $e->getMessage()], - $e->getCode() - ); } catch (\Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); return new JSONResponse( - ['message' => 'Internal error at ' . $this->urlGenerator->getBaseUrl()], + [ + 'message' => 'Internal error at ' . $this->urlGenerator->getBaseUrl(), + 'validationErrors' => [], + ], Http::STATUS_BAD_REQUEST ); } @@ -222,28 +228,32 @@ public function addShare($shareWith, $name, $description, $providerId, $owner, $ } /** - * receive notification about existing share + * Send a notification about an existing share * * @NoCSRFRequired * @PublicPage * @BruteForceProtection(action=receiveFederatedShareNotification) * - * @param string $notificationType (notification type, e.g. SHARE_ACCEPTED) - * @param string $resourceType (calendar, file, contact,...) - * @param string $providerId id of the share - * @param array $notification the actual payload of the notification - * @return JSONResponse + * @param string $notificationType Notification type, e.g. SHARE_ACCEPTED + * @param string $resourceType calendar, file, contact,... + * @param string|null $providerId ID of the share + * @param array|null $notification The actual payload of the notification + * + * @return JSONResponse, array{}>|JSONResponse|JSONResponse + * 201: The notification was successfully received + * 400: Bad request due to invalid parameters, e.g. when `type` is invalid or missing + * 403: Getting resource is not allowed + * 501: The resource type is not supported */ - public function receiveNotification($notificationType, $resourceType, $providerId, array $notification) { + public function receiveNotification(string $notificationType, string $resourceType, ?string $providerId, ?array $notification): JSONResponse { // check if all required parameters are set - if ($notificationType === null || - $resourceType === null || - $providerId === null || - !is_array($notification) - ) { + if ($providerId === null || !is_array($notification)) { return new JSONResponse( - ['message' => 'Missing arguments'], + [ + 'message' => 'Missing arguments', + 'validationErrors' => [], + ], Http::STATUS_BAD_REQUEST ); } @@ -253,12 +263,18 @@ public function receiveNotification($notificationType, $resourceType, $providerI $result = $provider->notificationReceived($notificationType, $providerId, $notification); } catch (ProviderDoesNotExistsException $e) { return new JSONResponse( - ['message' => $e->getMessage()], + [ + 'message' => $e->getMessage(), + 'validationErrors' => [], + ], Http::STATUS_BAD_REQUEST ); } catch (ShareNotFound $e) { $response = new JSONResponse( - ['message' => $e->getMessage()], + [ + 'message' => $e->getMessage(), + 'validationErrors' => [], + ], Http::STATUS_BAD_REQUEST ); $response->throttle(); @@ -271,12 +287,21 @@ public function receiveNotification($notificationType, $resourceType, $providerI } catch (BadRequestException $e) { return new JSONResponse($e->getReturnMessage(), Http::STATUS_BAD_REQUEST); } catch (AuthenticationFailedException $e) { - $response = new JSONResponse(['message' => 'RESOURCE_NOT_FOUND'], Http::STATUS_FORBIDDEN); + $response = new JSONResponse( + [ + 'message' => 'RESOURCE_NOT_FOUND', + 'validationErrors' => [], + ], + Http::STATUS_FORBIDDEN + ); $response->throttle(); return $response; } catch (\Exception $e) { return new JSONResponse( - ['message' => 'Internal error at ' . $this->urlGenerator->getBaseUrl()], + [ + 'message' => 'Internal error at ' . $this->urlGenerator->getBaseUrl(), + 'validationErrors' => [], + ], Http::STATUS_BAD_REQUEST ); } diff --git a/apps/cloud_federation_api/lib/ResponseDefinitions.php b/apps/cloud_federation_api/lib/ResponseDefinitions.php new file mode 100644 index 0000000000000..06b8124cc3273 --- /dev/null +++ b/apps/cloud_federation_api/lib/ResponseDefinitions.php @@ -0,0 +1,45 @@ + + * + * @author Kate Döen + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\CloudFederationAPI; + +/** + * @psalm-type CloudFederationApiAddShare = array{ + * recipientDisplayName: string, + * } + * + * @psalm-type CloudFederationApiError = array{ + * message: string, + * } + * + * @psalm-type CloudFederationApiValidationError = CloudFederationApiError&array{ + * validationErrors: array{ + * name: string, + * message: string|null, + * }[], + * } + */ +class ResponseDefinitions { +} diff --git a/apps/cloud_federation_api/openapi.json b/apps/cloud_federation_api/openapi.json index f017b864a2727..ecefd0635489b 100644 --- a/apps/cloud_federation_api/openapi.json +++ b/apps/cloud_federation_api/openapi.json @@ -351,8 +351,8 @@ "content": { "application/json": { "schema": { - "type": "array", - "items": { + "type": "object", + "additionalProperties": { "type": "object" } } @@ -370,7 +370,7 @@ } }, "403": { - "description": "Getting resource not allowed", + "description": "Getting resource is not allowed", "content": { "application/json": { "schema": { diff --git a/apps/comments/lib/Capabilities.php b/apps/comments/lib/Capabilities.php index ba5d2a2086b25..597f248412c7d 100644 --- a/apps/comments/lib/Capabilities.php +++ b/apps/comments/lib/Capabilities.php @@ -28,6 +28,9 @@ use OCP\Capabilities\ICapability; class Capabilities implements ICapability { + /** + * @return array{files: array{comments: bool}} + */ public function getCapabilities(): array { return [ 'files' => [ diff --git a/apps/comments/lib/Controller/NotificationsController.php b/apps/comments/lib/Controller/NotificationsController.php index 41a9541a68409..e286d1d279b0c 100644 --- a/apps/comments/lib/Controller/NotificationsController.php +++ b/apps/comments/lib/Controller/NotificationsController.php @@ -27,7 +27,7 @@ use OCP\AppFramework\Controller; use OCP\AppFramework\Http\NotFoundResponse; use OCP\AppFramework\Http\RedirectResponse; -use OCP\AppFramework\Http\Response; +use OCP\AppFramework\Http; use OCP\Comments\IComment; use OCP\Comments\ICommentsManager; use OCP\Files\IRootFolder; @@ -73,8 +73,17 @@ public function __construct( /** * @PublicPage * @NoCSRFRequired + * + * View a notification + * + * @param string $id ID of the notification + * + * @return RedirectResponse|NotFoundResponse + * + * 303: Redirected to notification + * 404: Notification not found */ - public function view(string $id): Response { + public function view(string $id): RedirectResponse|NotFoundResponse { $currentUser = $this->userSession->getUser(); if (!$currentUser instanceof IUser) { return new RedirectResponse( diff --git a/apps/comments/openapi.json b/apps/comments/openapi.json new file mode 100644 index 0000000000000..9dbf618a4c726 --- /dev/null +++ b/apps/comments/openapi.json @@ -0,0 +1,103 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "comments", + "version": "0.0.1", + "description": "Files app plugin to add comments to files", + "license": { + "name": "agpl" + } + }, + "components": { + "securitySchemes": { + "basic_auth": { + "type": "http", + "scheme": "basic" + }, + "bearer_auth": { + "type": "http", + "scheme": "bearer" + } + }, + "schemas": { + "Capabilities": { + "type": "object", + "required": [ + "files" + ], + "properties": { + "files": { + "type": "object", + "required": [ + "comments" + ], + "properties": { + "comments": { + "type": "boolean" + } + } + } + } + } + } + }, + "paths": { + "/index.php/apps/comments/notifications/view/{id}": { + "get": { + "operationId": "notifications-view", + "summary": "View a notification", + "tags": [ + "notifications" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of the notification", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "303": { + "description": "Redirected to notification", + "headers": { + "Location": { + "schema": { + "type": "string" + } + } + } + }, + "404": { + "description": "Notification not found", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + }, + "tags": [ + { + "name": "notifications", + "description": "Class NotificationsController" + } + ] +} \ No newline at end of file diff --git a/apps/dashboard/lib/Controller/DashboardApiController.php b/apps/dashboard/lib/Controller/DashboardApiController.php index 1062cf1bdba02..df1c75e4b6883 100644 --- a/apps/dashboard/lib/Controller/DashboardApiController.php +++ b/apps/dashboard/lib/Controller/DashboardApiController.php @@ -6,6 +6,7 @@ * @copyright Copyright (c) 2021 Julien Veyssier * * @author Julien Veyssier + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -26,7 +27,9 @@ namespace OCA\Dashboard\Controller; +use OCA\Dashboard\ResponseDefinitions; use OCP\AppFramework\OCSController; +use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\Dashboard\IButtonWidget; use OCP\Dashboard\IIconWidget; @@ -41,6 +44,10 @@ use OCP\Dashboard\IAPIWidget; use OCP\Dashboard\Model\WidgetItem; +/** + * @psalm-import-type DashboardWidget from ResponseDefinitions + * @psalm-import-type DashboardWidgetItem from ResponseDefinitions + */ class DashboardApiController extends OCSController { /** @var IManager */ @@ -65,15 +72,15 @@ public function __construct( } /** - * Example request with Curl: - * curl -u user:passwd http://my.nc/ocs/v2.php/apps/dashboard/api/v1/widget-items -H Content-Type:application/json -X GET -d '{"sinceIds":{"github_notifications":"2021-03-22T15:01:10Z"}}' + * @NoAdminRequired + * @NoCSRFRequired + * + * Get the items for the widgets * - * @param array $sinceIds Array indexed by widget Ids, contains date/id from which we want the new items + * @param array $sinceIds Array indexed by widget Ids, contains date/id from which we want the new items * @param int $limit Limit number of result items per widget * @param string[] $widgets Limit results to specific widgets - * - * @NoAdminRequired - * @NoCSRFRequired + * @return DataResponse, array{}> */ public function getWidgetItems(array $sinceIds = [], int $limit = 7, array $widgets = []): DataResponse { $showWidgets = $widgets; @@ -97,11 +104,12 @@ public function getWidgetItems(array $sinceIds = [], int $limit = 7, array $widg } /** - * Example request with Curl: - * curl -u user:passwd http://my.nc/ocs/v2.php/apps/dashboard/api/v1/widgets + * Get the widgets * * @NoAdminRequired * @NoCSRFRequired + * + * @return DataResponse */ public function getWidgets(): DataResponse { $widgets = $this->dashboardManager->getWidgets(); diff --git a/apps/dashboard/lib/Controller/DashboardController.php b/apps/dashboard/lib/Controller/DashboardController.php index b0e150e00f7eb..450895498a6d0 100644 --- a/apps/dashboard/lib/Controller/DashboardController.php +++ b/apps/dashboard/lib/Controller/DashboardController.php @@ -9,6 +9,7 @@ * @author Julius Härtl * @author Morris Jobke * @author Roeland Jago Douma + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -76,6 +77,7 @@ public function __construct( /** * @NoCSRFRequired * @NoAdminRequired + * @IgnoreAPI * @return TemplateResponse */ public function index(): TemplateResponse { diff --git a/apps/dashboard/lib/ResponseDefinitions.php b/apps/dashboard/lib/ResponseDefinitions.php new file mode 100644 index 0000000000000..1c40f251f2a57 --- /dev/null +++ b/apps/dashboard/lib/ResponseDefinitions.php @@ -0,0 +1,53 @@ + + * + * @author Kate Döen + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Dashboard; + +/** + * @psalm-type DashboardWidget = array{ + * id: string, + * title: string, + * order: int, + * icon_class: string, + * icon_url: string, + * widget_url: ?string, + * item_icons_round: bool, + * buttons?: array{ + * type: string, + * text: string, + * link: string, + * }[], + * } + * + * @psalm-type DashboardWidgetItem = array{ + * subtitle: string, + * title: string, + * link: string, + * iconUrl: string, + * sinceId: string, + * } + */ +class ResponseDefinitions { +} diff --git a/apps/dav/lib/Capabilities.php b/apps/dav/lib/Capabilities.php index b8096d3395ab5..f61fb5d2f0a73 100644 --- a/apps/dav/lib/Capabilities.php +++ b/apps/dav/lib/Capabilities.php @@ -5,6 +5,7 @@ * @author Thomas Müller * @author Louis Chemineau * @author Côme Chilliet + * @author Kate Döen * * @license AGPL-3.0 * @@ -33,6 +34,9 @@ public function __construct(IConfig $config) { $this->config = $config; } + /** + * @return array{dav: array{chunking: string, bulkupload?: string}} + */ public function getCapabilities() { $capabilities = [ 'dav' => [ diff --git a/apps/dav/lib/Controller/DirectController.php b/apps/dav/lib/Controller/DirectController.php index f9c83488935c4..1a7b3b5762684 100644 --- a/apps/dav/lib/Controller/DirectController.php +++ b/apps/dav/lib/Controller/DirectController.php @@ -7,6 +7,7 @@ * * @author Iscle * @author Roeland Jago Douma + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -28,6 +29,7 @@ use OCA\DAV\Db\Direct; use OCA\DAV\Db\DirectMapper; +use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCS\OCSBadRequestException; use OCP\AppFramework\OCS\OCSNotFoundException; @@ -88,6 +90,17 @@ public function __construct(string $appName, /** * @NoAdminRequired + * + * Get a direct link to a file + * + * @param int $fileId ID of the file + * @param int $expirationTime Duration until the link expires + * @return DataResponse + * @throws OCSNotFoundException File not found + * @throws OCSBadRequestException Getting direct link is not possible + * @throws OCSForbiddenException Missing permissions to get direct link + * + * 200: Direct link returned */ public function getUrl(int $fileId, int $expirationTime = 60 * 60 * 8): DataResponse { $userFolder = $this->rootFolder->getUserFolder($this->userId); diff --git a/apps/dav/lib/Controller/InvitationResponseController.php b/apps/dav/lib/Controller/InvitationResponseController.php index a360794987413..0a4ab2aefe5d9 100644 --- a/apps/dav/lib/Controller/InvitationResponseController.php +++ b/apps/dav/lib/Controller/InvitationResponseController.php @@ -8,6 +8,7 @@ * @author Christoph Wurst * @author Georg Ehrke * @author Joas Schilling + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -71,6 +72,7 @@ public function __construct(string $appName, IRequest $request, /** * @PublicPage * @NoCSRFRequired + * @IgnoreAPI * * @param string $token * @return TemplateResponse @@ -95,6 +97,7 @@ public function accept(string $token):TemplateResponse { /** * @PublicPage * @NoCSRFRequired + * @IgnoreAPI * * @param string $token * @return TemplateResponse @@ -120,6 +123,7 @@ public function decline(string $token):TemplateResponse { /** * @PublicPage * @NoCSRFRequired + * @IgnoreAPI * * @param string $token * @return TemplateResponse @@ -133,6 +137,7 @@ public function options(string $token):TemplateResponse { /** * @PublicPage * @NoCSRFRequired + * @IgnoreAPI * * @param string $token * diff --git a/apps/federatedfilesharing/lib/Controller/MountPublicLinkController.php b/apps/federatedfilesharing/lib/Controller/MountPublicLinkController.php index 404077e46affe..1a253441ae60a 100644 --- a/apps/federatedfilesharing/lib/Controller/MountPublicLinkController.php +++ b/apps/federatedfilesharing/lib/Controller/MountPublicLinkController.php @@ -125,10 +125,12 @@ public function __construct($appName, * @PublicPage * @BruteForceProtection(action=publicLink2FederatedShare) * - * @param string $shareWith - * @param string $token - * @param string $password - * @return JSONResponse + * @param string $shareWith Username to share with + * @param string $token Token of the share + * @param string $password Password of the share + * @return JSONResponse|JSONResponse + * 200: Remote URL returned + * 400: Creating share is not possible */ public function createFederatedShare($shareWith, $token, $password = '') { if (!$this->federatedShareProvider->isOutgoingServer2serverShareEnabled()) { @@ -182,7 +184,7 @@ public function createFederatedShare($shareWith, $token, $password = '') { return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST); } - return new JSONResponse(['remoteUrl' => $server]); + return new JSONResponse(['remoteUrl' => (string)$server]); } /** diff --git a/apps/federatedfilesharing/lib/Controller/RequestHandlerController.php b/apps/federatedfilesharing/lib/Controller/RequestHandlerController.php index 2dd8098b3b072..b845a5888c862 100644 --- a/apps/federatedfilesharing/lib/Controller/RequestHandlerController.php +++ b/apps/federatedfilesharing/lib/Controller/RequestHandlerController.php @@ -123,16 +123,16 @@ public function __construct(string $appName, * * create a new share * - * @param string|null $remote - * @param string|null $token - * @param string|null $name - * @param string|null $owner - * @param string|null $sharedBy - * @param string|null $shareWith - * @param int|null $remoteId - * @param string|null $sharedByFederatedId - * @param string|null $ownerFederatedId - * @return Http\DataResponse + * @param string|null $remote Address of the remote + * @param string|null $token Shared secret between servers + * @param string|null $name Name of the shared resource + * @param string|null $owner Display name of the receiver + * @param string|null $sharedBy Display name of the sender + * @param string|null $shareWith ID of the user that receives the share + * @param int|null $remoteId ID of the remote + * @param string|null $sharedByFederatedId Federated ID of the sender + * @param string|null $ownerFederatedId Federated ID of the receiver + * @return Http\DataResponse * @throws OCSException */ public function createShare( @@ -185,7 +185,7 @@ public function createShare( throw new OCSException('internal server error, was not able to add share from ' . $remote, 500); } - return new Http\DataResponse(); + return new Http\DataResponse(new \stdClass()); } /** @@ -194,19 +194,19 @@ public function createShare( * * create re-share on behalf of another user * - * @param int $id - * @param string|null $token - * @param string|null $shareWith - * @param int|null $permission - * @param int|null $remoteId - * @return Http\DataResponse - * @throws OCSBadRequestException + * @param int $id ID of the share + * @param string|null $token Shared secret between servers + * @param string|null $shareWith ID of the user that receives the share + * @param int|null $remoteId ID of the remote + * @return Http\DataResponse + * @throws OCSBadRequestException Re-sharing is not possible * @throws OCSException + * + * 200: Remote share returned */ - public function reShare(int $id, ?string $token = null, ?string $shareWith = null, ?int $permission = 0, ?int $remoteId = 0) { + public function reShare(int $id, ?string $token = null, ?string $shareWith = null, ?int $remoteId = 0) { if ($token === null || $shareWith === null || - $permission === null || $remoteId === null ) { throw new OCSBadRequestException(); @@ -223,8 +223,8 @@ public function reShare(int $id, ?string $token = null, ?string $shareWith = nul $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file'); [$newToken, $localId] = $provider->notificationReceived('REQUEST_RESHARE', $id, $notification); return new Http\DataResponse([ - 'token' => $newToken, - 'remoteId' => $localId + 'token' => (string)$newToken, + 'remoteId' => (string)$localId ]); } catch (ProviderDoesNotExistsException $e) { throw new OCSException('Server does not support federated cloud sharing', 503); @@ -244,12 +244,14 @@ public function reShare(int $id, ?string $token = null, ?string $shareWith = nul * * accept server-to-server share * - * @param int $id - * @param string|null $token - * @return Http\DataResponse + * @param int $id ID of the remote share + * @param string|null $token Shared secret between servers + * @return Http\DataResponse * @throws OCSException * @throws ShareNotFound * @throws \OCP\HintException + * + * 200: Share accepted successfully */ public function acceptShare(int $id, ?string $token = null) { $notification = [ @@ -269,7 +271,7 @@ public function acceptShare(int $id, ?string $token = null) { $this->logger->debug('internal server error, can not process notification: ' . $e->getMessage(), ['exception' => $e]); } - return new Http\DataResponse(); + return new Http\DataResponse(new \stdClass()); } /** @@ -278,9 +280,9 @@ public function acceptShare(int $id, ?string $token = null) { * * decline server-to-server share * - * @param int $id - * @param string|null $token - * @return Http\DataResponse + * @param int $id ID of the remote share + * @param string|null $token Shared secret between servers + * @return Http\DataResponse * @throws OCSException */ public function declineShare(int $id, ?string $token = null) { @@ -301,7 +303,7 @@ public function declineShare(int $id, ?string $token = null) { $this->logger->debug('internal server error, can not process notification: ' . $e->getMessage(), ['exception' => $e]); } - return new Http\DataResponse(); + return new Http\DataResponse(new \stdClass()); } /** @@ -310,9 +312,9 @@ public function declineShare(int $id, ?string $token = null) { * * remove server-to-server share if it was unshared by the owner * - * @param int $id - * @param string|null $token - * @return Http\DataResponse + * @param int $id ID of the share + * @param string|null $token Shared secret between servers + * @return Http\DataResponse * @throws OCSException */ public function unshare(int $id, ?string $token = null) { @@ -329,7 +331,7 @@ public function unshare(int $id, ?string $token = null) { $this->logger->debug('processing unshare notification failed: ' . $e->getMessage(), ['exception' => $e]); } - return new Http\DataResponse(); + return new Http\DataResponse(new \stdClass()); } private function cleanupRemote($remote) { @@ -345,17 +347,19 @@ private function cleanupRemote($remote) { * * federated share was revoked, either by the owner or the re-sharer * - * @param int $id - * @param string|null $token - * @return Http\DataResponse - * @throws OCSBadRequestException + * @param int $id ID of the share + * @param string|null $token Shared secret between servers + * @return Http\DataResponse + * @throws OCSBadRequestException Revoking the share is not possible + * + * 200: Share revoked successfully */ public function revoke(int $id, ?string $token = null) { try { $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file'); $notification = ['sharedSecret' => $token]; $provider->notificationReceived('RESHARE_UNDO', $id, $notification); - return new Http\DataResponse(); + return new Http\DataResponse(new \stdClass()); } catch (\Exception $e) { throw new OCSBadRequestException(); } @@ -385,11 +389,13 @@ private function isS2SEnabled($incoming = false) { * * update share information to keep federated re-shares in sync * - * @param int $id - * @param string|null $token - * @param int|null $permissions - * @return Http\DataResponse - * @throws OCSBadRequestException + * @param int $id ID of the share + * @param string|null $token Shared secret between servers + * @param int|null $permissions New permissions + * @return Http\DataResponse + * @throws OCSBadRequestException Updating permissions is not possible + * + * 200: Permissions updated successfully */ public function updatePermissions(int $id, ?string $token = null, ?int $permissions = null) { $ncPermissions = $permissions; @@ -405,7 +411,7 @@ public function updatePermissions(int $id, ?string $token = null, ?int $permissi throw new OCSBadRequestException(); } - return new Http\DataResponse(); + return new Http\DataResponse(new \stdClass()); } /** @@ -439,14 +445,14 @@ protected function ncPermissions2ocmPermissions($ncPermissions) { * * change the owner of a server-to-server share * - * @param int $id - * @param string|null $token - * @param string|null $remote - * @param string|null $remote_id - * @return Http\DataResponse - * @throws OCSBadRequestException - * @throws OCSException - * @throws \OCP\DB\Exception + * @param int $id ID of the share + * @param string|null $token Shared secret between servers + * @param string|null $remote Address of the remote + * @param string|null $remote_id ID of the remote + * @return Http\DataResponse + * @throws OCSBadRequestException Moving share is not possible + * + * 200: Share moved successfully */ public function move(int $id, ?string $token = null, ?string $remote = null, ?string $remote_id = null) { if (!$this->isS2SEnabled()) { diff --git a/apps/federatedfilesharing/openapi.json b/apps/federatedfilesharing/openapi.json new file mode 100644 index 0000000000000..a8e1cd975ab95 --- /dev/null +++ b/apps/federatedfilesharing/openapi.json @@ -0,0 +1,973 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "federatedfilesharing", + "version": "0.0.1", + "description": "Provide federated file sharing across servers", + "license": { + "name": "agpl" + } + }, + "components": { + "securitySchemes": { + "basic_auth": { + "type": "http", + "scheme": "basic" + }, + "bearer_auth": { + "type": "http", + "scheme": "bearer" + } + }, + "schemas": { + "OCSMeta": { + "type": "object", + "required": [ + "status", + "statuscode" + ], + "properties": { + "status": { + "type": "string" + }, + "statuscode": { + "type": "integer" + }, + "message": { + "type": "string" + }, + "totalitems": { + "type": "string" + }, + "itemsperpage": { + "type": "string" + } + } + } + } + }, + "paths": { + "/index.php/apps/federatedfilesharing/createFederatedShare": { + "post": { + "operationId": "mount_public_link-create-federated-share", + "summary": "send federated share to a user of a public link", + "tags": [ + "mount_public_link" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "shareWith", + "in": "query", + "description": "Username to share with", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "token", + "in": "query", + "description": "Token of the share", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "password", + "in": "query", + "description": "Password of the share", + "schema": { + "type": "string", + "default": "" + } + } + ], + "responses": { + "200": { + "description": "Remote URL returned", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "remoteUrl" + ], + "properties": { + "remoteUrl": { + "type": "string" + } + } + } + } + } + }, + "400": { + "description": "Creating share is not possible", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/cloud/shares": { + "post": { + "operationId": "request_handler-create-share", + "summary": "create a new share", + "tags": [ + "request_handler" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "remote", + "in": "query", + "description": "Address of the remote", + "schema": { + "type": "string", + "nullable": true + } + }, + { + "name": "token", + "in": "query", + "description": "Shared secret between servers", + "schema": { + "type": "string", + "nullable": true + } + }, + { + "name": "name", + "in": "query", + "description": "Name of the shared resource", + "schema": { + "type": "string", + "nullable": true + } + }, + { + "name": "owner", + "in": "query", + "description": "Display name of the receiver", + "schema": { + "type": "string", + "nullable": true + } + }, + { + "name": "sharedBy", + "in": "query", + "description": "Display name of the sender", + "schema": { + "type": "string", + "nullable": true + } + }, + { + "name": "shareWith", + "in": "query", + "description": "ID of the user that receives the share", + "schema": { + "type": "string", + "nullable": true + } + }, + { + "name": "remoteId", + "in": "query", + "description": "ID of the remote", + "schema": { + "type": "integer", + "format": "int64", + "nullable": true + } + }, + { + "name": "sharedByFederatedId", + "in": "query", + "description": "Federated ID of the sender", + "schema": { + "type": "string", + "nullable": true + } + }, + { + "name": "ownerFederatedId", + "in": "query", + "description": "Federated ID of the receiver", + "schema": { + "type": "string", + "nullable": true + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "required": true, + "schema": { + "type": "string", + "default": "true" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "additionalProperties": true + } + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/cloud/shares/{id}/reshare": { + "post": { + "operationId": "request_handler-re-share", + "summary": "create re-share on behalf of another user", + "tags": [ + "request_handler" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "token", + "in": "query", + "description": "Shared secret between servers", + "schema": { + "type": "string", + "nullable": true + } + }, + { + "name": "shareWith", + "in": "query", + "description": "ID of the user that receives the share", + "schema": { + "type": "string", + "nullable": true + } + }, + { + "name": "remoteId", + "in": "query", + "description": "ID of the remote", + "schema": { + "type": "integer", + "format": "int64", + "nullable": true, + "default": 0 + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the share", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "required": true, + "schema": { + "type": "string", + "default": "true" + } + } + ], + "responses": { + "200": { + "description": "Remote share returned", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "token", + "remoteId" + ], + "properties": { + "token": { + "type": "string" + }, + "remoteId": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "400": { + "description": "Re-sharing is not possible", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/ocs/v2.php/cloud/shares/{id}/permissions": { + "post": { + "operationId": "request_handler-update-permissions", + "summary": "update share information to keep federated re-shares in sync", + "tags": [ + "request_handler" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "token", + "in": "query", + "description": "Shared secret between servers", + "schema": { + "type": "string", + "nullable": true + } + }, + { + "name": "permissions", + "in": "query", + "description": "New permissions", + "schema": { + "type": "integer", + "format": "int64", + "nullable": true + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the share", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "required": true, + "schema": { + "type": "string", + "default": "true" + } + } + ], + "responses": { + "200": { + "description": "Permissions updated successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "additionalProperties": true + } + } + } + } + } + } + } + }, + "400": { + "description": "Updating permissions is not possible", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/ocs/v2.php/cloud/shares/{id}/accept": { + "post": { + "operationId": "request_handler-accept-share", + "summary": "accept server-to-server share", + "tags": [ + "request_handler" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "token", + "in": "query", + "description": "Shared secret between servers", + "schema": { + "type": "string", + "nullable": true + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the remote share", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "required": true, + "schema": { + "type": "string", + "default": "true" + } + } + ], + "responses": { + "200": { + "description": "Share accepted successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "additionalProperties": true + } + } + } + } + } + } + } + }, + "500": { + "description": "", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/ocs/v2.php/cloud/shares/{id}/decline": { + "post": { + "operationId": "request_handler-decline-share", + "summary": "decline server-to-server share", + "tags": [ + "request_handler" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "token", + "in": "query", + "description": "Shared secret between servers", + "schema": { + "type": "string", + "nullable": true + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the remote share", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "required": true, + "schema": { + "type": "string", + "default": "true" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "additionalProperties": true + } + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/cloud/shares/{id}/unshare": { + "post": { + "operationId": "request_handler-unshare", + "summary": "remove server-to-server share if it was unshared by the owner", + "tags": [ + "request_handler" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "token", + "in": "query", + "description": "Shared secret between servers", + "schema": { + "type": "string", + "nullable": true + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the share", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "required": true, + "schema": { + "type": "string", + "default": "true" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "additionalProperties": true + } + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/cloud/shares/{id}/revoke": { + "post": { + "operationId": "request_handler-revoke", + "summary": "federated share was revoked, either by the owner or the re-sharer", + "tags": [ + "request_handler" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "token", + "in": "query", + "description": "Shared secret between servers", + "schema": { + "type": "string", + "nullable": true + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the share", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "required": true, + "schema": { + "type": "string", + "default": "true" + } + } + ], + "responses": { + "200": { + "description": "Share revoked successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "additionalProperties": true + } + } + } + } + } + } + } + }, + "400": { + "description": "Revoking the share is not possible", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/ocs/v2.php/cloud/shares/{id}/move": { + "post": { + "operationId": "request_handler-move", + "summary": "change the owner of a server-to-server share", + "tags": [ + "request_handler" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "token", + "in": "query", + "description": "Shared secret between servers", + "schema": { + "type": "string", + "nullable": true + } + }, + { + "name": "remote", + "in": "query", + "description": "Address of the remote", + "schema": { + "type": "string", + "nullable": true + } + }, + { + "name": "remote_id", + "in": "query", + "description": "ID of the remote", + "schema": { + "type": "string", + "nullable": true + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the share", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "required": true, + "schema": { + "type": "string", + "default": "true" + } + } + ], + "responses": { + "200": { + "description": "Share moved successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "remote", + "owner" + ], + "properties": { + "remote": { + "type": "string" + }, + "owner": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "400": { + "description": "Moving share is not possible", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + }, + "tags": [ + { + "name": "mount_public_link", + "description": "Class MountPublicLinkController\nconvert public links to federated shares" + } + ] +} \ No newline at end of file diff --git a/apps/federation/lib/Controller/OCSAuthAPIController.php b/apps/federation/lib/Controller/OCSAuthAPIController.php index 5a976720b04d7..cf241c215fdf9 100644 --- a/apps/federation/lib/Controller/OCSAuthAPIController.php +++ b/apps/federation/lib/Controller/OCSAuthAPIController.php @@ -79,7 +79,10 @@ public function __construct( * * @NoCSRFRequired * @PublicPage - * @throws OCSForbiddenException + * + * @param string $url URL of the server + * @param string $token Token of the server + * @throws OCSForbiddenException Requesting shared secret is not allowed */ public function requestSharedSecretLegacy(string $url, string $token): DataResponse { return $this->requestSharedSecret($url, $token); @@ -91,7 +94,10 @@ public function requestSharedSecretLegacy(string $url, string $token): DataRespo * * @NoCSRFRequired * @PublicPage - * @throws OCSForbiddenException + * + * @param string $url URL of the server + * @param string $token Token of the server + * @throws OCSForbiddenException Getting shared secret is not allowed */ public function getSharedSecretLegacy(string $url, string $token): DataResponse { return $this->getSharedSecret($url, $token); @@ -102,7 +108,10 @@ public function getSharedSecretLegacy(string $url, string $token): DataResponse * * @NoCSRFRequired * @PublicPage - * @throws OCSForbiddenException + * + * @param string $url URL of the server + * @param string $token Token of the server + * @throws OCSForbiddenException Requesting shared secret is not allowed */ public function requestSharedSecret(string $url, string $token): DataResponse { if ($this->trustedServers->isTrustedServer($url) === false) { @@ -138,7 +147,10 @@ public function requestSharedSecret(string $url, string $token): DataResponse { * * @NoCSRFRequired * @PublicPage - * @throws OCSForbiddenException + * + * @param string $url URL of the server + * @param string $token Token of the server + * @throws OCSForbiddenException Getting shared secret is not allowed */ public function getSharedSecret(string $url, string $token): DataResponse { if ($this->trustedServers->isTrustedServer($url) === false) { diff --git a/apps/federation/openapi.json b/apps/federation/openapi.json new file mode 100644 index 0000000000000..09963abff73d5 --- /dev/null +++ b/apps/federation/openapi.json @@ -0,0 +1,291 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "federation", + "version": "0.0.1", + "description": "Federation allows you to connect with other trusted servers to exchange the user directory.", + "license": { + "name": "agpl" + } + }, + "components": { + "securitySchemes": { + "basic_auth": { + "type": "http", + "scheme": "basic" + }, + "bearer_auth": { + "type": "http", + "scheme": "bearer" + } + }, + "schemas": { + "OCSMeta": { + "type": "object", + "required": [ + "status", + "statuscode" + ], + "properties": { + "status": { + "type": "string" + }, + "statuscode": { + "type": "integer" + }, + "message": { + "type": "string" + }, + "totalitems": { + "type": "string" + }, + "itemsperpage": { + "type": "string" + } + } + } + } + }, + "paths": { + "/ocs/v2.php/apps/federation/api/v1/shared-secret": { + "get": { + "operationId": "ocs_authapi-get-shared-secret-legacy", + "summary": "Create shared secret and return it, for legacy end-points", + "tags": [ + "ocs_authapi" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "url", + "in": "query", + "description": "URL of the server", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "token", + "in": "query", + "description": "Token of the server", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "required": true, + "schema": { + "type": "string", + "default": "true" + } + } + ], + "responses": { + "403": { + "description": "Getting shared secret is not allowed", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/federation/api/v1/request-shared-secret": { + "post": { + "operationId": "ocs_authapi-request-shared-secret-legacy", + "summary": "Request received to ask remote server for a shared secret, for legacy end-points", + "tags": [ + "ocs_authapi" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "url", + "in": "query", + "description": "URL of the server", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "token", + "in": "query", + "description": "Token of the server", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "required": true, + "schema": { + "type": "string", + "default": "true" + } + } + ], + "responses": { + "403": { + "description": "Requesting shared secret is not allowed", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/ocs/v2.php/cloud/shared-secret": { + "get": { + "operationId": "ocs_authapi-get-shared-secret", + "summary": "Create shared secret and return it", + "tags": [ + "ocs_authapi" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "url", + "in": "query", + "description": "URL of the server", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "token", + "in": "query", + "description": "Token of the server", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "required": true, + "schema": { + "type": "string", + "default": "true" + } + } + ], + "responses": { + "403": { + "description": "Getting shared secret is not allowed", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + }, + "post": { + "operationId": "ocs_authapi-request-shared-secret", + "summary": "Request received to ask remote server for a shared secret", + "tags": [ + "ocs_authapi" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "url", + "in": "query", + "description": "URL of the server", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "token", + "in": "query", + "description": "Token of the server", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "required": true, + "schema": { + "type": "string", + "default": "true" + } + } + ], + "responses": { + "403": { + "description": "Requesting shared secret is not allowed", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + }, + "tags": [ + { + "name": "ocs_authapi", + "description": "Class OCSAuthAPI\nOCS API end-points to exchange shared secret between two connected Nextclouds" + } + ] +} \ No newline at end of file diff --git a/apps/files/lib/Capabilities.php b/apps/files/lib/Capabilities.php index 5cb976a47be0b..dc2aae6acfcd9 100644 --- a/apps/files/lib/Capabilities.php +++ b/apps/files/lib/Capabilities.php @@ -38,12 +38,14 @@ public function __construct(IConfig $config) { /** * Return this classes capabilities + * + * @return array{files: array{bigfilechunking: bool, blacklisted_files: array}} */ public function getCapabilities() { return [ 'files' => [ 'bigfilechunking' => true, - 'blacklisted_files' => $this->config->getSystemValue('blacklisted_files', ['.htaccess']) + 'blacklisted_files' => (array)$this->config->getSystemValue('blacklisted_files', ['.htaccess']) ], ]; } diff --git a/apps/files/lib/Controller/ApiController.php b/apps/files/lib/Controller/ApiController.php index fd0f3bdf26166..cca9a0c45be98 100644 --- a/apps/files/lib/Controller/ApiController.php +++ b/apps/files/lib/Controller/ApiController.php @@ -104,10 +104,14 @@ public function __construct(string $appName, * @NoCSRFRequired * @StrictCookieRequired * - * @param int $x - * @param int $y + * @param int $x Width of the thumbnail + * @param int $y Height of the thumbnail * @param string $file URL-encoded filename - * @return DataResponse|FileDisplayResponse + * @return FileDisplayResponse|DataResponse + * + * 200: Thumbnail returned + * 400: Getting thumbnail is not possible + * 404: File not found */ public function getThumbnail($x, $y, $file) { if ($x < 1 || $y < 1) { @@ -127,7 +131,7 @@ public function getThumbnail($x, $y, $file) { } catch (NotFoundException $e) { return new DataResponse(['message' => 'File not found.'], Http::STATUS_NOT_FOUND); } catch (\Exception $e) { - return new DataResponse([], Http::STATUS_BAD_REQUEST); + return new DataResponse(['message' => null], Http::STATUS_BAD_REQUEST); } } @@ -400,6 +404,12 @@ public function getNodeType($folderpath) { /** * @NoAdminRequired * @NoCSRFRequired + * + * Get the service-worker Javascript for previews + * + * @psalm-suppress MoreSpecificReturnType The value of Service-Worker-Allowed is not relevant + * @psalm-suppress LessSpecificReturnStatement The value of Service-Worker-Allowed is not relevant + * @return StreamResponse */ public function serviceWorker(): StreamResponse { $response = new StreamResponse(__DIR__ . '/../../../../dist/preview-service-worker.js'); diff --git a/apps/files/lib/Controller/DirectEditingController.php b/apps/files/lib/Controller/DirectEditingController.php index 9b48d6958aa8a..d58be166e797d 100644 --- a/apps/files/lib/Controller/DirectEditingController.php +++ b/apps/files/lib/Controller/DirectEditingController.php @@ -63,6 +63,9 @@ public function __construct($appName, IRequest $request, $corsMethods, $corsAllo /** * @NoAdminRequired + * + * Get the direct editing capabilities + * @return DataResponse, creators: array}, array{}> */ public function info(): DataResponse { $response = new DataResponse($this->directEditingService->getDirectEditingCapabilitites()); @@ -72,6 +75,18 @@ public function info(): DataResponse { /** * @NoAdminRequired + * + * Create a file for direct editing + * + * @param string $path Path of the file + * @param string $editorId ID of the editor + * @param string $creatorId ID of the creator + * @param ?string $templateId ID of the template + * + * @return DataResponse|DataResponse + * + * 200: URL for direct editing returned + * 403: Opening file is not allowed */ public function create(string $path, string $editorId, string $creatorId, string $templateId = null): DataResponse { if (!$this->directEditingManager->isEnabled()) { @@ -92,6 +107,17 @@ public function create(string $path, string $editorId, string $creatorId, string /** * @NoAdminRequired + * + * Open a file for direct editing + * + * @param string $path Path of the file + * @param ?string $editorId ID of the editor + * @param ?int $fileId ID of the file + * + * @return DataResponse|DataResponse + * + * 200: URL for direct editing returned + * 403: Opening file is not allowed */ public function open(string $path, string $editorId = null, ?int $fileId = null): DataResponse { if (!$this->directEditingManager->isEnabled()) { @@ -114,6 +140,15 @@ public function open(string $path, string $editorId = null, ?int $fileId = null) /** * @NoAdminRequired + * + * Get the templates for direct editing + * + * @param string $editorId ID of the editor + * @param string $creatorId ID of the creator + * + * @return DataResponse}, array{}>|DataResponse + * + * 200: Templates returned */ public function templates(string $editorId, string $creatorId): DataResponse { if (!$this->directEditingManager->isEnabled()) { diff --git a/apps/files/lib/Controller/DirectEditingViewController.php b/apps/files/lib/Controller/DirectEditingViewController.php index 30d54d5ceb366..cff111f633fe0 100644 --- a/apps/files/lib/Controller/DirectEditingViewController.php +++ b/apps/files/lib/Controller/DirectEditingViewController.php @@ -55,6 +55,7 @@ public function __construct($appName, IRequest $request, IEventDispatcher $event * @PublicPage * @NoCSRFRequired * @UseSession + * @IgnoreAPI * * @param string $token * @return Response diff --git a/apps/files/lib/Controller/OpenLocalEditorController.php b/apps/files/lib/Controller/OpenLocalEditorController.php index 7d784196361c1..060472024725f 100644 --- a/apps/files/lib/Controller/OpenLocalEditorController.php +++ b/apps/files/lib/Controller/OpenLocalEditorController.php @@ -70,6 +70,14 @@ public function __construct( /** * @NoAdminRequired * @UserRateThrottle(limit=10, period=120) + * + * Create a local editor + * + * @param string $path Path of the file + * + * @return DataResponse|DataResponse + * + * 200: Local editor returned */ public function create(string $path): DataResponse { $pathHash = sha1($path); @@ -101,12 +109,22 @@ public function create(string $path): DataResponse { } $this->logger->error('Giving up after ' . self::TOKEN_RETRIES . ' retries to generate a unique local editor token for path hash: ' . $pathHash); - return new DataResponse([], Http::STATUS_INTERNAL_SERVER_ERROR); + return new DataResponse(new \stdClass(), Http::STATUS_INTERNAL_SERVER_ERROR); } /** * @NoAdminRequired * @BruteForceProtection(action=openLocalEditor) + * + * Validate a local editor + * + * @param string $path Path of the file + * @param string $token Token of the local editor + * + * @return DataResponse|DataResponse + * + * 200: Local editor validated successfully + * 404: Local editor not found */ public function validate(string $path, string $token): DataResponse { $pathHash = sha1($path); @@ -114,7 +132,7 @@ public function validate(string $path, string $token): DataResponse { try { $entity = $this->mapper->verifyToken($this->userId, $pathHash, $token); } catch (DoesNotExistException $e) { - $response = new DataResponse([], Http::STATUS_NOT_FOUND); + $response = new DataResponse(new \stdClass(), Http::STATUS_NOT_FOUND); $response->throttle(['userId' => $this->userId, 'pathHash' => $pathHash]); return $response; } @@ -122,7 +140,7 @@ public function validate(string $path, string $token): DataResponse { $this->mapper->delete($entity); if ($entity->getExpirationTime() <= $this->timeFactory->getTime()) { - $response = new DataResponse([], Http::STATUS_NOT_FOUND); + $response = new DataResponse(new \stdClass(), Http::STATUS_NOT_FOUND); $response->throttle(['userId' => $this->userId, 'pathHash' => $pathHash]); return $response; } diff --git a/apps/files/lib/Controller/TemplateController.php b/apps/files/lib/Controller/TemplateController.php index d04d86760e629..645350010eccb 100644 --- a/apps/files/lib/Controller/TemplateController.php +++ b/apps/files/lib/Controller/TemplateController.php @@ -26,13 +26,21 @@ */ namespace OCA\Files\Controller; +use OCA\Files\ResponseDefinitions; +use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCS\OCSForbiddenException; use OCP\AppFramework\OCSController; use OCP\Files\GenericFileException; use OCP\Files\Template\ITemplateManager; +use OCP\Files\Template\TemplateFileCreator; use OCP\IRequest; +/** + * @psalm-import-type FilesTemplate from ResponseDefinitions + * @psalm-import-type FilesTemplateFile from ResponseDefinitions + * @psalm-import-type FilesTemplateFileCreator from ResponseDefinitions + */ class TemplateController extends OCSController { protected $templateManager; @@ -43,6 +51,10 @@ public function __construct($appName, IRequest $request, ITemplateManager $templ /** * @NoAdminRequired + * + * List the available templates + * + * @return DataResponse, array{}> */ public function list(): DataResponse { return new DataResponse($this->templateManager->listTemplates()); @@ -50,7 +62,17 @@ public function list(): DataResponse { /** * @NoAdminRequired - * @throws OCSForbiddenException + * + * Create a template + * + * @param string $filePath Path of the file + * @param string $templatePath Name of the template + * @param string $templateType Type of the template + * + * @return DataResponse + * @throws OCSForbiddenException Creating template is not allowed + * + * 200: Template created successfully */ public function create(string $filePath, string $templatePath = '', string $templateType = 'user'): DataResponse { try { @@ -62,13 +84,24 @@ public function create(string $filePath, string $templatePath = '', string $temp /** * @NoAdminRequired + * + * Initialize the template directory + * + * @param string $templatePath Path of the template directory + * @param bool $copySystemTemplates Whether to copy the system templates to the template directory + * + * @return DataResponse + * @throws OCSForbiddenException Initializing the template directory is not allowed + * + * 200: Template directory initialized successfully */ public function path(string $templatePath = '', bool $copySystemTemplates = false) { try { + /** @var string $templatePath */ $templatePath = $this->templateManager->initializeTemplateDirectory($templatePath, null, $copySystemTemplates); return new DataResponse([ 'template_path' => $templatePath, - 'templates' => $this->templateManager->listCreators() + 'templates' => array_map(fn(TemplateFileCreator $creator) => $creator->jsonSerialize(), $this->templateManager->listCreators()), ]); } catch (\Exception $e) { throw new OCSForbiddenException($e->getMessage()); diff --git a/apps/files/lib/Controller/TransferOwnershipController.php b/apps/files/lib/Controller/TransferOwnershipController.php index 5abd65444bf99..7adc538bf0a46 100644 --- a/apps/files/lib/Controller/TransferOwnershipController.php +++ b/apps/files/lib/Controller/TransferOwnershipController.php @@ -82,12 +82,23 @@ public function __construct(string $appName, /** * @NoAdminRequired + * + * Transfer the ownership to another user + * + * @param string $recipient Username of the recipient + * @param string $path Path of the file + * + * @return DataResponse + * + * 200: Ownership transferred successfully + * 400: Transferring ownership is not possible + * 403: Transferring ownership is not allowed */ public function transfer(string $recipient, string $path): DataResponse { $recipientUser = $this->userManager->get($recipient); if ($recipientUser === null) { - return new DataResponse([], Http::STATUS_BAD_REQUEST); + return new DataResponse(new \stdClass(), Http::STATUS_BAD_REQUEST); } $userRoot = $this->rootFolder->getUserFolder($this->userId); @@ -95,11 +106,11 @@ public function transfer(string $recipient, string $path): DataResponse { try { $node = $userRoot->get($path); } catch (\Exception $e) { - return new DataResponse([], Http::STATUS_BAD_REQUEST); + return new DataResponse(new \stdClass(), Http::STATUS_BAD_REQUEST); } if ($node->getOwner()->getUID() !== $this->userId || !$node->getStorage()->instanceOfStorage(IHomeStorage::class)) { - return new DataResponse([], Http::STATUS_FORBIDDEN); + return new DataResponse(new \stdClass(), Http::STATUS_FORBIDDEN); } $transferOwnership = new TransferOwnershipEntity(); @@ -122,21 +133,31 @@ public function transfer(string $recipient, string $path): DataResponse { $this->notificationManager->notify($notification); - return new DataResponse([]); + return new DataResponse(new \stdClass()); } /** * @NoAdminRequired + * + * Accept an ownership transfer + * + * @param int $id ID of the ownership transfer + * + * @return DataResponse + * + * 200: Ownership transfer accepted successfully + * 403: Accepting ownership transfer is not allowed + * 404: Ownership transfer not found */ public function accept(int $id): DataResponse { try { $transferOwnership = $this->mapper->getById($id); } catch (DoesNotExistException $e) { - return new DataResponse([], Http::STATUS_NOT_FOUND); + return new DataResponse(new \stdClass(), Http::STATUS_NOT_FOUND); } if ($transferOwnership->getTargetUser() !== $this->userId) { - return new DataResponse([], Http::STATUS_FORBIDDEN); + return new DataResponse(new \stdClass(), Http::STATUS_FORBIDDEN); } $notification = $this->notificationManager->createNotification(); @@ -155,21 +176,31 @@ public function accept(int $id): DataResponse { 'id' => $newTransferOwnership->getId(), ]); - return new DataResponse([], Http::STATUS_OK); + return new DataResponse(new \stdClass(), Http::STATUS_OK); } /** * @NoAdminRequired + * + * Reject an ownership transfer + * + * @param int $id ID of the ownership transfer + * + * @return DataResponse + * + * 200: Ownership transfer rejected successfully + * 403: Rejecting ownership transfer is not allowed + * 404: Ownership transfer not found */ public function reject(int $id): DataResponse { try { $transferOwnership = $this->mapper->getById($id); } catch (DoesNotExistException $e) { - return new DataResponse([], Http::STATUS_NOT_FOUND); + return new DataResponse(new \stdClass(), Http::STATUS_NOT_FOUND); } if ($transferOwnership->getTargetUser() !== $this->userId) { - return new DataResponse([], Http::STATUS_FORBIDDEN); + return new DataResponse(new \stdClass(), Http::STATUS_FORBIDDEN); } $notification = $this->notificationManager->createNotification(); @@ -191,6 +222,6 @@ public function reject(int $id): DataResponse { $this->mapper->delete($transferOwnership); - return new DataResponse([], Http::STATUS_OK); + return new DataResponse(new \stdClass(), Http::STATUS_OK); } } diff --git a/apps/files/lib/Controller/ViewController.php b/apps/files/lib/Controller/ViewController.php index 70e0fd4845654..2f6df7c5d5395 100644 --- a/apps/files/lib/Controller/ViewController.php +++ b/apps/files/lib/Controller/ViewController.php @@ -35,6 +35,7 @@ */ namespace OCA\Files\Controller; +use OC\AppFramework\Http; use OCA\Files\Activity\Helper; use OCA\Files\AppInfo\Application; use OCA\Files\Event\LoadAdditionalScriptsEvent; @@ -149,6 +150,7 @@ protected function getStorageInfo(string $dir = '/') { /** * @NoCSRFRequired * @NoAdminRequired + * @IgnoreAPI * * @param string $fileid * @return TemplateResponse|RedirectResponse @@ -167,6 +169,7 @@ public function showFile(string $fileid = null, int $openfile = 1): Response { * @NoCSRFRequired * @NoAdminRequired * @UseSession + * @IgnoreAPI * * @param string $dir * @param string $view diff --git a/apps/files/lib/DirectEditingCapabilities.php b/apps/files/lib/DirectEditingCapabilities.php index 10c8e95105a31..1bc00519ae85e 100644 --- a/apps/files/lib/DirectEditingCapabilities.php +++ b/apps/files/lib/DirectEditingCapabilities.php @@ -38,6 +38,9 @@ public function __construct(DirectEditingService $directEditingService, IURLGene $this->urlGenerator = $urlGenerator; } + /** + * @return array{files: array{directEditing: array{url: string, etag: string, supportsFileId: bool}}} + */ public function getCapabilities() { return [ 'files' => [ diff --git a/apps/files/lib/ResponseDefinitions.php b/apps/files/lib/ResponseDefinitions.php new file mode 100644 index 0000000000000..8a27ec4bb2fc6 --- /dev/null +++ b/apps/files/lib/ResponseDefinitions.php @@ -0,0 +1,67 @@ + + * + * @author Kate Döen + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Files; + +/** + * @psalm-type FilesTemplate = array{ + * templateType: string, + * templateId: string, + * basename: string, + * etag: string, + * fileid: int, + * filename: string, + * lastmod: int, + * mime: string, + * size: int, + * type: string, + * hasPreview: bool, + * previewUrl: ?string, + * } + * + * @psalm-type FilesTemplateFile = array{ + * basename: string, + * etag: string, + * fileid: int, + * filename: ?string, + * lastmod: int, + * mime: string, + * size: int, + * type: string, + * hasPreview: bool, + * } + * + * @psalm-type FilesTemplateFileCreator = array{ + * app: string, + * label: string, + * extension: string, + * iconClass: ?string, + * mimetypes: string[], + * ratio: ?float, + * actionLabel: string, + * } + */ +class ResponseDefinitions { +} diff --git a/apps/files/openapi.json b/apps/files/openapi.json new file mode 100644 index 0000000000000..41062f69953b6 --- /dev/null +++ b/apps/files/openapi.json @@ -0,0 +1,1954 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "files", + "version": "0.0.1", + "description": "File Management", + "license": { + "name": "agpl" + } + }, + "components": { + "securitySchemes": { + "basic_auth": { + "type": "http", + "scheme": "basic" + }, + "bearer_auth": { + "type": "http", + "scheme": "bearer" + } + }, + "schemas": { + "Capabilities": { + "type": "object", + "properties": { + "files": { + "type": [ + "object", + "object" + ], + "required": [ + "bigfilechunking", + "blacklisted_files", + "directEditing" + ], + "properties": { + "bigfilechunking": { + "type": "boolean" + }, + "blacklisted_files": { + "type": "array", + "items": { + "type": "object" + } + }, + "directEditing": { + "type": "object", + "required": [ + "url", + "etag", + "supportsFileId" + ], + "properties": { + "url": { + "type": "string" + }, + "etag": { + "type": "string" + }, + "supportsFileId": { + "type": "boolean" + } + } + } + } + } + } + }, + "OCSMeta": { + "type": "object", + "required": [ + "status", + "statuscode" + ], + "properties": { + "status": { + "type": "string" + }, + "statuscode": { + "type": "integer" + }, + "message": { + "type": "string" + }, + "totalitems": { + "type": "string" + }, + "itemsperpage": { + "type": "string" + } + } + }, + "Template": { + "type": "object", + "required": [ + "templateType", + "templateId", + "basename", + "etag", + "fileid", + "filename", + "lastmod", + "mime", + "size", + "type", + "hasPreview", + "previewUrl" + ], + "properties": { + "templateType": { + "type": "string" + }, + "templateId": { + "type": "string" + }, + "basename": { + "type": "string" + }, + "etag": { + "type": "string" + }, + "fileid": { + "type": "integer", + "format": "int64" + }, + "filename": { + "type": "string" + }, + "lastmod": { + "type": "integer", + "format": "int64" + }, + "mime": { + "type": "string" + }, + "size": { + "type": "integer", + "format": "int64" + }, + "type": { + "type": "string" + }, + "hasPreview": { + "type": "boolean" + }, + "previewUrl": { + "type": "string", + "nullable": true + } + } + }, + "TemplateFile": { + "type": "object", + "required": [ + "basename", + "etag", + "fileid", + "filename", + "lastmod", + "mime", + "size", + "type", + "hasPreview" + ], + "properties": { + "basename": { + "type": "string" + }, + "etag": { + "type": "string" + }, + "fileid": { + "type": "integer", + "format": "int64" + }, + "filename": { + "type": "string", + "nullable": true + }, + "lastmod": { + "type": "integer", + "format": "int64" + }, + "mime": { + "type": "string" + }, + "size": { + "type": "integer", + "format": "int64" + }, + "type": { + "type": "string" + }, + "hasPreview": { + "type": "boolean" + } + } + }, + "TemplateFileCreator": { + "type": "object", + "required": [ + "app", + "label", + "extension", + "iconClass", + "mimetypes", + "ratio", + "actionLabel" + ], + "properties": { + "app": { + "type": "string" + }, + "label": { + "type": "string" + }, + "extension": { + "type": "string" + }, + "iconClass": { + "type": "string", + "nullable": true + }, + "mimetypes": { + "type": "array", + "items": { + "type": "string" + } + }, + "ratio": { + "type": "number", + "format": "float", + "nullable": true + }, + "actionLabel": { + "type": "string" + } + } + } + } + }, + "paths": { + "/index.php/apps/files/api/v1/thumbnail/{x}/{y}/{file}": { + "get": { + "operationId": "api-get-thumbnail", + "summary": "Gets a thumbnail of the specified file", + "tags": [ + "api" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "x", + "in": "path", + "description": "Width of the thumbnail", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "y", + "in": "path", + "description": "Height of the thumbnail", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "file", + "in": "path", + "description": "URL-encoded filename", + "required": true, + "schema": { + "type": "string", + "pattern": "^.+$" + } + } + ], + "responses": { + "200": { + "description": "Thumbnail returned", + "content": { + "*/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "400": { + "description": "Getting thumbnail is not possible", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string", + "nullable": true + } + } + } + } + } + }, + "404": { + "description": "File not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string", + "nullable": true + } + } + } + } + } + } + } + } + }, + "/index.php/apps/files/preview-service-worker.js": { + "get": { + "operationId": "api-service-worker", + "summary": "Get the service-worker Javascript for previews", + "tags": [ + "api" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "responses": { + "200": { + "description": "", + "headers": { + "Service-Worker-Allowed": { + "schema": { + "type": "string" + } + } + }, + "content": { + "application/javascript": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/files/api/v1/directEditing": { + "get": { + "operationId": "direct_editing-info", + "summary": "Get the direct editing capabilities", + "tags": [ + "direct_editing" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "OCS-APIRequest", + "in": "header", + "required": true, + "schema": { + "type": "string", + "default": "true" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "editors", + "creators" + ], + "properties": { + "editors": { + "type": "object", + "additionalProperties": { + "type": "object", + "required": [ + "id", + "name", + "mimetypes", + "optionalMimetypes", + "secure" + ], + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "mimetypes": { + "type": "array", + "items": { + "type": "string" + } + }, + "optionalMimetypes": { + "type": "array", + "items": { + "type": "string" + } + }, + "secure": { + "type": "boolean" + } + } + } + }, + "creators": { + "type": "object", + "additionalProperties": { + "type": "object", + "required": [ + "id", + "editor", + "name", + "extension", + "templates", + "mimetypes" + ], + "properties": { + "id": { + "type": "string" + }, + "editor": { + "type": "string" + }, + "name": { + "type": "string" + }, + "extension": { + "type": "string" + }, + "templates": { + "type": "boolean" + }, + "mimetypes": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/files/api/v1/directEditing/templates/{editorId}/{creatorId}": { + "get": { + "operationId": "direct_editing-templates", + "summary": "Get the templates for direct editing", + "tags": [ + "direct_editing" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "editorId", + "in": "path", + "description": "ID of the editor", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "creatorId", + "in": "path", + "description": "ID of the creator", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "required": true, + "schema": { + "type": "string", + "default": "true" + } + } + ], + "responses": { + "200": { + "description": "Templates returned", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "templates" + ], + "properties": { + "templates": { + "type": "object", + "additionalProperties": { + "type": "object", + "required": [ + "id", + "title", + "preview", + "extension", + "mimetype" + ], + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "preview": { + "type": "string", + "nullable": true + }, + "extension": { + "type": "string" + }, + "mimetype": { + "type": "string" + } + } + } + } + } + } + } + } + } + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/files/api/v1/directEditing/open": { + "post": { + "operationId": "direct_editing-open", + "summary": "Open a file for direct editing", + "tags": [ + "direct_editing" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "path", + "in": "query", + "description": "Path of the file", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "editorId", + "in": "query", + "description": "ID of the editor", + "schema": { + "type": "string", + "nullable": true + } + }, + { + "name": "fileId", + "in": "query", + "description": "ID of the file", + "schema": { + "type": "integer", + "format": "int64", + "nullable": true + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "required": true, + "schema": { + "type": "string", + "default": "true" + } + } + ], + "responses": { + "200": { + "description": "URL for direct editing returned", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "url" + ], + "properties": { + "url": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Opening file is not allowed", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/files/api/v1/directEditing/create": { + "post": { + "operationId": "direct_editing-create", + "summary": "Create a file for direct editing", + "tags": [ + "direct_editing" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "path", + "in": "query", + "description": "Path of the file", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "editorId", + "in": "query", + "description": "ID of the editor", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "creatorId", + "in": "query", + "description": "ID of the creator", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "templateId", + "in": "query", + "description": "ID of the template", + "schema": { + "type": "string", + "nullable": true + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "required": true, + "schema": { + "type": "string", + "default": "true" + } + } + ], + "responses": { + "200": { + "description": "URL for direct editing returned", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "url" + ], + "properties": { + "url": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Opening file is not allowed", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/files/api/v1/templates": { + "get": { + "operationId": "template-list", + "summary": "List the available templates", + "tags": [ + "template" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "OCS-APIRequest", + "in": "header", + "required": true, + "schema": { + "type": "string", + "default": "true" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TemplateFileCreator" + } + } + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/files/api/v1/templates/create": { + "post": { + "operationId": "template-create", + "summary": "Create a template", + "tags": [ + "template" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "filePath", + "in": "query", + "description": "Path of the file", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "templatePath", + "in": "query", + "description": "Name of the template", + "schema": { + "type": "string", + "default": "" + } + }, + { + "name": "templateType", + "in": "query", + "description": "Type of the template", + "schema": { + "type": "string", + "default": "user" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "required": true, + "schema": { + "type": "string", + "default": "true" + } + } + ], + "responses": { + "200": { + "description": "Template created successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "$ref": "#/components/schemas/TemplateFile" + } + } + } + } + } + } + } + }, + "403": { + "description": "Creating template is not allowed", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/files/api/v1/templates/path": { + "post": { + "operationId": "template-path", + "summary": "Initialize the template directory", + "tags": [ + "template" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "templatePath", + "in": "query", + "description": "Path of the template directory", + "schema": { + "type": "string", + "default": "" + } + }, + { + "name": "copySystemTemplates", + "in": "query", + "description": "Whether to copy the system templates to the template directory", + "schema": { + "type": "integer", + "default": 0 + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "required": true, + "schema": { + "type": "string", + "default": "true" + } + } + ], + "responses": { + "200": { + "description": "Template directory initialized successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "template_path", + "templates" + ], + "properties": { + "template_path": { + "type": "string" + }, + "templates": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TemplateFileCreator" + } + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Initializing the template directory is not allowed", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/files/api/v1/transferownership": { + "post": { + "operationId": "transfer_ownership-transfer", + "summary": "Transfer the ownership to another user", + "tags": [ + "transfer_ownership" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "recipient", + "in": "query", + "description": "Username of the recipient", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "path", + "in": "query", + "description": "Path of the file", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "required": true, + "schema": { + "type": "string", + "default": "true" + } + } + ], + "responses": { + "200": { + "description": "Ownership transferred successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "additionalProperties": true + } + } + } + } + } + } + } + }, + "400": { + "description": "Transferring ownership is not possible", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "additionalProperties": true + } + } + } + } + } + } + } + }, + "403": { + "description": "Transferring ownership is not allowed", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "additionalProperties": true + } + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/files/api/v1/transferownership/{id}": { + "post": { + "operationId": "transfer_ownership-accept", + "summary": "Accept an ownership transfer", + "tags": [ + "transfer_ownership" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of the ownership transfer", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "required": true, + "schema": { + "type": "string", + "default": "true" + } + } + ], + "responses": { + "200": { + "description": "Ownership transfer accepted successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "additionalProperties": true + } + } + } + } + } + } + } + }, + "403": { + "description": "Accepting ownership transfer is not allowed", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "additionalProperties": true + } + } + } + } + } + } + } + }, + "404": { + "description": "Ownership transfer not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "additionalProperties": true + } + } + } + } + } + } + } + } + } + }, + "delete": { + "operationId": "transfer_ownership-reject", + "summary": "Reject an ownership transfer", + "tags": [ + "transfer_ownership" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of the ownership transfer", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "required": true, + "schema": { + "type": "string", + "default": "true" + } + } + ], + "responses": { + "200": { + "description": "Ownership transfer rejected successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "additionalProperties": true + } + } + } + } + } + } + } + }, + "403": { + "description": "Rejecting ownership transfer is not allowed", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "additionalProperties": true + } + } + } + } + } + } + } + }, + "404": { + "description": "Ownership transfer not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "additionalProperties": true + } + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/files/api/v1/openlocaleditor": { + "post": { + "operationId": "open_local_editor-create", + "summary": "Create a local editor", + "tags": [ + "open_local_editor" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "path", + "in": "query", + "description": "Path of the file", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "required": true, + "schema": { + "type": "string", + "default": "true" + } + } + ], + "responses": { + "200": { + "description": "Local editor returned", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "userId", + "pathHash", + "expirationTime", + "token" + ], + "properties": { + "userId": { + "type": "string", + "nullable": true + }, + "pathHash": { + "type": "string" + }, + "expirationTime": { + "type": "integer", + "format": "int64" + }, + "token": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "additionalProperties": true + } + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/files/api/v1/openlocaleditor/{token}": { + "post": { + "operationId": "open_local_editor-validate", + "summary": "Validate a local editor", + "tags": [ + "open_local_editor" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "path", + "in": "query", + "description": "Path of the file", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "token", + "in": "path", + "description": "Token of the local editor", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "required": true, + "schema": { + "type": "string", + "default": "true" + } + } + ], + "responses": { + "200": { + "description": "Local editor validated successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "userId", + "pathHash", + "expirationTime", + "token" + ], + "properties": { + "userId": { + "type": "string" + }, + "pathHash": { + "type": "string" + }, + "expirationTime": { + "type": "integer", + "format": "int64" + }, + "token": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Local editor not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "additionalProperties": true + } + } + } + } + } + } + } + } + } + } + } + }, + "tags": [ + { + "name": "view", + "description": "Class ViewController" + }, + { + "name": "api", + "description": "Class ApiController" + } + ] +} \ No newline at end of file diff --git a/apps/files_external/lib/Controller/ApiController.php b/apps/files_external/lib/Controller/ApiController.php index 40539d0bbcaec..3124ae725e5ce 100644 --- a/apps/files_external/lib/Controller/ApiController.php +++ b/apps/files_external/lib/Controller/ApiController.php @@ -30,13 +30,18 @@ namespace OCA\Files_External\Controller; use OCA\Files_External\Lib\StorageConfig; +use OCA\Files_External\ResponseDefinitions; use OCA\Files_External\Service\UserGlobalStoragesService; use OCA\Files_External\Service\UserStoragesService; +use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCSController; use OCP\IRequest; use OCP\IUserSession; +/** + * @psalm-import-type FilesExternalMount from ResponseDefinitions + */ class ApiController extends OCSController { /** @var IUserSession */ @@ -66,7 +71,7 @@ public function __construct( * @param string $mountPoint mount point name, relative to the data dir * @param StorageConfig $mountConfig mount config to format * - * @return array entry + * @return FilesExternalMount */ private function formatMount(string $mountPoint, StorageConfig $mountConfig): array { // split path from mount point @@ -83,6 +88,9 @@ private function formatMount(string $mountPoint, StorageConfig $mountConfig): ar $permissions |= \OCP\Constants::PERMISSION_DELETE; } + /** @var int|string $id */ + $id = $mountConfig->getId(); + /** @var int $permissions */ $entry = [ 'name' => basename($mountPoint), 'path' => $path, @@ -90,7 +98,7 @@ private function formatMount(string $mountPoint, StorageConfig $mountConfig): ar 'backend' => $mountConfig->getBackend()->getText(), 'scope' => $isSystemMount ? 'system' : 'personal', 'permissions' => $permissions, - 'id' => $mountConfig->getId(), + 'id' => $id, 'class' => $mountConfig->getBackend()->getIdentifier(), ]; return $entry; @@ -99,9 +107,9 @@ private function formatMount(string $mountPoint, StorageConfig $mountConfig): ar /** * @NoAdminRequired * - * Returns the mount points visible for this user. + * Get the mount points visible for this user * - * @return DataResponse share information + * @return DataResponse */ public function getUserMounts(): DataResponse { $entries = []; diff --git a/apps/files_external/lib/ResponseDefinitions.php b/apps/files_external/lib/ResponseDefinitions.php new file mode 100644 index 0000000000000..abbc8b46edbbf --- /dev/null +++ b/apps/files_external/lib/ResponseDefinitions.php @@ -0,0 +1,41 @@ + + * + * @author Kate Döen + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Files_External; + +/** + * @psalm-type FilesExternalMount = array{ + * name: string, + * path: string, + * type: string, + * backend: string, + * scope: string, + * permissions: int, + * id: string|int, + * class: string, + * } + */ +class ResponseDefinitions { +} diff --git a/apps/files_external/openapi.json b/apps/files_external/openapi.json new file mode 100644 index 0000000000000..b07cd988a24e2 --- /dev/null +++ b/apps/files_external/openapi.json @@ -0,0 +1,163 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "files_external", + "version": "0.0.1", + "description": "Adds basic external storage support", + "license": { + "name": "agpl" + } + }, + "components": { + "securitySchemes": { + "basic_auth": { + "type": "http", + "scheme": "basic" + }, + "bearer_auth": { + "type": "http", + "scheme": "bearer" + } + }, + "schemas": { + "Mount": { + "type": "object", + "required": [ + "name", + "path", + "type", + "backend", + "scope", + "permissions", + "id", + "class" + ], + "properties": { + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "type": { + "type": "string" + }, + "backend": { + "type": "string" + }, + "scope": { + "type": "string" + }, + "permissions": { + "type": "integer", + "format": "int64" + }, + "id": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "integer", + "format": "int64" + } + ] + }, + "class": { + "type": "string" + } + } + }, + "OCSMeta": { + "type": "object", + "required": [ + "status", + "statuscode" + ], + "properties": { + "status": { + "type": "string" + }, + "statuscode": { + "type": "integer" + }, + "message": { + "type": "string" + }, + "totalitems": { + "type": "string" + }, + "itemsperpage": { + "type": "string" + } + } + } + } + }, + "paths": { + "/ocs/v2.php/apps/files_external/api/v1/mounts": { + "get": { + "operationId": "api-get-user-mounts", + "summary": "Get the mount points visible for this user", + "tags": [ + "api" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "OCS-APIRequest", + "in": "header", + "required": true, + "schema": { + "type": "string", + "default": "true" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Mount" + } + } + } + } + } + } + } + } + } + } + } + } + }, + "tags": [] +} \ No newline at end of file diff --git a/apps/files_sharing/lib/Capabilities.php b/apps/files_sharing/lib/Capabilities.php index 8b1160aeb63df..defeb517a018b 100644 --- a/apps/files_sharing/lib/Capabilities.php +++ b/apps/files_sharing/lib/Capabilities.php @@ -8,6 +8,7 @@ * @author Roeland Jago Douma * @author Tobias Kaminsky * @author Vincent Petry + * @author Kate Döen * * @license AGPL-3.0 * @@ -50,6 +51,67 @@ public function __construct(IConfig $config, IManager $shareManager) { /** * Return this classes capabilities + * + * @return array{ + * files_sharing: array{ + * api_enabled: bool, + * public: array{ + * enabled: bool, + * password?: array{ + * enforced: bool, + * askForOptionalPassword: bool + * }, + * multiple_links?: bool, + * expire_date?: array{ + * enabled: bool, + * days?: int, + * enforced?: bool, + * }, + * expire_date_internal?: array{ + * enabled: bool, + * days?: int, + * enforced?: bool, + * }, + * expire_date_remote?: array{ + * enabled: bool, + * days?: int, + * enforced?: bool, + * }, + * send_mail?: bool, + * upload?: bool, + * upload_files_drop?: bool, + * }, + * user: array{ + * send_mail: bool, + * expire_date?: array{ + * enabled: bool, + * }, + * }, + * resharing: bool, + * group_sharing?: bool, + * group?: array{ + * enabled: bool, + * expire_date?: array{ + * enabled: bool, + * }, + * }, + * default_permissions?: int, + * federation: array{ + * outgoing: bool, + * incoming: bool, + * expire_date: array{ + * enabled: bool, + * }, + * expire_date_supported: array{ + * enabled: bool, + * }, + * }, + * sharee: array{ + * query_lookup_default: bool, + * always_show_unique: bool, + * }, + * }, + * } */ public function getCapabilities() { $res = []; diff --git a/apps/files_sharing/lib/Controller/AcceptController.php b/apps/files_sharing/lib/Controller/AcceptController.php index c97780d6f0ce4..aa07ac8db3eb8 100644 --- a/apps/files_sharing/lib/Controller/AcceptController.php +++ b/apps/files_sharing/lib/Controller/AcceptController.php @@ -6,6 +6,7 @@ * @copyright Copyright (c) 2020, Roeland Jago Douma * * @author Roeland Jago Douma + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -58,6 +59,7 @@ public function __construct(IRequest $request, ShareManager $shareManager, IUser /** * @NoAdminRequired * @NoCSRFRequired + * @IgnoreAPI */ public function accept(string $shareId): Response { try { diff --git a/apps/files_sharing/lib/Controller/DeletedShareAPIController.php b/apps/files_sharing/lib/Controller/DeletedShareAPIController.php index 19d1cbd0af602..e3c15984e4151 100644 --- a/apps/files_sharing/lib/Controller/DeletedShareAPIController.php +++ b/apps/files_sharing/lib/Controller/DeletedShareAPIController.php @@ -11,6 +11,7 @@ * @author John Molakvoæ * @author Julius Härtl * @author Roeland Jago Douma + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -30,7 +31,9 @@ */ namespace OCA\Files_Sharing\Controller; +use OCA\Files_Sharing\ResponseDefinitions; use OCP\App\IAppManager; +use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCS\OCSException; use OCP\AppFramework\OCS\OCSNotFoundException; @@ -47,6 +50,9 @@ use OCP\Share\IManager as ShareManager; use OCP\Share\IShare; +/** + * @psalm-import-type FilesSharingDeletedShare from ResponseDefinitions + */ class DeletedShareAPIController extends OCSController { /** @var ShareManager */ @@ -92,6 +98,8 @@ public function __construct(string $appName, /** * @suppress PhanUndeclaredClassMethod + * + * @return FilesSharingDeletedShare */ private function formatShare(IShare $share): array { $result = [ @@ -174,6 +182,10 @@ private function formatShare(IShare $share): array { /** * @NoAdminRequired + * + * Get a list of all deleted shares + * + * @return DataResponse */ public function index(): DataResponse { $groupShares = $this->shareManager->getDeletedSharedWith($this->userId, IShare::TYPE_GROUP, null, -1, 0); @@ -193,7 +205,14 @@ public function index(): DataResponse { /** * @NoAdminRequired * + * Undelete a deleted share + * + * @param string $id ID of the share + * @return DataResponse * @throws OCSException + * @throws OCSNotFoundException Share not found + * + * 200: Share undeleted successfully */ public function undelete(string $id): DataResponse { try { @@ -212,7 +231,7 @@ public function undelete(string $id): DataResponse { throw new OCSException('Something went wrong'); } - return new DataResponse([]); + return new DataResponse(new \stdClass()); } /** diff --git a/apps/files_sharing/lib/Controller/PublicPreviewController.php b/apps/files_sharing/lib/Controller/PublicPreviewController.php index ee11cf5f3f0bf..468b767fa4721 100644 --- a/apps/files_sharing/lib/Controller/PublicPreviewController.php +++ b/apps/files_sharing/lib/Controller/PublicPreviewController.php @@ -5,6 +5,7 @@ * @author Julius Härtl * @author Morris Jobke * @author Roeland Jago Douma + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -81,6 +82,7 @@ protected function isPasswordProtected(): bool { /** * @PublicPage * @NoCSRFRequired + * @IgnoreAPI * * @param string $file * @param int $x @@ -137,6 +139,7 @@ public function getPreview( * @PublicPage * @NoCSRFRequired * @NoSameSiteCookieRequired + * @IgnoreAPI * * @param $token * @return DataResponse|FileDisplayResponse diff --git a/apps/files_sharing/lib/Controller/RemoteController.php b/apps/files_sharing/lib/Controller/RemoteController.php index 47523e08639d4..bfc21fa36a8af 100644 --- a/apps/files_sharing/lib/Controller/RemoteController.php +++ b/apps/files_sharing/lib/Controller/RemoteController.php @@ -5,6 +5,7 @@ * @author Bjoern Schiessle * @author Joas Schilling * @author Roeland Jago Douma + * @author Kate Döen * * @license AGPL-3.0 * @@ -24,6 +25,8 @@ namespace OCA\Files_Sharing\Controller; use OCA\Files_Sharing\External\Manager; +use OCA\Files_Sharing\ResponseDefinitions; +use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCS\OCSForbiddenException; use OCP\AppFramework\OCS\OCSNotFoundException; @@ -31,6 +34,9 @@ use OCP\ILogger; use OCP\IRequest; +/** + * @psalm-import-type FilesSharingRemoteShare from ResponseDefinitions + */ class RemoteController extends OCSController { /** @var Manager */ @@ -63,9 +69,9 @@ public function __construct($appName, * * Get list of pending remote shares * - * @return DataResponse + * @return DataResponse */ - public function getOpenShares() { + public function getOpenShares(): DataResponse { return new DataResponse($this->externalManager->getOpenShares()); } @@ -74,13 +80,15 @@ public function getOpenShares() { * * Accept a remote share * - * @param int $id - * @return DataResponse - * @throws OCSNotFoundException + * @param int $id ID of the share + * @return DataResponse + * @throws OCSNotFoundException Share not found + * + * 200: Share accepted successfully */ - public function acceptShare($id) { + public function acceptShare(int $id): DataResponse { if ($this->externalManager->acceptShare($id)) { - return new DataResponse(); + return new DataResponse(new \stdClass()); } $this->logger->error('Could not accept federated share with id: ' . $id, @@ -94,13 +102,15 @@ public function acceptShare($id) { * * Decline a remote share * - * @param int $id - * @return DataResponse - * @throws OCSNotFoundException + * @param int $id ID of the share + * @return DataResponse + * @throws OCSNotFoundException Share not found + * + * 200: Share declined successfully */ - public function declineShare($id) { + public function declineShare(int $id): DataResponse { if ($this->externalManager->declineShare($id)) { - return new DataResponse(); + return new DataResponse(new \stdClass()); } // Make sure the user has no notification for something that does not exist anymore. @@ -133,11 +143,11 @@ private static function extendShareInfo($share) { /** * @NoAdminRequired * - * List accepted remote shares + * Get a list of accepted remote shares * - * @return DataResponse + * @return DataResponse */ - public function getShares() { + public function getShares(): DataResponse { $shares = $this->externalManager->getAcceptedShares(); $shares = array_map('self::extendShareInfo', $shares); @@ -149,11 +159,13 @@ public function getShares() { * * Get info of a remote share * - * @param int $id - * @return DataResponse - * @throws OCSNotFoundException + * @param int $id ID of the share + * @return DataResponse + * @throws OCSNotFoundException Share not found + * + * 200: Share returned */ - public function getShare($id) { + public function getShare(int $id): DataResponse { $shareInfo = $this->externalManager->getShare($id); if ($shareInfo === false) { @@ -169,12 +181,14 @@ public function getShare($id) { * * Unshare a remote share * - * @param int $id - * @return DataResponse - * @throws OCSNotFoundException - * @throws OCSForbiddenException + * @param int $id ID of the share + * @return DataResponse + * @throws OCSNotFoundException Share not found + * @throws OCSForbiddenException Unsharing is not possible + * + * 200: Share unshared successfully */ - public function unshare($id) { + public function unshare(int $id): DataResponse { $shareInfo = $this->externalManager->getShare($id); if ($shareInfo === false) { @@ -184,7 +198,7 @@ public function unshare($id) { $mountPoint = '/' . \OC_User::getUser() . '/files' . $shareInfo['mountpoint']; if ($this->externalManager->removeShare($mountPoint) === true) { - return new DataResponse(); + return new DataResponse(new \stdClass()); } else { throw new OCSForbiddenException('Could not unshare'); } diff --git a/apps/files_sharing/lib/Controller/ShareAPIController.php b/apps/files_sharing/lib/Controller/ShareAPIController.php index e2fb950dceb93..59213ee1a941f 100644 --- a/apps/files_sharing/lib/Controller/ShareAPIController.php +++ b/apps/files_sharing/lib/Controller/ShareAPIController.php @@ -26,6 +26,7 @@ * @author Valdnet <47037905+Valdnet@users.noreply.github.com> * @author Vincent Petry * @author waleczny + * @author Kate Döen * * @license AGPL-3.0 * @@ -42,6 +43,7 @@ * along with this program. If not, see * */ + namespace OCA\Files_Sharing\Controller; use Exception; @@ -50,8 +52,10 @@ use OCA\Files\Helper; use OCA\Files_Sharing\Exceptions\SharingRightsException; use OCA\Files_Sharing\External\Storage; +use OCA\Files_Sharing\ResponseDefinitions; use OCA\Files_Sharing\SharedStorage; use OCP\App\IAppManager; +use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCS\OCSBadRequestException; use OCP\AppFramework\OCS\OCSException; @@ -85,9 +89,9 @@ use Psr\Log\LoggerInterface; /** - * Class Share20OCS - * * @package OCA\Files_Sharing\API + * + * @psalm-import-type FilesSharingShare from ResponseDefinitions */ class ShareAPIController extends OCSController { @@ -173,7 +177,7 @@ public function __construct( * * @param \OCP\Share\IShare $share * @param Node|null $recipientNode - * @return array + * @return FilesSharingShare * @throws NotFoundException In case the node can't be resolved. * * @suppress PhanUndeclaredClassMethod @@ -346,6 +350,7 @@ protected function formatShare(IShare $share, Node $recipientNode = null): array $result['attributes'] = \json_encode($attributes->toArray()); } + /** @var FilesSharingShare $result */ return $result; } @@ -481,14 +486,16 @@ private function getCachedFederatedDisplayName(string $userId, bool $cacheOnly = /** + * @NoAdminRequired + * * Get a specific share by id * - * @NoAdminRequired + * @param string $id ID of the share + * @param bool $include_tags Include tags in the share + * @return DataResponse + * @throws OCSNotFoundException Share not found * - * @param string $id - * @param bool $includeTags - * @return DataResponse - * @throws OCSNotFoundException + * 200: Share returned */ public function getShare(string $id, bool $include_tags = false): DataResponse { try { @@ -517,13 +524,16 @@ public function getShare(string $id, bool $include_tags = false): DataResponse { } /** + * @NoAdminRequired + * * Delete a share * - * @NoAdminRequired + * @param string $id ID of the share + * @return DataResponse + * @throws OCSNotFoundException Share not found + * @throws OCSForbiddenException Missing permissions to delete the share * - * @param string $id - * @return DataResponse - * @throws OCSNotFoundException + * 200: Share deleted successfully */ public function deleteShare(string $id): DataResponse { try { @@ -556,31 +566,34 @@ public function deleteShare(string $id): DataResponse { $this->shareManager->deleteShare($share); } - return new DataResponse(); + return new DataResponse(new \stdClass()); } /** * @NoAdminRequired * - * @param string $path - * @param int $permissions - * @param int $shareType - * @param string $shareWith - * @param string $publicUpload - * @param string $password - * @param string $sendPasswordByTalk - * @param string $expireDate - * @param string $label - * @param string $attributes + * Create a share * - * @return DataResponse - * @throws NotFoundException - * @throws OCSBadRequestException + * @param string|null $path Path of the share + * @param int|null $permissions Permissions for the share + * @param int $shareType Type of the share + * @param string|null $shareWith The entity this should be shared with + * @param string $publicUpload If public uploading is allowed + * @param string $password Password for the share + * @param string|null $sendPasswordByTalk Send the password for the share over Talk + * @param string $expireDate Expiry date of the share + * @param string $note Note for the share + * @param string $label Label for the share (only used in link and email) + * @param string|null $attributes Additional attributes for the share + * + * @return DataResponse + * @throws OCSBadRequestException Unknown share type * @throws OCSException - * @throws OCSForbiddenException - * @throws OCSNotFoundException - * @throws InvalidPathException + * @throws OCSForbiddenException Creating the share is not allowed + * @throws OCSNotFoundException Creating the share failed * @suppress PhanUndeclaredClassMethod + * + * 200: Share created */ public function createShare( string $path = null, @@ -850,7 +863,7 @@ public function createShare( * @param null|Node $node * @param boolean $includeTags * - * @return array + * @return FilesSharingShare[] */ private function getSharedWithMe($node, bool $includeTags): array { $userShares = $this->shareManager->getSharedWith($this->currentUser, IShare::TYPE_USER, $node, -1, 0); @@ -887,7 +900,7 @@ private function getSharedWithMe($node, bool $includeTags): array { /** * @param \OCP\Files\Node $folder * - * @return array + * @return FilesSharingShare[] * @throws OCSBadRequestException * @throws NotFoundException */ @@ -939,27 +952,20 @@ private function getSharesInDir(Node $folder): array { } /** - * The getShares function. - * * @NoAdminRequired * - * @param string $shared_with_me - * @param string $reshares - * @param string $subfiles - * @param string $path + * Get shares of the current user * - * - Get shares by the current user - * - Get shares by the current user and reshares (?reshares=true) - * - Get shares with the current user (?shared_with_me=true) - * - Get shares for a specific path (?path=...) - * - Get all shares in a folder (?subfiles=true&path=..) + * @param string $shared_with_me Only get shares with the current user + * @param string $reshares Only get shares by the current user and reshares + * @param string $subfiles Only get all shares in a folder + * @param string $path Get shares for a specific path + * @param string $include_tags Include tags in the share * - * @param string $include_tags + * @return DataResponse + * @throws OCSNotFoundException The folder was not found or is inaccessible * - * @return DataResponse - * @throws NotFoundException - * @throws OCSBadRequestException - * @throws OCSNotFoundException + * 200: Shares returned */ public function getShares( string $shared_with_me = 'false', @@ -1004,7 +1010,7 @@ public function getShares( * @param bool $subFiles * @param bool $includeTags * - * @return array + * @return FilesSharingShare[] * @throws NotFoundException * @throws OCSBadRequestException */ @@ -1083,25 +1089,19 @@ private function getFormattedShares( /** - * The getInheritedShares function. - * returns all shares relative to a file, including parent folders shares rights. - * * @NoAdminRequired * - * @param string $path + * Get all shares relative to a file, including parent folders shares rights * - * - Get shares by the current user - * - Get shares by the current user and reshares (?reshares=true) - * - Get shares with the current user (?shared_with_me=true) - * - Get shares for a specific path (?path=...) - * - Get all shares in a folder (?subfiles=true&path=..) + * @param string $path Path all shares will be relative to * - * @return DataResponse + * @return DataResponse * @throws InvalidPathException * @throws NotFoundException - * @throws OCSNotFoundException - * @throws OCSBadRequestException + * @throws OCSNotFoundException The given path is invalid * @throws SharingRightsException + * + * 200: Shares returned */ public function getInheritedShares(string $path): DataResponse { @@ -1185,22 +1185,24 @@ private function hasPermission(int $permissionsSet, int $permissionsToCheck): bo /** * @NoAdminRequired * - * @param string $id - * @param int $permissions - * @param string $password - * @param string $sendPasswordByTalk - * @param string $publicUpload - * @param string $expireDate - * @param string $note - * @param string $label - * @param string $hideDownload - * @param string $attributes - * @return DataResponse - * @throws LockedException - * @throws NotFoundException - * @throws OCSBadRequestException - * @throws OCSForbiddenException - * @throws OCSNotFoundException + * Update a share + * + * @param string $id ID of the share + * @param int|null $permissions New permissions + * @param string|null $password New password + * @param string|null $sendPasswordByTalk New condition if the password should be send over Talk + * @param string|null $publicUpload New condition if public uploading is allowed + * @param string|null $expireDate New expiry date + * @param string|null $note New note + * @param string|null $label New label + * @param string|null $hideDownload New condition if the download should be hidden + * @param string|null $attributes New additional attributes + * @return DataResponse + * @throws OCSBadRequestException Share could not be updated because the requested changes are invalid + * @throws OCSForbiddenException Missing permissions to update the share + * @throws OCSNotFoundException Share not found + * + * 200: Share updated successfully */ public function updateShare( string $id, @@ -1388,6 +1390,10 @@ public function updateShare( /** * @NoAdminRequired + * + * Get all shares that are still pending + * + * @return DataResponse */ public function pendingShares(): DataResponse { $pendingShares = []; @@ -1439,11 +1445,15 @@ public function pendingShares(): DataResponse { /** * @NoAdminRequired * - * @param string $id - * @return DataResponse - * @throws OCSNotFoundException + * Accept a share + * + * @param string $id ID of the share + * @return DataResponse + * @throws OCSNotFoundException Share not found * @throws OCSException - * @throws OCSBadRequestException + * @throws OCSBadRequestException Share could not be accepted + * + * 200: Share accepted successfully */ public function acceptShare(string $id): DataResponse { try { @@ -1465,7 +1475,7 @@ public function acceptShare(string $id): DataResponse { throw new OCSBadRequestException($e->getMessage(), $e); } - return new DataResponse(); + return new DataResponse(new \stdClass()); } /** diff --git a/apps/files_sharing/lib/Controller/ShareController.php b/apps/files_sharing/lib/Controller/ShareController.php index ae21259e21e7a..376e90fb97afa 100644 --- a/apps/files_sharing/lib/Controller/ShareController.php +++ b/apps/files_sharing/lib/Controller/ShareController.php @@ -24,6 +24,7 @@ * @author Sascha Sambale * @author Thomas Müller * @author Vincent Petry + * @author Kate Döen * * @license AGPL-3.0 * @@ -147,6 +148,7 @@ public function __construct( /** * @PublicPage * @NoCSRFRequired + * @IgnoreAPI * * Show the authentication page * The form has to submit to the authenticate method route @@ -348,7 +350,7 @@ private function validateShare(\OCP\Share\IShare $share) { /** * @PublicPage * @NoCSRFRequired - * + * @IgnoreAPI * * @param string $path * @return TemplateResponse @@ -406,6 +408,7 @@ public function showShare($path = ''): TemplateResponse { * @PublicPage * @NoCSRFRequired * @NoSameSiteCookieRequired + * @IgnoreAPI * * @param string $token * @param string $files diff --git a/apps/files_sharing/lib/Controller/ShareInfoController.php b/apps/files_sharing/lib/Controller/ShareInfoController.php index b6242f9ee9a07..4bc941e62fef1 100644 --- a/apps/files_sharing/lib/Controller/ShareInfoController.php +++ b/apps/files_sharing/lib/Controller/ShareInfoController.php @@ -4,6 +4,7 @@ * * @author Morris Jobke * @author Roeland Jago Douma + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -24,6 +25,7 @@ namespace OCA\Files_Sharing\Controller; use OCA\Files_External\NotFoundException; +use OCA\Files_Sharing\ResponseDefinitions; use OCP\AppFramework\ApiController; use OCP\AppFramework\Http; use OCP\AppFramework\Http\JSONResponse; @@ -35,6 +37,9 @@ use OCP\Share\Exceptions\ShareNotFound; use OCP\Share\IManager; +/** + * @psalm-import-type FilesSharingShareInfo from ResponseDefinitions + */ class ShareInfoController extends ApiController { /** @var IManager */ @@ -60,28 +65,35 @@ public function __construct(string $appName, * @NoCSRFRequired * @BruteForceProtection(action=shareinfo) * - * @param string $t - * @param ?string $password - * @param ?string $dir - * @return JSONResponse + * Get the info about a share + * + * @param string $t Token of the share + * @param string|null $password Password of the share + * @param string|null $dir Subdirectory to get info about + * @param int $depth Maximum depth to get info about + * @return JSONResponse|JSONResponse + * + * 200: Share info returned + * 403: Getting share info is not allowed + * 404: Share not found */ public function info(string $t, ?string $password = null, ?string $dir = null, int $depth = -1): JSONResponse { try { $share = $this->shareManager->getShareByToken($t); } catch (ShareNotFound $e) { - $response = new JSONResponse([], Http::STATUS_NOT_FOUND); + $response = new JSONResponse(new \stdClass(), Http::STATUS_NOT_FOUND); $response->throttle(['token' => $t]); return $response; } if ($share->getPassword() && !$this->shareManager->checkPassword($share, $password)) { - $response = new JSONResponse([], Http::STATUS_FORBIDDEN); + $response = new JSONResponse(new \stdClass(), Http::STATUS_FORBIDDEN); $response->throttle(['token' => $t]); return $response; } if (!($share->getPermissions() & Constants::PERMISSION_READ)) { - $response = new JSONResponse([], Http::STATUS_FORBIDDEN); + $response = new JSONResponse(new \stdClass(), Http::STATUS_FORBIDDEN); $response->throttle(['token' => $t]); return $response; } @@ -99,6 +111,9 @@ public function info(string $t, ?string $password = null, ?string $dir = null, i return new JSONResponse($this->parseNode($node, $permissionMask, $depth)); } + /** + * @return FilesSharingShareInfo + */ private function parseNode(Node $node, int $permissionMask, int $depth): array { if ($node instanceof File) { return $this->parseFile($node, $permissionMask); @@ -107,10 +122,16 @@ private function parseNode(Node $node, int $permissionMask, int $depth): array { return $this->parseFolder($node, $permissionMask, $depth); } + /** + * @return FilesSharingShareInfo + */ private function parseFile(File $file, int $permissionMask): array { return $this->format($file, $permissionMask); } + /** + * @return FilesSharingShareInfo + */ private function parseFolder(Folder $folder, int $permissionMask, int $depth): array { $data = $this->format($folder, $permissionMask); @@ -128,6 +149,9 @@ private function parseFolder(Folder $folder, int $permissionMask, int $depth): a return $data; } + /** + * @return FilesSharingShareInfo + */ private function format(Node $node, int $permissionMask): array { $entry = []; diff --git a/apps/files_sharing/lib/Controller/ShareesAPIController.php b/apps/files_sharing/lib/Controller/ShareesAPIController.php index 8daa7dc5ab9c7..fd0990bed0888 100644 --- a/apps/files_sharing/lib/Controller/ShareesAPIController.php +++ b/apps/files_sharing/lib/Controller/ShareesAPIController.php @@ -43,6 +43,8 @@ use function array_values; use Generator; use OC\Collaboration\Collaborators\SearchResult; +use OCA\Files_Sharing\ResponseDefinitions; +use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCS\OCSBadRequestException; use OCP\AppFramework\OCSController; @@ -56,6 +58,10 @@ use OCP\Share\IManager; use function usort; +/** + * @psalm-import-type FilesSharingShareesSearchResult from ResponseDefinitions + * @psalm-import-type FilesSharingShareesRecommendedResult from ResponseDefinitions + */ class ShareesAPIController extends OCSController { /** @var string */ @@ -76,7 +82,7 @@ class ShareesAPIController extends OCSController { /** @var int */ protected $limit = 10; - /** @var array */ + /** @var FilesSharingShareesSearchResult */ protected $result = [ 'exact' => [ 'users' => [], @@ -131,14 +137,18 @@ public function __construct( /** * @NoAdminRequired * - * @param string $search - * @param string $itemType - * @param int $page - * @param int $perPage - * @param int|int[] $shareType - * @param bool $lookup - * @return DataResponse - * @throws OCSBadRequestException + * Search for sharees + * + * @param string $search Text to search for + * @param string|null $itemType Limit to specific item types + * @param int $page Page offset for searching + * @param int $perPage Limit amount of search results per page + * @param int|int[]|null $shareType Limit to specific share types + * @param bool $lookup If a global lookup should be performed too + * @return DataResponse + * @throws OCSBadRequestException Invalid search parameters + * + * 200: Sharees search result returned */ public function search(string $search = '', string $itemType = null, int $page = 1, int $perPage = 200, $shareType = null, bool $lookup = false): DataResponse { @@ -231,18 +241,12 @@ public function search(string $search = '', string $itemType = null, int $page = $result['exact'] = array_merge($this->result['exact'], $result['exact']); } $this->result = array_merge($this->result, $result); - $response = new DataResponse($this->result); - - if ($hasMoreResults) { - $response->addHeader('Link', $this->getPaginationLink($page, [ - 'search' => $search, - 'itemType' => $itemType, - 'shareType' => $shareTypes, - 'perPage' => $perPage, - ])); - } - - return $response; + return new DataResponse($this->result, Http::STATUS_OK, $hasMoreResults ? ['Link' => $this->getPaginationLink($page, [ + 'search' => $search, + 'itemType' => $itemType, + 'shareType' => $shareTypes, + 'perPage' => $perPage, + ])] : []); } /** @@ -333,18 +337,18 @@ private function getAllSharees(string $user, array $shareTypes): ISearchResult { /** * @NoAdminRequired * - * @param string $itemType - * @return DataResponse - * @throws OCSBadRequestException + * Find recommended sharees + * + * @param string $itemType Limit to specific item types + * @param int|int[]|null $shareType Limit to specific share types + * @return DataResponse */ - public function findRecommended(string $itemType = null, $shareType = null): DataResponse { + public function findRecommended(string $itemType, $shareType = null): DataResponse { $shareTypes = [ IShare::TYPE_USER, ]; - if ($itemType === null) { - throw new OCSBadRequestException('Missing itemType'); - } elseif ($itemType === 'file' || $itemType === 'folder') { + if ($itemType === 'file' || $itemType === 'folder') { if ($this->shareManager->allowGroupSharing()) { $shareTypes[] = IShare::TYPE_GROUP; } diff --git a/apps/files_sharing/lib/ResponseDefinitions.php b/apps/files_sharing/lib/ResponseDefinitions.php new file mode 100644 index 0000000000000..4e7bc7da8e2ba --- /dev/null +++ b/apps/files_sharing/lib/ResponseDefinitions.php @@ -0,0 +1,237 @@ + + * + * @author Kate Döen + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Files_Sharing; + +/** + * @psalm-type FilesSharingShare = array{ + * attributes: string|null, + * can_delete: bool, + * can_edit: bool, + * displayname_file_owner: string, + * displayname_owner: string, + * expiration: string|null, + * file_parent: int, + * file_source: int, + * file_target: string, + * has_preview: bool, + * id: string, + * item_source: int, + * item_type: string, + * label: string, + * mail_send: int, + * mimetype: string, + * note: string, + * password: string|null, + * password_expiration_time: string|null, + * path: string, + * permissions: int, + * send_password_by_talk: bool|null, + * share_type: int, + * share_with: string|null, + * share_with_avatar: string|null, + * share_with_displayname: string|null, + * share_with_link: string|null, + * status: array{status: string, message: string|null, icon: string|null, clearAt: int|null}|int|null, + * stime: int, + * storage: int, + * storage_id: string, + * token: string|null, + * uid_file_owner: string, + * uid_owner: string, + * url: string|null, + * } + * + * @psalm-type FilesSharingDeletedShare = array{ + * id: string, + * share_type: int, + * uid_owner: string, + * displayname_owner: string, + * permissions: int, + * stime: int, + * uid_file_owner: string, + * displayname_file_owner: string, + * path: string, + * item_type: string, + * mimetype: string, + * storage: int, + * item_source: int, + * file_source: int, + * file_parent: int, + * file_target: int, + * expiration: string|null, + * share_with: string|null, + * share_with_displayname: string|null, + * share_with_link: string|null, + * } + * + * @psalm-type FilesSharingRemoteShare = array{ + * accepted: bool, + * file_id: int|null, + * id: int, + * mimetype: string|null, + * mountpoint: string, + * mtime: int|null, + * name: string, + * owner: string, + * parent: int|null, + * permissions: int|null, + * remote: string, + * remote_id: string, + * share_token: string, + * share_type: int, + * type: string|null, + * user: string, + * } + * + * @psalm-type FilesSharingSharee = array{ + * count: int|null, + * label: string, + * } + * + * @psalm-type FilesSharingShareeValue = array{ + * shareType: int, + * shareWith: string, + * } + * + * @psalm-type FilesSharingShareeUser = FilesSharingSharee&array{ + * subline: string, + * icon: string, + * shareWithDisplayNameUnique: string, + * status: array{ + * status: string, + * message: string, + * icon: string, + * clearAt: int|null, + * }, + * value: FilesSharingShareeValue, + * } + * + * @psalm-type FilesSharingShareeRemoteGroup = FilesSharingSharee&array{ + * guid: string, + * name: string, + * value: FilesSharingShareeValue&array{ + * server: string, + * } + * } + * + * @psalm-type FilesSharingLookup = array{ + * value: string, + * verified: int, + * } + * + * @psalm-type FilesSharingShareeLookup = FilesSharingSharee&array{ + * extra: array{ + * federationId: string, + * name: FilesSharingLookup|null, + * email: FilesSharingLookup|null, + * address: FilesSharingLookup|null, + * website: FilesSharingLookup|null, + * twitter: FilesSharingLookup|null, + * phone: FilesSharingLookup|null, + * twitter_signature: FilesSharingLookup|null, + * website_signature: FilesSharingLookup|null, + * userid: FilesSharingLookup|null, + * }, + * value: FilesSharingShareeValue&array{ + * globalScale: bool, + * } + * } + * + * @psalm-type FilesSharingShareeEmail = FilesSharingSharee&array{ + * uuid: string, + * name: string, + * type: string, + * shareWithDisplayNameUnique: string, + * value: FilesSharingShareeValue, + * } + * + * @psalm-type FilesSharingShareeRemote = FilesSharingSharee&array{ + * uuid: string, + * name: string, + * type: string, + * value: FilesSharingShareeValue&array{ + * server: string, + * } + * } + * + * @psalm-type FilesSharingShareeCircle = FilesSharingSharee&array{ + * shareWithDescription: string, + * value: FilesSharingShareeValue&array{ + * circle: string, + * } + * } + * + * @psalm-type FilesSharingShareesSearchResult = array{ + * exact: array{ + * circles: FilesSharingShareeCircle[], + * emails: FilesSharingShareeEmail[], + * groups: FilesSharingSharee[], + * remote_groups: FilesSharingShareeRemoteGroup[], + * remotes: FilesSharingShareeRemote[], + * rooms: FilesSharingSharee[], + * users: FilesSharingShareeUser[], + * }, + * circles: FilesSharingShareeCircle[], + * emails: FilesSharingShareeEmail[], + * groups: FilesSharingSharee[], + * lookup: FilesSharingShareeLookup[], + * remote_groups: FilesSharingShareeRemoteGroup[], + * remotes: FilesSharingShareeRemote[], + * rooms: FilesSharingSharee[], + * users: FilesSharingShareeUser[], + * lookupEnabled: bool, + * } + * + * @psalm-type FilesSharingShareesRecommendedResult = array{ + * exact: array{ + * emails: FilesSharingShareeEmail[], + * groups: FilesSharingSharee[], + * remote_groups: FilesSharingShareeRemoteGroup[], + * remotes: FilesSharingShareeRemote[], + * users: FilesSharingShareeUser[], + * }, + * emails: FilesSharingShareeEmail[], + * groups: FilesSharingSharee[], + * remote_groups: FilesSharingShareeRemoteGroup[], + * remotes: FilesSharingShareeRemote[], + * users: FilesSharingShareeUser[], + * } + * + * @psalm-type FilesSharingShareInfo = array{ + * id: int, + * parentId: int, + * mtime: int, + * name: string, + * permissions: int, + * mimetype: string, + * size: int|float, + * type: string, + * etag: string, + * children?: array[], + * } + */ +class ResponseDefinitions { +} diff --git a/apps/files_sharing/openapi.json b/apps/files_sharing/openapi.json index 59f94c5a5ea39..96291a0c31543 100644 --- a/apps/files_sharing/openapi.json +++ b/apps/files_sharing/openapi.json @@ -676,8 +676,7 @@ "mimetype", "size", "type", - "etag", - "children" + "etag" ], "properties": { "id": { @@ -703,8 +702,16 @@ "type": "string" }, "size": { - "type": "integer", - "format": "int64" + "oneOf": [ + { + "type": "integer", + "format": "int64" + }, + { + "type": "number", + "format": "float" + } + ] }, "type": { "type": "string" @@ -714,7 +721,6 @@ }, "children": { "type": "array", - "nullable": true, "items": { "type": "object", "additionalProperties": { @@ -1339,7 +1345,7 @@ } }, "403": { - "description": "Getting share info not allowed", + "description": "Getting share info is not allowed", "content": { "application/json": { "schema": { @@ -2498,6 +2504,13 @@ "responses": { "200": { "description": "Sharees search result returned", + "headers": { + "Link": { + "schema": { + "type": "string" + } + } + }, "content": { "application/json": { "schema": { @@ -3060,7 +3073,7 @@ } }, "403": { - "description": "Unsharing not possible", + "description": "Unsharing is not possible", "content": { "text/plain": { "schema": { diff --git a/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php b/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php index 2a2a7d940be5b..df7675abffff1 100644 --- a/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php +++ b/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php @@ -253,7 +253,7 @@ public function testDeleteShare() { ->method('lock') ->with(\OCP\Lock\ILockingProvider::LOCK_SHARED); - $expected = new DataResponse(); + $expected = new DataResponse(new \stdClass()); $result = $this->ocs->deleteShare(42); $this->assertInstanceOf(get_class($expected), $result); diff --git a/apps/files_sharing/tests/Controller/ShareInfoControllerTest.php b/apps/files_sharing/tests/Controller/ShareInfoControllerTest.php index 18bda2f488d1c..a64b76d2cee08 100644 --- a/apps/files_sharing/tests/Controller/ShareInfoControllerTest.php +++ b/apps/files_sharing/tests/Controller/ShareInfoControllerTest.php @@ -64,7 +64,7 @@ public function testNoShare() { ->with('token') ->willThrowException(new ShareNotFound()); - $expected = new JSONResponse([], Http::STATUS_NOT_FOUND); + $expected = new JSONResponse(new \stdClass(), Http::STATUS_NOT_FOUND); $expected->throttle(['token' => 'token']); $this->assertEquals($expected, $this->controller->info('token')); } @@ -81,7 +81,7 @@ public function testWrongPassword() { ->with($share, 'pass') ->willReturn(false); - $expected = new JSONResponse([], Http::STATUS_FORBIDDEN); + $expected = new JSONResponse(new \stdClass(), Http::STATUS_FORBIDDEN); $expected->throttle(['token' => 'token']); $this->assertEquals($expected, $this->controller->info('token', 'pass')); } @@ -100,7 +100,7 @@ public function testNoReadPermissions() { ->with($share, 'pass') ->willReturn(true); - $expected = new JSONResponse([], Http::STATUS_FORBIDDEN); + $expected = new JSONResponse(new \stdClass(), Http::STATUS_FORBIDDEN); $expected->throttle(['token' => 'token']); $this->assertEquals($expected, $this->controller->info('token', 'pass')); } diff --git a/apps/files_trashbin/lib/Capabilities.php b/apps/files_trashbin/lib/Capabilities.php index c0788ff7308a7..b53881daa29bc 100644 --- a/apps/files_trashbin/lib/Capabilities.php +++ b/apps/files_trashbin/lib/Capabilities.php @@ -33,6 +33,8 @@ class Capabilities implements ICapability { /** * Return this classes capabilities + * + * @return array{files: array{undelete: bool}} */ public function getCapabilities() { return [ diff --git a/apps/files_trashbin/lib/Controller/PreviewController.php b/apps/files_trashbin/lib/Controller/PreviewController.php index 652570dccd70b..562a3e0bef136 100644 --- a/apps/files_trashbin/lib/Controller/PreviewController.php +++ b/apps/files_trashbin/lib/Controller/PreviewController.php @@ -85,7 +85,18 @@ public function __construct( * @NoAdminRequired * @NoCSRFRequired * - * @return DataResponse|Http\FileDisplayResponse + * Get the preview for a file + * + * @param int $fileId ID of the file + * @param int $x Width of the preview + * @param int $y Height of the preview + * @param bool $a Whether to crop the preview + * + * @return Http\FileDisplayResponse|DataResponse + * + * 200: Preview returned + * 400: Getting preview is not possible + * 404: Preview not found */ public function getPreview( int $fileId = -1, @@ -94,16 +105,16 @@ public function getPreview( bool $a = false, ) { if ($fileId === -1 || $x === 0 || $y === 0) { - return new DataResponse([], Http::STATUS_BAD_REQUEST); + return new DataResponse(new \stdClass(), Http::STATUS_BAD_REQUEST); } try { $file = $this->trashManager->getTrashNodeById($this->userSession->getUser(), $fileId); if ($file === null) { - return new DataResponse([], Http::STATUS_NOT_FOUND); + return new DataResponse(new \stdClass(), Http::STATUS_NOT_FOUND); } if ($file instanceof Folder) { - return new DataResponse([], Http::STATUS_BAD_REQUEST); + return new DataResponse(new \stdClass(), Http::STATUS_BAD_REQUEST); } $pathParts = pathinfo($file->getName()); @@ -126,9 +137,9 @@ public function getPreview( $response->cacheFor(3600 * 24); return $response; } catch (NotFoundException $e) { - return new DataResponse([], Http::STATUS_NOT_FOUND); + return new DataResponse(new \stdClass(), Http::STATUS_NOT_FOUND); } catch (\InvalidArgumentException $e) { - return new DataResponse([], Http::STATUS_BAD_REQUEST); + return new DataResponse(new \stdClass(), Http::STATUS_BAD_REQUEST); } } } diff --git a/apps/files_trashbin/openapi.json b/apps/files_trashbin/openapi.json new file mode 100644 index 0000000000000..e2203e4c95a50 --- /dev/null +++ b/apps/files_trashbin/openapi.json @@ -0,0 +1,140 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "files_trashbin", + "version": "0.0.1", + "description": "This application enables users to restore files that were deleted from the system.", + "license": { + "name": "agpl" + } + }, + "components": { + "securitySchemes": { + "basic_auth": { + "type": "http", + "scheme": "basic" + }, + "bearer_auth": { + "type": "http", + "scheme": "bearer" + } + }, + "schemas": { + "Capabilities": { + "type": "object", + "required": [ + "files" + ], + "properties": { + "files": { + "type": "object", + "required": [ + "undelete" + ], + "properties": { + "undelete": { + "type": "boolean" + } + } + } + } + } + } + }, + "paths": { + "/index.php/apps/files_trashbin/preview": { + "get": { + "operationId": "preview-get-preview", + "summary": "Get the preview for a file", + "tags": [ + "preview" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "fileId", + "in": "query", + "description": "ID of the file", + "schema": { + "type": "integer", + "format": "int64", + "default": 1 + } + }, + { + "name": "x", + "in": "query", + "description": "Width of the preview", + "schema": { + "type": "integer", + "format": "int64", + "default": 32 + } + }, + { + "name": "y", + "in": "query", + "description": "Height of the preview", + "schema": { + "type": "integer", + "format": "int64", + "default": 32 + } + }, + { + "name": "a", + "in": "query", + "description": "Whether to crop the preview", + "schema": { + "type": "integer", + "default": 0 + } + } + ], + "responses": { + "200": { + "description": "Preview returned", + "content": { + "*/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "400": { + "description": "Getting preview is not possible", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "404": { + "description": "Preview not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + } + } + } + }, + "tags": [] +} \ No newline at end of file diff --git a/apps/files_versions/lib/Capabilities.php b/apps/files_versions/lib/Capabilities.php index 6439c18772b51..5da5f93965b4d 100644 --- a/apps/files_versions/lib/Capabilities.php +++ b/apps/files_versions/lib/Capabilities.php @@ -42,6 +42,8 @@ public function __construct( /** * Return this classes capabilities + * + * @return array{files: array{versioning: bool, version_labeling: bool, version_deletion: bool}} */ public function getCapabilities() { $groupFolderInstalled = $this->appManager->isInstalled('groupfolders'); diff --git a/apps/files_versions/lib/Controller/PreviewController.php b/apps/files_versions/lib/Controller/PreviewController.php index 0e625dc213997..684aecd7c7d31 100644 --- a/apps/files_versions/lib/Controller/PreviewController.php +++ b/apps/files_versions/lib/Controller/PreviewController.php @@ -69,11 +69,17 @@ public function __construct( * @NoAdminRequired * @NoCSRFRequired * - * @param string $file - * @param int $x - * @param int $y - * @param string $version - * @return DataResponse|FileDisplayResponse + * Get the preview for a file version + * + * @param string $file Path of the file + * @param int $x Width of the preview + * @param int $y Height of the preview + * @param string $version Version of the file to get the preview for + * @return FileDisplayResponse|DataResponse + * + * 200: Preview returned + * 400: Getting preview is not possible + * 404: Preview not found */ public function getPreview( string $file = '', @@ -82,7 +88,7 @@ public function getPreview( string $version = '' ) { if ($file === '' || $version === '' || $x === 0 || $y === 0) { - return new DataResponse([], Http::STATUS_BAD_REQUEST); + return new DataResponse(new \stdClass(), Http::STATUS_BAD_REQUEST); } try { @@ -93,9 +99,9 @@ public function getPreview( $preview = $this->previewManager->getPreview($versionFile, $x, $y, true, IPreview::MODE_FILL, $versionFile->getMimetype()); return new FileDisplayResponse($preview, Http::STATUS_OK, ['Content-Type' => $preview->getMimeType()]); } catch (NotFoundException $e) { - return new DataResponse([], Http::STATUS_NOT_FOUND); + return new DataResponse(new \stdClass(), Http::STATUS_NOT_FOUND); } catch (\InvalidArgumentException $e) { - return new DataResponse([], Http::STATUS_BAD_REQUEST); + return new DataResponse(new \stdClass(), Http::STATUS_BAD_REQUEST); } } } diff --git a/apps/files_versions/openapi.json b/apps/files_versions/openapi.json new file mode 100644 index 0000000000000..a278ea4496c78 --- /dev/null +++ b/apps/files_versions/openapi.json @@ -0,0 +1,147 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "files_versions", + "version": "0.0.1", + "description": "This application automatically maintains older versions of files that are changed.", + "license": { + "name": "agpl" + } + }, + "components": { + "securitySchemes": { + "basic_auth": { + "type": "http", + "scheme": "basic" + }, + "bearer_auth": { + "type": "http", + "scheme": "bearer" + } + }, + "schemas": { + "Capabilities": { + "type": "object", + "required": [ + "files" + ], + "properties": { + "files": { + "type": "object", + "required": [ + "versioning", + "version_labeling", + "version_deletion" + ], + "properties": { + "versioning": { + "type": "boolean" + }, + "version_labeling": { + "type": "boolean" + }, + "version_deletion": { + "type": "boolean" + } + } + } + } + } + } + }, + "paths": { + "/index.php/apps/files_versions/preview": { + "get": { + "operationId": "preview-get-preview", + "summary": "Get the preview for a file version", + "tags": [ + "preview" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "file", + "in": "query", + "description": "Path of the file", + "schema": { + "type": "string", + "default": "" + } + }, + { + "name": "x", + "in": "query", + "description": "Width of the preview", + "schema": { + "type": "integer", + "format": "int64", + "default": 44 + } + }, + { + "name": "y", + "in": "query", + "description": "Height of the preview", + "schema": { + "type": "integer", + "format": "int64", + "default": 44 + } + }, + { + "name": "version", + "in": "query", + "description": "Version of the file to get the preview for", + "schema": { + "type": "string", + "default": "" + } + } + ], + "responses": { + "200": { + "description": "Preview returned", + "content": { + "*/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "400": { + "description": "Getting preview is not possible", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "404": { + "description": "Preview not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + } + } + } + }, + "tags": [] +} \ No newline at end of file diff --git a/apps/oauth2/lib/Controller/LoginRedirectorController.php b/apps/oauth2/lib/Controller/LoginRedirectorController.php index 57f18a97f85c5..b8b09c80b2722 100644 --- a/apps/oauth2/lib/Controller/LoginRedirectorController.php +++ b/apps/oauth2/lib/Controller/LoginRedirectorController.php @@ -8,6 +8,7 @@ * @author Daniel Kesselberg * @author Lukas Reschke * @author Roeland Jago Douma + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -30,8 +31,8 @@ use OCA\OAuth2\Db\ClientMapper; use OCA\OAuth2\Exceptions\ClientNotFoundException; use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; use OCP\AppFramework\Http\RedirectResponse; -use OCP\AppFramework\Http\Response; use OCP\AppFramework\Http\TemplateResponse; use OCP\IL10N; use OCP\IRequest; @@ -74,14 +75,19 @@ public function __construct(string $appName, * @NoCSRFRequired * @UseSession * - * @param string $client_id - * @param string $state - * @param string $response_type - * @return Response + * Authorize the user + * + * @param string $client_id Client ID + * @param string $state State of the flow + * @param string $response_type Response type for the flow + * @return TemplateResponse|RedirectResponse + * + * 200: Client not found + * 303: Redirect to login URL */ public function authorize($client_id, $state, - $response_type): Response { + $response_type) { try { $client = $this->clientMapper->getByIdentifier($client_id); } catch (ClientNotFoundException $e) { diff --git a/apps/oauth2/lib/Controller/OauthApiController.php b/apps/oauth2/lib/Controller/OauthApiController.php index e07a2c2de1579..8cdd813244176 100644 --- a/apps/oauth2/lib/Controller/OauthApiController.php +++ b/apps/oauth2/lib/Controller/OauthApiController.php @@ -8,6 +8,7 @@ * @author Christoph Wurst * @author Lukas Reschke * @author Roeland Jago Douma + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -66,12 +67,17 @@ public function __construct( * @NoCSRFRequired * @BruteForceProtection(action=oauth2GetToken) * - * @param string $grant_type - * @param string $code - * @param string $refresh_token - * @param string $client_id - * @param string $client_secret - * @return JSONResponse + * Get a token + * + * @param string $grant_type Token type that should be granted + * @param string $code Code of the flow + * @param string $refresh_token Refresh token + * @param string $client_id Client ID + * @param string $client_secret Client secret + * @return JSONResponse|JSONResponse + * + * 200: Token returned + * 400: Getting token is not possible */ public function getToken($grant_type, $code, $refresh_token, $client_id, $client_secret): JSONResponse { diff --git a/apps/oauth2/openapi.json b/apps/oauth2/openapi.json index 57a7bee1a64b4..a9903ae3473e5 100644 --- a/apps/oauth2/openapi.json +++ b/apps/oauth2/openapi.json @@ -19,7 +19,7 @@ "scheme": "bearer" } }, - "schemas": [] + "schemas": {} }, "paths": { "/index.php/apps/oauth2/authorize": { diff --git a/apps/provisioning_api/lib/Capabilities.php b/apps/provisioning_api/lib/Capabilities.php index 614c20e66a8a1..3504868fcf32e 100644 --- a/apps/provisioning_api/lib/Capabilities.php +++ b/apps/provisioning_api/lib/Capabilities.php @@ -3,6 +3,7 @@ * @copyright Copyright (c) 2021 Vincent Petry * * @author Vincent Petry + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -37,6 +38,15 @@ public function __construct(IAppManager $appManager) { /** * Function an app uses to return the capabilities + * + * @return array{ + * provisioning_api: array{ + * version: string, + * AccountPropertyScopesVersion: int, + * AccountPropertyScopesFederatedEnabled: bool, + * AccountPropertyScopesPublishedEnabled: bool, + * }, + * } */ public function getCapabilities() { $federatedScopeEnabled = $this->appManager->isEnabledForUser('federation'); diff --git a/apps/provisioning_api/lib/Controller/AUserData.php b/apps/provisioning_api/lib/Controller/AUserData.php index 128fda588e281..7ce1484cf714c 100644 --- a/apps/provisioning_api/lib/Controller/AUserData.php +++ b/apps/provisioning_api/lib/Controller/AUserData.php @@ -36,6 +36,7 @@ use OC\User\Backend; use OC\User\NoUserException; use OC_Helper; +use OCA\Provisioning_API\ResponseDefinitions; use OCP\Accounts\IAccountManager; use OCP\Accounts\PropertyDoesNotExistException; use OCP\AppFramework\Http; @@ -52,6 +53,10 @@ use OCP\User\Backend\ISetDisplayNameBackend; use OCP\User\Backend\ISetPasswordBackend; +/** + * @psalm-import-type ProvisioningApiUserDetails from ResponseDefinitions + * @psalm-import-type ProvisioningApiUserDetailsQuota from ResponseDefinitions + */ abstract class AUserData extends OCSController { public const SCOPE_SUFFIX = 'Scope'; @@ -99,12 +104,12 @@ public function __construct(string $appName, * * @param string $userId * @param bool $includeScopes - * @return array + * @return ProvisioningApiUserDetails|null * @throws NotFoundException * @throws OCSException * @throws OCSNotFoundException */ - protected function getUserData(string $userId, bool $includeScopes = false): array { + protected function getUserData(string $userId, bool $includeScopes = false): ?array { $currentLoggedInUser = $this->userSession->getUser(); assert($currentLoggedInUser !== null, 'No user logged in'); @@ -123,7 +128,7 @@ protected function getUserData(string $userId, bool $includeScopes = false): arr } else { // Check they are looking up themselves if ($currentLoggedInUser->getUID() !== $targetUserObject->getUID()) { - return $data; + return null; } } @@ -225,7 +230,7 @@ protected function getUserData(string $userId, bool $includeScopes = false): arr * Get the groups a user is a subadmin of * * @param string $userId - * @return array + * @return string[] * @throws OCSException */ protected function getUserSubAdminGroupsData(string $userId): array { @@ -247,7 +252,7 @@ protected function getUserSubAdminGroupsData(string $userId): array { /** * @param string $userId - * @return array + * @return ProvisioningApiUserDetailsQuota * @throws OCSException */ protected function fillStorageInfo(string $userId): array { diff --git a/apps/provisioning_api/lib/Controller/AppConfigController.php b/apps/provisioning_api/lib/Controller/AppConfigController.php index 929676be16ec1..4092830e0d3e0 100644 --- a/apps/provisioning_api/lib/Controller/AppConfigController.php +++ b/apps/provisioning_api/lib/Controller/AppConfigController.php @@ -7,6 +7,7 @@ * * @author Joas Schilling * @author Roeland Jago Douma + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -84,7 +85,9 @@ public function __construct(string $appName, } /** - * @return DataResponse + * Get a list of apps + * + * @return DataResponse */ public function getApps(): DataResponse { return new DataResponse([ @@ -93,8 +96,13 @@ public function getApps(): DataResponse { } /** - * @param string $app - * @return DataResponse + * Get the config keys of an app + * + * @param string $app ID of the app + * @return DataResponse|DataResponse + * + * 200: Keys returned + * 403: App is not allowed */ public function getKeys(string $app): DataResponse { try { @@ -108,10 +116,15 @@ public function getKeys(string $app): DataResponse { } /** - * @param string $app - * @param string $key - * @param string $defaultValue - * @return DataResponse + * Get a the config value of an app + * + * @param string $app ID if the app + * @param string $key Key + * @param string $defaultValue Default returned value if the value is empty + * @return DataResponse|DataResponse + * + * 200: Value returned + * 403: App is not allowed */ public function getValue(string $app, string $key, string $defaultValue = ''): DataResponse { try { @@ -128,10 +141,16 @@ public function getValue(string $app, string $key, string $defaultValue = ''): D * @PasswordConfirmationRequired * @NoSubAdminRequired * @NoAdminRequired - * @param string $app - * @param string $key - * @param string $value - * @return DataResponse + * + * Update the config value of an app + * + * @param string $app ID of the app + * @param string $key Key to update + * @param string $value New value for the key + * @return DataResponse|DataResponse + * + * 200: Value updated successfully + * 403: App or key is not allowed */ public function setValue(string $app, string $key, string $value): DataResponse { $user = $this->userSession->getUser(); @@ -151,14 +170,20 @@ public function setValue(string $app, string $key, string $value): DataResponse } $this->config->setAppValue($app, $key, $value); - return new DataResponse(); + return new DataResponse(new \stdClass()); } /** * @PasswordConfirmationRequired - * @param string $app - * @param string $key - * @return DataResponse + * + * Delete a config key of an app + * + * @param string $app ID of the app + * @param string $key Key to delete + * @return DataResponse|DataResponse + * + * 200: Key deleted successfully + * 403: App or key is not allowed */ public function deleteKey(string $app, string $key): DataResponse { try { @@ -169,7 +194,7 @@ public function deleteKey(string $app, string $key): DataResponse { } $this->config->deleteAppValue($app, $key); - return new DataResponse(); + return new DataResponse(new \stdClass()); } /** diff --git a/apps/provisioning_api/lib/Controller/AppsController.php b/apps/provisioning_api/lib/Controller/AppsController.php index fa0f2597e7fde..382a927c1d2cf 100644 --- a/apps/provisioning_api/lib/Controller/AppsController.php +++ b/apps/provisioning_api/lib/Controller/AppsController.php @@ -10,6 +10,7 @@ * @author Lukas Reschke * @author Roeland Jago Douma * @author Tom Needham + * @author Kate Döen * * @license AGPL-3.0 * @@ -29,13 +30,18 @@ namespace OCA\Provisioning_API\Controller; use OC_App; +use OCA\Provisioning_API\ResponseDefinitions; use OCP\App\AppPathNotFoundException; use OCP\App\IAppManager; +use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCS\OCSException; use OCP\AppFramework\OCSController; use OCP\IRequest; +/** + * @psalm-import-type ProvisioningApiAppInfo from ResponseDefinitions + */ class AppsController extends OCSController { /** @var IAppManager */ private $appManager; @@ -51,8 +57,10 @@ public function __construct( } /** - * @param string|null $filter - * @return DataResponse + * Get a list of installed apps + * + * @param string|null $filter Filter for enabled or disabled apps + * @return DataResponse * @throws OCSException */ public function getApps(string $filter = null): DataResponse { @@ -61,6 +69,7 @@ public function getApps(string $filter = null): DataResponse { foreach ($apps as $app) { $list[] = $app['id']; } + /** @var string[] $list */ if ($filter) { switch ($filter) { case 'enabled': @@ -80,8 +89,10 @@ public function getApps(string $filter = null): DataResponse { } /** - * @param string $app - * @return DataResponse + * Get the app info for an app + * + * @param string $app ID of the app + * @return DataResponse * @throws OCSException */ public function getAppInfo(string $app): DataResponse { @@ -95,8 +106,11 @@ public function getAppInfo(string $app): DataResponse { /** * @PasswordConfirmationRequired - * @param string $app - * @return DataResponse + * + * Enable an app + * + * @param string $app ID of the app + * @return DataResponse * @throws OCSException */ public function enable(string $app): DataResponse { @@ -105,16 +119,19 @@ public function enable(string $app): DataResponse { } catch (AppPathNotFoundException $e) { throw new OCSException('The request app was not found', OCSController::RESPOND_NOT_FOUND); } - return new DataResponse(); + return new DataResponse(new \stdClass()); } /** * @PasswordConfirmationRequired - * @param string $app - * @return DataResponse + * + * Disable an app + * + * @param string $app ID of the app + * @return DataResponse */ public function disable(string $app): DataResponse { $this->appManager->disableApp($app); - return new DataResponse(); + return new DataResponse(new \stdClass()); } } diff --git a/apps/provisioning_api/lib/Controller/GroupsController.php b/apps/provisioning_api/lib/Controller/GroupsController.php index e7e2a666b7bb4..08decb11e4e0f 100644 --- a/apps/provisioning_api/lib/Controller/GroupsController.php +++ b/apps/provisioning_api/lib/Controller/GroupsController.php @@ -14,6 +14,7 @@ * @author Robin Appelman * @author Roeland Jago Douma * @author Tom Needham + * @author Kate Döen * * @license AGPL-3.0 * @@ -32,7 +33,9 @@ */ namespace OCA\Provisioning_API\Controller; +use OCA\Provisioning_API\ResponseDefinitions; use OCP\Accounts\IAccountManager; +use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCS\OCSException; use OCP\AppFramework\OCS\OCSForbiddenException; @@ -48,6 +51,10 @@ use OCP\L10N\IFactory; use Psr\Log\LoggerInterface; +/** + * @psalm-import-type ProvisioningApiGroupDetails from ResponseDefinitions + * @psalm-import-type ProvisioningApiUserDetails from ResponseDefinitions + */ class GroupsController extends AUserData { /** @var LoggerInterface */ @@ -76,14 +83,14 @@ public function __construct(string $appName, } /** - * returns a list of groups - * * @NoAdminRequired * - * @param string $search - * @param int $limit - * @param int $offset - * @return DataResponse + * Get a list of groups + * + * @param string $search Text to search for + * @param int|null $limit Limit the amount of groups returned + * @param int $offset Offset for searching for groups + * @return DataResponse */ public function getGroups(string $search = '', int $limit = null, int $offset = 0): DataResponse { $groups = $this->groupManager->search($search, $limit, $offset); @@ -96,15 +103,15 @@ public function getGroups(string $search = '', int $limit = null, int $offset = } /** - * Returns a list of groups details with ids and displaynames - * * @NoAdminRequired * @AuthorizedAdminSetting(settings=OCA\Settings\Settings\Admin\Sharing) * - * @param string $search - * @param int $limit - * @param int $offset - * @return DataResponse + * Get a list of groups details + * + * @param string $search Text to search for + * @param int|null $limit Limit the amount of groups returned + * @param int $offset Offset for searching for groups + * @return DataResponse */ public function getGroupsDetails(string $search = '', int $limit = null, int $offset = 0): DataResponse { $groups = $this->groupManager->search($search, $limit, $offset); @@ -126,8 +133,10 @@ public function getGroupsDetails(string $search = '', int $limit = null, int $of /** * @NoAdminRequired * - * @param string $groupId - * @return DataResponse + * Get a list of users in the specified group + * + * @param string $groupId ID of the group + * @return DataResponse * @throws OCSException * * @deprecated 14 Use getGroupUsers @@ -137,13 +146,17 @@ public function getGroup(string $groupId): DataResponse { } /** - * returns an array of users in the specified group - * * @NoAdminRequired * - * @param string $groupId - * @return DataResponse + * Get a list of users in the specified group + * + * @param string $groupId ID of the group + * @return DataResponse * @throws OCSException + * @throws OCSNotFoundException Group not found + * @throws OCSForbiddenException Missing permissions to get users in the group + * + * 200: User IDs returned */ public function getGroupUsers(string $groupId): DataResponse { $groupId = urldecode($groupId); @@ -167,6 +180,7 @@ public function getGroupUsers(string $groupId): DataResponse { /** @var IUser $user */ return $user->getUID(); }, $users); + /** @var string[] $users */ $users = array_values($users); return new DataResponse(['users' => $users]); } @@ -175,15 +189,16 @@ public function getGroupUsers(string $groupId): DataResponse { } /** - * returns an array of users details in the specified group - * * @NoAdminRequired * - * @param string $groupId - * @param string $search - * @param int $limit - * @param int $offset - * @return DataResponse + * Get a list of users details in the specified group + * + * @param string $groupId ID of the group + * @param string $search Text to search for + * @param int|null $limit Limit the amount of groups returned + * @param int $offset Offset for searching for groups + * + * @return DataResponse}, array{}> * @throws OCSException */ public function getGroupUsersDetails(string $groupId, string $search = '', int $limit = null, int $offset = 0): DataResponse { @@ -210,7 +225,7 @@ public function getGroupUsersDetails(string $groupId, string $search = '', int $ $userId = (string)$user->getUID(); $userData = $this->getUserData($userId); // Do not insert empty entry - if (!empty($userData)) { + if ($userData != null) { $usersDetails[$userId] = $userData; } else { // Logged user does not have permissions to see this user @@ -228,13 +243,13 @@ public function getGroupUsersDetails(string $groupId, string $search = '', int $ } /** - * creates a new group - * * @PasswordConfirmationRequired * - * @param string $groupid - * @param string $displayname - * @return DataResponse + * Create a new group + * + * @param string $groupid ID of the group + * @param string $displayname Display name of the group + * @return DataResponse * @throws OCSException */ public function addGroup(string $groupid, string $displayname = ''): DataResponse { @@ -254,16 +269,18 @@ public function addGroup(string $groupid, string $displayname = ''): DataRespons if ($displayname !== '') { $group->setDisplayName($displayname); } - return new DataResponse(); + return new DataResponse(new \stdClass()); } /** * @PasswordConfirmationRequired * - * @param string $groupId - * @param string $key - * @param string $value - * @return DataResponse + * Update a group + * + * @param string $groupId ID of the group + * @param string $key Key to update, only 'displayname' + * @param string $value New value for the key + * @return DataResponse * @throws OCSException */ public function updateGroup(string $groupId, string $key, string $value): DataResponse { @@ -272,7 +289,7 @@ public function updateGroup(string $groupId, string $key, string $value): DataRe if ($key === 'displayname') { $group = $this->groupManager->get($groupId); if ($group->setDisplayName($value)) { - return new DataResponse(); + return new DataResponse(new \stdClass()); } throw new OCSException('Not supported by backend', 101); @@ -284,8 +301,10 @@ public function updateGroup(string $groupId, string $key, string $value): DataRe /** * @PasswordConfirmationRequired * - * @param string $groupId - * @return DataResponse + * Delete a group + * + * @param string $groupId ID of the group + * @return DataResponse * @throws OCSException */ public function deleteGroup(string $groupId): DataResponse { @@ -299,12 +318,14 @@ public function deleteGroup(string $groupId): DataResponse { throw new OCSException('', 102); } - return new DataResponse(); + return new DataResponse(new \stdClass()); } /** - * @param string $groupId - * @return DataResponse + * Get the list of user IDs that are a subadmin of the group + * + * @param string $groupId ID of the group + * @return DataResponse * @throws OCSException */ public function getSubAdminsOfGroup(string $groupId): DataResponse { @@ -317,6 +338,7 @@ public function getSubAdminsOfGroup(string $groupId): DataResponse { /** @var IUser[] $subadmins */ $subadmins = $this->groupManager->getSubAdmin()->getGroupsSubAdmins($targetGroup); // New class returns IUser[] so convert back + /** @var string[] $uids */ $uids = []; foreach ($subadmins as $user) { $uids[] = $user->getUID(); diff --git a/apps/provisioning_api/lib/Controller/PreferencesController.php b/apps/provisioning_api/lib/Controller/PreferencesController.php index 2dba8b86eb6fc..27a395930dfa4 100644 --- a/apps/provisioning_api/lib/Controller/PreferencesController.php +++ b/apps/provisioning_api/lib/Controller/PreferencesController.php @@ -6,6 +6,7 @@ * @copyright Copyright (c) 2022 Joas Schilling * * @author Joas Schilling + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -58,6 +59,16 @@ public function __construct( /** * @NoAdminRequired * @NoSubAdminRequired + * + * Update multiple preference values of an app + * + * @param string $appId ID of the app + * @param array $configs Key-value pairs of the preferences + * + * @return DataResponse + * + * 200: Preferences updated successfully + * 400: Preference invalid */ public function setMultiplePreferences(string $appId, array $configs): DataResponse { $userId = $this->userSession->getUser()->getUID(); @@ -74,7 +85,7 @@ public function setMultiplePreferences(string $appId, array $configs): DataRespo if (!$event->isValid()) { // No listener validated that the preference can be set (to this value) - return new DataResponse([], Http::STATUS_BAD_REQUEST); + return new DataResponse(new \stdClass(), Http::STATUS_BAD_REQUEST); } } @@ -87,12 +98,22 @@ public function setMultiplePreferences(string $appId, array $configs): DataRespo ); } - return new DataResponse(); + return new DataResponse(new \stdClass()); } /** * @NoAdminRequired * @NoSubAdminRequired + * + * Update a preference value of an app + * + * @param string $appId ID of the app + * @param string $configKey Key of the preference + * @param string $configValue New value + * @return DataResponse + * + * 200: Preference updated successfully + * 400: Preference invalid */ public function setPreference(string $appId, string $configKey, string $configValue): DataResponse { $userId = $this->userSession->getUser()->getUID(); @@ -108,7 +129,7 @@ public function setPreference(string $appId, string $configKey, string $configVa if (!$event->isValid()) { // No listener validated that the preference can be set (to this value) - return new DataResponse([], Http::STATUS_BAD_REQUEST); + return new DataResponse(new \stdClass(), Http::STATUS_BAD_REQUEST); } $this->config->setUserValue( @@ -118,12 +139,21 @@ public function setPreference(string $appId, string $configKey, string $configVa $configValue ); - return new DataResponse(); + return new DataResponse(new \stdClass()); } /** * @NoAdminRequired * @NoSubAdminRequired + * + * Delete multiple preferences for an app + * + * @param string $appId ID of the app + * @param string[] $configKeys Keys to delete + * + * @return DataResponse + * 200: Preferences deleted successfully + * 400: Preference invalid */ public function deleteMultiplePreference(string $appId, array $configKeys): DataResponse { $userId = $this->userSession->getUser()->getUID(); @@ -139,7 +169,7 @@ public function deleteMultiplePreference(string $appId, array $configKeys): Data if (!$event->isValid()) { // No listener validated that the preference can be deleted - return new DataResponse([], Http::STATUS_BAD_REQUEST); + return new DataResponse(new \stdClass(), Http::STATUS_BAD_REQUEST); } } @@ -151,12 +181,21 @@ public function deleteMultiplePreference(string $appId, array $configKeys): Data ); } - return new DataResponse(); + return new DataResponse(new \stdClass()); } /** * @NoAdminRequired * @NoSubAdminRequired + * + * Delete a preference for an app + * + * @param string $appId ID of the app + * @param string $configKey Key to delete + * @return DataResponse + * + * 200: Preference deleted successfully + * 400: Preference invalid */ public function deletePreference(string $appId, string $configKey): DataResponse { $userId = $this->userSession->getUser()->getUID(); @@ -171,7 +210,7 @@ public function deletePreference(string $appId, string $configKey): DataResponse if (!$event->isValid()) { // No listener validated that the preference can be deleted - return new DataResponse([], Http::STATUS_BAD_REQUEST); + return new DataResponse(new \stdClass(), Http::STATUS_BAD_REQUEST); } $this->config->deleteUserValue( @@ -180,6 +219,6 @@ public function deletePreference(string $appId, string $configKey): DataResponse $configKey ); - return new DataResponse(); + return new DataResponse(new \stdClass()); } } diff --git a/apps/provisioning_api/lib/Controller/UsersController.php b/apps/provisioning_api/lib/Controller/UsersController.php index 6ce46087d8042..643a8494ad071 100644 --- a/apps/provisioning_api/lib/Controller/UsersController.php +++ b/apps/provisioning_api/lib/Controller/UsersController.php @@ -51,6 +51,7 @@ use OC\Authentication\Token\RemoteWipe; use OC\KnownUser\KnownUserService; use OC\User\Backend; +use OCA\Provisioning_API\ResponseDefinitions; use OCA\Settings\Mailer\NewUserMailHelper; use OCP\Accounts\IAccountManager; use OCP\Accounts\IAccountProperty; @@ -76,6 +77,9 @@ use OCP\User\Backend\ISetDisplayNameBackend; use Psr\Log\LoggerInterface; +/** + * @psalm-import-type ProvisioningApiUserDetails from ResponseDefinitions + */ class UsersController extends AUserData { /** @var IURLGenerator */ protected $urlGenerator; @@ -135,12 +139,12 @@ public function __construct( /** * @NoAdminRequired * - * returns a list of users + * Get a list of users * - * @param string $search - * @param int $limit - * @param int $offset - * @return DataResponse + * @param string $search Text to search for + * @param int|null $limit Limit the amount of groups returned + * @param int $offset Offset for searching for groups + * @return DataResponse */ public function getUsers(string $search = '', int $limit = null, int $offset = 0): DataResponse { $user = $this->userSession->getUser(); @@ -163,6 +167,7 @@ public function getUsers(string $search = '', int $limit = null, int $offset = 0 } } + /** @var string[] $users */ $users = array_keys($users); return new DataResponse([ @@ -173,7 +178,12 @@ public function getUsers(string $search = '', int $limit = null, int $offset = 0 /** * @NoAdminRequired * - * returns a list of users and their data + * Get a list of users and their details + * + * @param string $search Text to search for + * @param int|null $limit Limit the amount of groups returned + * @param int $offset Offset for searching for groups + * @return DataResponse}, array{}> */ public function getUsersDetails(string $search = '', int $limit = null, int $offset = 0): DataResponse { $currentUser = $this->userSession->getUser(); @@ -198,12 +208,13 @@ public function getUsersDetails(string $search = '', int $limit = null, int $off $users = array_merge(...$users); } + /** @var array $usersDetails */ $usersDetails = []; foreach ($users as $userId) { $userId = (string) $userId; $userData = $this->getUserData($userId); // Do not insert empty entry - if (!empty($userData)) { + if ($userData != null) { $usersDetails[$userId] = $userData; } else { // Logged user does not have permissions to see this user @@ -222,16 +233,21 @@ public function getUsersDetails(string $search = '', int $limit = null, int $off * @NoAdminRequired * @NoSubAdminRequired * - * @param string $location - * @param array $search - * @return DataResponse + * Search users by their phone numbers + * + * @param string $location Location of the phone number (for country code) + * @param array $search Phone numbers to search for + * @return DataResponse, array{}>|DataResponse + * + * 200: Users returned + * 400: Invalid location */ public function searchByPhoneNumbers(string $location, array $search): DataResponse { $phoneUtil = PhoneNumberUtil::getInstance(); if ($phoneUtil->getCountryCodeForRegion($location) === 0) { // Not a valid region code - return new DataResponse([], Http::STATUS_BAD_REQUEST); + return new DataResponse(new \stdClass(), Http::STATUS_BAD_REQUEST); } /** @var IUser $user */ @@ -278,10 +294,6 @@ public function searchByPhoneNumbers(string $location, array $search): DataRespo $userMatches = $this->accountManager->searchUsers(IAccountManager::PROPERTY_PHONE, $phoneNumbers); - if (empty($userMatches)) { - return new DataResponse(); - } - $cloudUrl = rtrim($this->urlGenerator->getAbsoluteURL('/'), '/'); if (strpos($cloudUrl, 'http://') === 0) { $cloudUrl = substr($cloudUrl, strlen('http://')); @@ -318,16 +330,22 @@ private function createNewUserId(): string { * @PasswordConfirmationRequired * @NoAdminRequired * - * @param string $userid - * @param string $password - * @param string $displayName - * @param string $email - * @param array $groups - * @param array $subadmin - * @param string $quota - * @param string $language - * @return DataResponse + * Create a new user + * + * @param string $userid ID of the user + * @param string $password Password of the user + * @param string $displayName Display name of the user + * @param string $email Email of the user + * @param string[] $groups Groups of the user + * @param string[] $subadmin Groups where the user is subadmin + * @param string $quota Quota of the user + * @param string $language Language of the user + * @param ?string $manager Manager of the user + * @return DataResponse * @throws OCSException + * @throws OCSForbiddenException Missing permissions to make user subadmin + * + * 200: User added successfully */ public function addUser( string $userid, @@ -521,10 +539,10 @@ public function addUser( * @NoAdminRequired * @NoSubAdminRequired * - * gets user info + * Get the details of a user * - * @param string $userId - * @return DataResponse + * @param string $userId ID of the user + * @return DataResponse * @throws OCSException */ public function getUser(string $userId): DataResponse { @@ -536,7 +554,7 @@ public function getUser(string $userId): DataResponse { $data = $this->getUserData($userId, $includeScopes); // getUserData returns empty array if not enough permissions - if (empty($data)) { + if ($data == null) { throw new OCSException('', OCSController::RESPOND_NOT_FOUND); } return new DataResponse($data); @@ -546,14 +564,15 @@ public function getUser(string $userId): DataResponse { * @NoAdminRequired * @NoSubAdminRequired * - * gets user info from the currently logged in user + * Get the details of the current user * - * @return DataResponse + * @return DataResponse * @throws OCSException */ public function getCurrentUser(): DataResponse { $user = $this->userSession->getUser(); if ($user) { + /** @var ProvisioningApiUserDetails $data */ $data = $this->getUserData($user->getUID(), true); return new DataResponse($data); } @@ -565,7 +584,9 @@ public function getCurrentUser(): DataResponse { * @NoAdminRequired * @NoSubAdminRequired * - * @return DataResponse + * Get a list of fields that are editable for the current user + * + * @return DataResponse * @throws OCSException */ public function getEditableFields(): DataResponse { @@ -581,8 +602,10 @@ public function getEditableFields(): DataResponse { * @NoAdminRequired * @NoSubAdminRequired * - * @param string $userId - * @return DataResponse + * Get a list of fields that are editable for a user + * + * @param string $userId ID of the user + * @return DataResponse * @throws OCSException */ public function getEditableFieldsForUser(string $userId): DataResponse { @@ -642,6 +665,13 @@ public function getEditableFieldsForUser(string $userId): DataResponse { * @PasswordConfirmationRequired * @UserRateThrottle(limit=5, period=60) * + * Update multiple values of the user's details + * + * @param string $userId ID of the user + * @param string $collectionName Collection to update + * @param string $key Key that will be updated + * @param string $value New value for the key + * @return DataResponse * @throws OCSException */ public function editUserMultiValue( @@ -726,7 +756,7 @@ public function editUserMultiValue( default: throw new OCSException('', 103); } - return new DataResponse(); + return new DataResponse(new \stdClass()); } /** @@ -735,12 +765,12 @@ public function editUserMultiValue( * @PasswordConfirmationRequired * @UserRateThrottle(limit=50, period=600) * - * edit users + * Update a value of the user's details * - * @param string $userId - * @param string $key - * @param string $value - * @return DataResponse + * @param string $userId ID of the user + * @param string $key Key that will be updated + * @param string $value New value for the key + * @return DataResponse * @throws OCSException */ public function editUser(string $userId, string $key, string $value): DataResponse { @@ -1039,16 +1069,18 @@ public function editUser(string $userId, string $key, string $value): DataRespon default: throw new OCSException('', 103); } - return new DataResponse(); + return new DataResponse(new \stdClass()); } /** * @PasswordConfirmationRequired * @NoAdminRequired * - * @param string $userId + * Wipe all devices of a user + * + * @param string $userId ID of the user * - * @return DataResponse + * @return DataResponse * * @throws OCSException */ @@ -1074,15 +1106,17 @@ public function wipeUserDevices(string $userId): DataResponse { $this->remoteWipe->markAllTokensForWipe($targetUser); - return new DataResponse(); + return new DataResponse(new \stdClass()); } /** * @PasswordConfirmationRequired * @NoAdminRequired * - * @param string $userId - * @return DataResponse + * Delete a user + * + * @param string $userId ID of the user + * @return DataResponse * @throws OCSException */ public function deleteUser(string $userId): DataResponse { @@ -1106,7 +1140,7 @@ public function deleteUser(string $userId): DataResponse { // Go ahead with the delete if ($targetUser->delete()) { - return new DataResponse(); + return new DataResponse(new \stdClass()); } else { throw new OCSException('', 101); } @@ -1116,10 +1150,11 @@ public function deleteUser(string $userId): DataResponse { * @PasswordConfirmationRequired * @NoAdminRequired * - * @param string $userId - * @return DataResponse + * Disable a user + * + * @param string $userId ID of the user + * @return DataResponse * @throws OCSException - * @throws OCSForbiddenException */ public function disableUser(string $userId): DataResponse { return $this->setEnabled($userId, false); @@ -1129,10 +1164,11 @@ public function disableUser(string $userId): DataResponse { * @PasswordConfirmationRequired * @NoAdminRequired * - * @param string $userId - * @return DataResponse + * Enable a user + * + * @param string $userId ID of the user + * @return DataResponse * @throws OCSException - * @throws OCSForbiddenException */ public function enableUser(string $userId): DataResponse { return $this->setEnabled($userId, true); @@ -1141,7 +1177,7 @@ public function enableUser(string $userId): DataResponse { /** * @param string $userId * @param bool $value - * @return DataResponse + * @return DataResponse * @throws OCSException */ private function setEnabled(string $userId, bool $value): DataResponse { @@ -1160,15 +1196,17 @@ private function setEnabled(string $userId, bool $value): DataResponse { // enable/disable the user now $targetUser->setEnabled($value); - return new DataResponse(); + return new DataResponse(new \stdClass()); } /** * @NoAdminRequired * @NoSubAdminRequired * - * @param string $userId - * @return DataResponse + * Get a list of groups the user belongs to + * + * @param string $userId ID of the user + * @return DataResponse * @throws OCSException */ public function getUsersGroups(string $userId): DataResponse { @@ -1195,6 +1233,7 @@ public function getUsersGroups(string $userId): DataResponse { foreach ($getSubAdminsGroups as $key => $group) { $getSubAdminsGroups[$key] = $group->getGID(); } + /** @var string[] $groups */ $groups = array_intersect( $getSubAdminsGroups, $this->groupManager->getUserGroupIds($targetUser) @@ -1211,9 +1250,11 @@ public function getUsersGroups(string $userId): DataResponse { * @PasswordConfirmationRequired * @NoAdminRequired * - * @param string $userId - * @param string $groupid - * @return DataResponse + * Add a user to a group + * + * @param string $userId ID of the user + * @param string $groupid ID of the group + * @return DataResponse * @throws OCSException */ public function addToGroup(string $userId, string $groupid = ''): DataResponse { @@ -1239,16 +1280,18 @@ public function addToGroup(string $userId, string $groupid = ''): DataResponse { // Add user to group $group->addUser($targetUser); - return new DataResponse(); + return new DataResponse(new \stdClass()); } /** * @PasswordConfirmationRequired * @NoAdminRequired * - * @param string $userId - * @param string $groupid - * @return DataResponse + * Remove a user from a group + * + * @param string $userId ID of the user + * @param string $groupid ID of the group + * @return DataResponse * @throws OCSException */ public function removeFromGroup(string $userId, string $groupid): DataResponse { @@ -1301,17 +1344,17 @@ public function removeFromGroup(string $userId, string $groupid): DataResponse { // Remove user from group $group->removeUser($targetUser); - return new DataResponse(); + return new DataResponse(new \stdClass()); } /** - * Creates a subadmin - * * @PasswordConfirmationRequired * - * @param string $userId - * @param string $groupid - * @return DataResponse + * Make a user a subadmin of a group + * + * @param string $userId ID of the user + * @param string $groupid ID of the group + * @return DataResponse * @throws OCSException */ public function addSubAdmin(string $userId, string $groupid): DataResponse { @@ -1335,21 +1378,21 @@ public function addSubAdmin(string $userId, string $groupid): DataResponse { // We cannot be subadmin twice if ($subAdminManager->isSubAdminOfGroup($user, $group)) { - return new DataResponse(); + return new DataResponse(new \stdClass()); } // Go $subAdminManager->createSubAdmin($user, $group); - return new DataResponse(); + return new DataResponse(new \stdClass()); } /** - * Removes a subadmin from a group - * * @PasswordConfirmationRequired * - * @param string $userId - * @param string $groupid - * @return DataResponse + * Remove a user from the subadmins of a group + * + * @param string $userId ID of the user + * @param string $groupid ID of the group + * @return DataResponse * @throws OCSException */ public function removeSubAdmin(string $userId, string $groupid): DataResponse { @@ -1372,14 +1415,14 @@ public function removeSubAdmin(string $userId, string $groupid): DataResponse { // Go $subAdminManager->deleteSubAdmin($user, $group); - return new DataResponse(); + return new DataResponse(new \stdClass()); } /** * Get the groups a user is a subadmin of * - * @param string $userId - * @return DataResponse + * @param string $userId ID if the user + * @return DataResponse * @throws OCSException */ public function getUserSubAdminGroups(string $userId): DataResponse { @@ -1391,10 +1434,10 @@ public function getUserSubAdminGroups(string $userId): DataResponse { * @NoAdminRequired * @PasswordConfirmationRequired * - * resend welcome message + * Resend the welcome message * - * @param string $userId - * @return DataResponse + * @param string $userId ID if the user + * @return DataResponse * @throws OCSException */ public function resendWelcomeMessage(string $userId): DataResponse { @@ -1434,6 +1477,6 @@ public function resendWelcomeMessage(string $userId): DataResponse { throw new OCSException('Sending email failed', 102); } - return new DataResponse(); + return new DataResponse(new \stdClass()); } } diff --git a/apps/provisioning_api/lib/Controller/VerificationController.php b/apps/provisioning_api/lib/Controller/VerificationController.php index f16f50385e779..62b5c12d0280e 100644 --- a/apps/provisioning_api/lib/Controller/VerificationController.php +++ b/apps/provisioning_api/lib/Controller/VerificationController.php @@ -6,6 +6,7 @@ * @copyright Copyright (c) 2021 Arthur Schiwon * * @author Arthur Schiwon + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -76,6 +77,7 @@ public function __construct( * @NoCSRFRequired * @NoAdminRequired * @NoSubAdminRequired + * @IgnoreAPI */ public function showVerifyMail(string $token, string $userId, string $key) { if ($this->userSession->getUser()->getUID() !== $userId) { diff --git a/apps/provisioning_api/lib/ResponseDefinitions.php b/apps/provisioning_api/lib/ResponseDefinitions.php new file mode 100644 index 0000000000000..4e562ec648f14 --- /dev/null +++ b/apps/provisioning_api/lib/ResponseDefinitions.php @@ -0,0 +1,136 @@ + + * + * @author Kate Döen + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Provisioning_API; + +/** + * @psalm-type ProvisioningApiUserDetailsQuota = array{ + * free?: float, + * quota?: float|string, + * relative?: float, + * total?: float, + * used?: float, + * } + * + * @psalm-type ProvisioningApiUserDetails = array{ + * additional_mail: string[], + * additional_mailScope?: string[], + * address: string, + * addressScope?: string, + * avatarScope?: string, + * backend: string, + * backendCapabilities: array{ + * setDisplayName: bool, + * setPassword: bool + * }, + * biography: string, + * biographyScope?: string, + * display-name: string, + * displayname: string, + * displaynameScope?: string, + * email: ?string, + * emailScope?: string, + * enabled?: bool, + * fediverse: string, + * fediverseScope?: string, + * groups: string[], + * headline: string, + * headlineScope?: string, + * id: string, + * language: string, + * lastLogin: int, + * locale: string, + * manager: string, + * notify_email: ?string, + * organisation: string, + * organisationScope?: string, + * phone: string, + * phoneScope?: string, + * profile_enabled: string, + * profile_enabledScope?: string, + * quota: ProvisioningApiUserDetailsQuota, + * role: string, + * roleScope?: string, + * storageLocation?: string, + * subadmin: string[], + * twitter: string, + * twitterScope?: string, + * website: string, + * websiteScope?: string, + * } + * + * @psalm-type ProvisioningApiAppInfo = array{ + * active: bool|null, + * activity: ?mixed, + * author: ?mixed, + * background-jobs: ?mixed, + * bugs: ?mixed, + * category: ?mixed, + * collaboration: ?mixed, + * commands: ?mixed, + * default_enable: ?mixed, + * dependencies: ?mixed, + * description: string, + * discussion: ?mixed, + * documentation: ?mixed, + * groups: ?mixed, + * id: string, + * info: ?mixed, + * internal: bool|null, + * level: int|null, + * licence: ?mixed, + * name: string, + * namespace: ?mixed, + * navigations: ?mixed, + * preview: ?mixed, + * previewAsIcon: bool|null, + * public: ?mixed, + * remote: ?mixed, + * removable: bool|null, + * repair-steps: ?mixed, + * repository: ?mixed, + * sabre: ?mixed, + * screenshot: ?mixed, + * settings: ?mixed, + * summary: string, + * trash: ?mixed, + * two-factor-providers: ?mixed, + * types: ?mixed, + * version: string, + * versions: ?mixed, + * website: ?mixed, + * } + * + * @psalm-type ProvisioningApiGroupDetails = array{ + * id: string, + * displayname: string, + * usercount: bool|int, + * disabled: bool|int, + * canAdd: bool, + * canRemove: bool, + * } + */ +class ResponseDefinitions { +} diff --git a/apps/provisioning_api/openapi.json b/apps/provisioning_api/openapi.json index 82ee9b16e3197..5268c2cf55391 100644 --- a/apps/provisioning_api/openapi.json +++ b/apps/provisioning_api/openapi.json @@ -325,45 +325,30 @@ "type": "object", "required": [ "additional_mail", - "additional_mailScope", "address", - "addressScope", - "avatarScope", "backend", "backendCapabilities", "biography", - "biographyScope", - "displayname", "display-name", - "displaynameScope", + "displayname", "email", - "emailScope", - "enabled", "fediverse", - "fediverseScope", "groups", "headline", - "headlineScope", "id", "language", "lastLogin", "locale", + "manager", "notify_email", "organisation", - "organisationScope", "phone", - "phoneScope", "profile_enabled", - "profile_enabledScope", "quota", "role", - "roleScope", - "storageLocation", "subadmin", "twitter", - "twitterScope", - "website", - "websiteScope" + "website" ], "properties": { "additional_mail": { @@ -374,7 +359,6 @@ }, "additional_mailScope": { "type": "array", - "nullable": true, "items": { "type": "string" } @@ -383,12 +367,10 @@ "type": "string" }, "addressScope": { - "type": "string", - "nullable": true + "type": "string" }, "avatarScope": { - "type": "string", - "nullable": true + "type": "string" }, "backend": { "type": "string" @@ -412,38 +394,32 @@ "type": "string" }, "biographyScope": { - "type": "string", - "nullable": true - }, - "displayname": { "type": "string" }, "display-name": { "type": "string" }, + "displayname": { + "type": "string" + }, "displaynameScope": { - "type": "string", - "nullable": true + "type": "string" }, "email": { "type": "string", "nullable": true }, "emailScope": { - "type": "string", - "nullable": true + "type": "string" }, "enabled": { - "type": "boolean", - "nullable": true + "type": "boolean" }, "fediverse": { - "type": "string", - "nullable": true + "type": "string" }, "fediverseScope": { - "type": "string", - "nullable": true + "type": "string" }, "groups": { "type": "array", @@ -455,8 +431,7 @@ "type": "string" }, "headlineScope": { - "type": "string", - "nullable": true + "type": "string" }, "id": { "type": "string" @@ -471,6 +446,9 @@ "locale": { "type": "string" }, + "manager": { + "type": "string" + }, "notify_email": { "type": "string", "nullable": true @@ -479,78 +457,31 @@ "type": "string" }, "organisationScope": { - "type": "string", - "nullable": true + "type": "string" }, "phone": { "type": "string" }, "phoneScope": { - "type": "string", - "nullable": true + "type": "string" }, "profile_enabled": { "type": "string" }, "profile_enabledScope": { - "type": "string", - "nullable": true + "type": "string" }, "quota": { - "type": "object", - "required": [ - "free", - "quota", - "relative", - "total", - "used" - ], - "properties": { - "free": { - "type": "integer", - "format": "int64", - "nullable": true - }, - "quota": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "integer", - "format": "int64" - }, - { - "type": "boolean" - } - ] - }, - "relative": { - "type": "number", - "format": "float", - "nullable": true - }, - "total": { - "type": "integer", - "format": "int64", - "nullable": true - }, - "used": { - "type": "integer", - "format": "int64" - } - } + "$ref": "#/components/schemas/UserDetailsQuota" }, "role": { "type": "string" }, "roleScope": { - "type": "string", - "nullable": true + "type": "string" }, "storageLocation": { - "type": "string", - "nullable": true + "type": "string" }, "subadmin": { "type": "array", @@ -562,15 +493,45 @@ "type": "string" }, "twitterScope": { - "type": "string", - "nullable": true + "type": "string" }, "website": { "type": "string" }, "websiteScope": { - "type": "string", - "nullable": true + "type": "string" + } + } + }, + "UserDetailsQuota": { + "type": "object", + "properties": { + "free": { + "type": "number", + "format": "float" + }, + "quota": { + "oneOf": [ + { + "type": "number", + "format": "float" + }, + { + "type": "string" + } + ] + }, + "relative": { + "type": "number", + "format": "float" + }, + "total": { + "type": "number", + "format": "float" + }, + "used": { + "type": "number", + "format": "float" } } } @@ -3576,7 +3537,7 @@ } }, "403": { - "description": "App not allowed", + "description": "App is not allowed", "content": { "application/json": { "schema": { @@ -3718,7 +3679,7 @@ } }, "403": { - "description": "App not allowed", + "description": "App is not allowed", "content": { "application/json": { "schema": { @@ -3850,7 +3811,7 @@ } }, "403": { - "description": "App or key not allowed", + "description": "App or key is not allowed", "content": { "application/json": { "schema": { @@ -3974,7 +3935,7 @@ } }, "403": { - "description": "App or key not allowed", + "description": "App or key is not allowed", "content": { "application/json": { "schema": { diff --git a/apps/provisioning_api/tests/Controller/AppConfigControllerTest.php b/apps/provisioning_api/tests/Controller/AppConfigControllerTest.php index f55f842debc48..34b7ffa567f47 100644 --- a/apps/provisioning_api/tests/Controller/AppConfigControllerTest.php +++ b/apps/provisioning_api/tests/Controller/AppConfigControllerTest.php @@ -280,7 +280,7 @@ public function testSetValue($app, $key, $value, $appThrows, $keyThrows, $status } elseif ($keyThrows instanceof \Exception) { $this->assertEquals(['data' => ['message' => $keyThrows->getMessage()]], $result->getData()); } else { - $this->assertEquals([], $result->getData()); + $this->assertEquals(\stdClass, $result->getData()); } } @@ -344,7 +344,7 @@ public function testDeleteValue($app, $key, $appThrows, $keyThrows, $status) { } elseif ($keyThrows instanceof \Exception) { $this->assertEquals(['data' => ['message' => $keyThrows->getMessage()]], $result->getData()); } else { - $this->assertEquals([], $result->getData()); + $this->assertEquals(\stdClass, $result->getData()); } } diff --git a/apps/provisioning_api/tests/Controller/UsersControllerTest.php b/apps/provisioning_api/tests/Controller/UsersControllerTest.php index 56a47f23a5cc4..bca84e74686e9 100644 --- a/apps/provisioning_api/tests/Controller/UsersControllerTest.php +++ b/apps/provisioning_api/tests/Controller/UsersControllerTest.php @@ -1420,12 +1420,12 @@ public function testGetUserDataAsSubAdminSelfLookup() { public function dataSearchByPhoneNumbers(): array { return [ - 'Invalid country' => ['Not a country code', ['12345' => ['NaN']], 400, null, null, []], - 'No number to search' => ['DE', ['12345' => ['NaN']], 200, null, null, []], - 'Valid number but no match' => ['DE', ['12345' => ['0711 / 25 24 28-90']], 200, ['+4971125242890'], [], []], - 'Invalid number' => ['FR', ['12345' => ['0711 / 25 24 28-90']], 200, null, null, []], - 'Invalid and valid number' => ['DE', ['12345' => ['NaN', '0711 / 25 24 28-90']], 200, ['+4971125242890'], [], []], - 'Valid and invalid number' => ['DE', ['12345' => ['0711 / 25 24 28-90', 'NaN']], 200, ['+4971125242890'], [], []], + 'Invalid country' => ['Not a country code', ['12345' => ['NaN']], 400, null, null, \stdClass], + 'No number to search' => ['DE', ['12345' => ['NaN']], 200, null, null, \stdClass], + 'Valid number but no match' => ['DE', ['12345' => ['0711 / 25 24 28-90']], 200, ['+4971125242890'], [], \stdClass], + 'Invalid number' => ['FR', ['12345' => ['0711 / 25 24 28-90']], 200, null, null, \stdClass], + 'Invalid and valid number' => ['DE', ['12345' => ['NaN', '0711 / 25 24 28-90']], 200, ['+4971125242890'], [], \stdClass], + 'Valid and invalid number' => ['DE', ['12345' => ['0711 / 25 24 28-90', 'NaN']], 200, ['+4971125242890'], [], \stdClass], 'Valid number and a match' => ['DE', ['12345' => ['0711 / 25 24 28-90']], 200, ['+4971125242890'], ['+4971125242890' => 'admin'], ['12345' => 'admin@localhost']], 'Same number twice, later hits' => ['DE', ['12345' => ['0711 / 25 24 28-90'], '23456' => ['0711 / 25 24 28-90']], 200, ['+4971125242890'], ['+4971125242890' => 'admin'], ['23456' => 'admin@localhost']], ]; @@ -1508,7 +1508,7 @@ public function testEditUserRegularUserSelfEditChangeDisplayName() { ->method('getUID') ->willReturn('UID'); - $this->assertEquals([], $this->api->editUser('UserToEdit', 'display', 'NewDisplayName')->getData()); + $this->assertEquals(\stdClass, $this->api->editUser('UserToEdit', 'display', 'NewDisplayName')->getData()); } public function testEditUserRegularUserSelfEditChangeEmailValid() { @@ -1546,7 +1546,7 @@ public function testEditUserRegularUserSelfEditChangeEmailValid() { ->method('getBackend') ->willReturn($backend); - $this->assertEquals([], $this->api->editUser('UserToEdit', 'email', 'demo@nextcloud.com')->getData()); + $this->assertEquals(\stdClass, $this->api->editUser('UserToEdit', 'email', 'demo@nextcloud.com')->getData()); } public function testEditUserRegularUserSelfEditAddAdditionalEmailValid(): void { @@ -1592,7 +1592,7 @@ public function testEditUserRegularUserSelfEditAddAdditionalEmailValid(): void { ->method('updateAccount') ->with($userAccount); - $this->assertEquals([], $this->api->editUser('UserToEdit', 'additional_mail', 'demo1@nextcloud.com')->getData()); + $this->assertEquals(\stdClass, $this->api->editUser('UserToEdit', 'additional_mail', 'demo1@nextcloud.com')->getData()); } public function testEditUserRegularUserSelfEditAddAdditionalEmailMainAddress(): void { @@ -1814,7 +1814,7 @@ public function testEditUserRegularUserSelfEditChangeProperty($propertyName, $ol ->method('updateAccount') ->with($accountMock); - $this->assertEquals([], $this->api->editUser('UserToEdit', $propertyName, $newValue)->getData()); + $this->assertEquals(\stdClass, $this->api->editUser('UserToEdit', $propertyName, $newValue)->getData()); } public function selfEditChangePropertyScopeProvider() { @@ -1891,7 +1891,7 @@ public function testEditUserRegularUserSelfEditChangePropertyScope($propertyName ->method('updateAccount') ->with($accountMock); - $this->assertEquals([], $this->api->editUser('UserToEdit', $propertyName . 'Scope', $newScope)->getData()); + $this->assertEquals(\stdClass, $this->api->editUser('UserToEdit', $propertyName . 'Scope', $newScope)->getData()); } public function testEditUserRegularUserSelfEditChangePassword() { @@ -1933,7 +1933,7 @@ public function testEditUserRegularUserSelfEditChangePassword() { ->method('getBackend') ->willReturn($backend); - $this->assertEquals([], $this->api->editUser('UserToEdit', 'password', 'NewPassword')->getData()); + $this->assertEquals(\stdClass, $this->api->editUser('UserToEdit', 'password', 'NewPassword')->getData()); } @@ -2019,7 +2019,7 @@ public function testEditUserAdminUserSelfEditChangeValidQuota() { ->method('getBackend') ->willReturn($backend); - $this->assertEquals([], $this->api->editUser('UserToEdit', 'quota', '3042824')->getData()); + $this->assertEquals(\stdClass, $this->api->editUser('UserToEdit', 'quota', '3042824')->getData()); } @@ -2114,7 +2114,7 @@ public function testEditUserAdminUserEditChangeValidQuota() { ->method('getBackend') ->willReturn($backend); - $this->assertEquals([], $this->api->editUser('UserToEdit', 'quota', '3042824')->getData()); + $this->assertEquals(\stdClass, $this->api->editUser('UserToEdit', 'quota', '3042824')->getData()); } public function testEditUserSelfEditChangeLanguage() { @@ -2162,7 +2162,7 @@ public function testEditUserSelfEditChangeLanguage() { ->method('getBackend') ->willReturn($backend); - $this->assertEquals([], $this->api->editUser('UserToEdit', 'language', 'de')->getData()); + $this->assertEquals(\stdClass, $this->api->editUser('UserToEdit', 'language', 'de')->getData()); } public function dataEditUserSelfEditChangeLanguageButForced() { @@ -2265,7 +2265,7 @@ public function testEditUserAdminEditChangeLanguage() { ->method('getBackend') ->willReturn($backend); - $this->assertEquals([], $this->api->editUser('UserToEdit', 'language', 'de')->getData()); + $this->assertEquals(\stdClass, $this->api->editUser('UserToEdit', 'language', 'de')->getData()); } /** @@ -2371,7 +2371,7 @@ public function testEditUserSubadminUserAccessible() { ->method('getBackend') ->willReturn($backend); - $this->assertEquals([], $this->api->editUser('UserToEdit', 'quota', '3042824')->getData()); + $this->assertEquals(\stdClass, $this->api->editUser('UserToEdit', 'quota', '3042824')->getData()); } @@ -2495,7 +2495,7 @@ public function testDeleteSuccessfulUserAsAdmin() { ->method('delete') ->willReturn(true); - $this->assertEquals([], $this->api->deleteUser('UserToDelete')->getData()); + $this->assertEquals(\stdClass, $this->api->deleteUser('UserToDelete')->getData()); } @@ -2576,7 +2576,7 @@ public function testDeleteSuccessfulUserAsSubadmin() { ->method('delete') ->willReturn(true); - $this->assertEquals([], $this->api->deleteUser('UserToDelete')->getData()); + $this->assertEquals(\stdClass, $this->api->deleteUser('UserToDelete')->getData()); } @@ -2981,7 +2981,7 @@ public function testAddToGroupSuccessAsSubadmin() { ->method('getUser') ->willReturn($loggedInUser); - $this->assertEquals(new DataResponse(), $this->api->addToGroup('TargetUser', 'GroupToAddTo')); + $this->assertEquals(new DataResponse(new \stdClass()), $this->api->addToGroup('TargetUser', 'GroupToAddTo')); } public function testAddToGroupSuccessAsAdmin() { @@ -3023,7 +3023,7 @@ public function testAddToGroupSuccessAsAdmin() { ->method('getUser') ->willReturn($loggedInUser); - $this->assertEquals(new DataResponse(), $this->api->addToGroup('TargetUser', 'GroupToAddTo')); + $this->assertEquals(new DataResponse(new \stdClass()), $this->api->addToGroup('TargetUser', 'GroupToAddTo')); } @@ -3344,7 +3344,7 @@ public function testRemoveFromGroupSuccessful() { ->method('removeUser') ->with($targetUser); - $this->assertEquals([], $this->api->removeFromGroup('AnotherUser', 'admin')->getData()); + $this->assertEquals(\stdClass, $this->api->removeFromGroup('AnotherUser', 'admin')->getData()); } @@ -3435,7 +3435,7 @@ public function testAddSubAdminTwice() { ->method('getSubAdmin') ->willReturn($subAdminManager); - $this->assertEquals([], $this->api->addSubAdmin('ExistingUser', 'TargetGroup')->getData()); + $this->assertEquals(\stdClass, $this->api->addSubAdmin('ExistingUser', 'TargetGroup')->getData()); } public function testAddSubAdminSuccessful() { @@ -3467,7 +3467,7 @@ public function testAddSubAdminSuccessful() { ->method('getSubAdmin') ->willReturn($subAdminManager); - $this->assertEquals([], $this->api->addSubAdmin('ExistingUser', 'TargetGroup')->getData()); + $this->assertEquals(\stdClass, $this->api->addSubAdmin('ExistingUser', 'TargetGroup')->getData()); } @@ -3569,7 +3569,7 @@ public function testRemoveSubAdminSuccessful() { ->method('getSubAdmin') ->willReturn($subAdminManager); - $this->assertEquals([], $this->api->removeSubAdmin('ExistingUser', 'GroupToDeleteFrom')->getData()); + $this->assertEquals(\stdClass, $this->api->removeSubAdmin('ExistingUser', 'GroupToDeleteFrom')->getData()); } @@ -3638,7 +3638,7 @@ public function testEnableUser() { ->method('isAdmin') ->willReturn(true); - $this->assertEquals([], $this->api->enableUser('RequestedUser')->getData()); + $this->assertEquals(\stdClass, $this->api->enableUser('RequestedUser')->getData()); } public function testDisableUser() { @@ -3665,7 +3665,7 @@ public function testDisableUser() { ->method('isAdmin') ->willReturn(true); - $this->assertEquals([], $this->api->disableUser('RequestedUser')->getData()); + $this->assertEquals(\stdClass, $this->api->disableUser('RequestedUser')->getData()); } public function testGetCurrentUserLoggedIn() { diff --git a/apps/settings/lib/Controller/AdminSettingsController.php b/apps/settings/lib/Controller/AdminSettingsController.php index dfaa26ff695e2..8e6eb1c496585 100644 --- a/apps/settings/lib/Controller/AdminSettingsController.php +++ b/apps/settings/lib/Controller/AdminSettingsController.php @@ -6,6 +6,7 @@ * @author Christoph Wurst * @author Lukas Reschke * @author Robin Appelman + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -61,6 +62,7 @@ public function __construct( * @NoCSRFRequired * @NoAdminRequired * @NoSubAdminRequired + * @IgnoreAPI * We are checking the permissions in the getSettings method. If there is no allowed * settings for the given section. The user will be gretted by an error message. */ diff --git a/apps/settings/lib/Controller/AppSettingsController.php b/apps/settings/lib/Controller/AppSettingsController.php index bc84e17535ed8..c150fb4ec1dab 100644 --- a/apps/settings/lib/Controller/AppSettingsController.php +++ b/apps/settings/lib/Controller/AppSettingsController.php @@ -13,6 +13,7 @@ * @author Morris Jobke * @author Roeland Jago Douma * @author Thomas Müller + * @author Kate Döen * * @license AGPL-3.0 * @@ -125,6 +126,7 @@ public function __construct(string $appName, /** * @NoCSRFRequired + * @IgnoreAPI * * @return TemplateResponse */ diff --git a/apps/settings/lib/Controller/CheckSetupController.php b/apps/settings/lib/Controller/CheckSetupController.php index 59175be68843b..334972ba9d82f 100644 --- a/apps/settings/lib/Controller/CheckSetupController.php +++ b/apps/settings/lib/Controller/CheckSetupController.php @@ -26,6 +26,7 @@ * @author Timo Förster * @author Valdnet <47037905+Valdnet@users.noreply.github.com> * @author MichaIng + * @author Kate Döen * * @license AGPL-3.0 * @@ -403,6 +404,7 @@ public function rescanFailedIntegrityCheck(): RedirectResponse { /** * @NoCSRFRequired * @AuthorizedAdminSetting(settings=OCA\Settings\Settings\Admin\Overview) + * @IgnoreAPI */ public function getFailedIntegrityCheckFiles(): DataDisplayResponse { if (!$this->checker->isCodeCheckEnforced()) { diff --git a/apps/settings/lib/Controller/HelpController.php b/apps/settings/lib/Controller/HelpController.php index 38ce84843ed04..d912d98137bdf 100644 --- a/apps/settings/lib/Controller/HelpController.php +++ b/apps/settings/lib/Controller/HelpController.php @@ -10,6 +10,7 @@ * @author Joas Schilling * @author Julius Härtl * @author Roeland Jago Douma + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -75,6 +76,7 @@ public function __construct( * @NoCSRFRequired * @NoAdminRequired * @NoSubAdminRequired + * @IgnoreAPI */ public function help(string $mode = 'user'): TemplateResponse { $this->navigationManager->setActiveEntry('help'); diff --git a/apps/settings/lib/Controller/LogSettingsController.php b/apps/settings/lib/Controller/LogSettingsController.php index 08c18189d038e..4a28c3d17e4c9 100644 --- a/apps/settings/lib/Controller/LogSettingsController.php +++ b/apps/settings/lib/Controller/LogSettingsController.php @@ -8,6 +8,7 @@ * @author Lukas Reschke * @author Roeland Jago Douma * @author Thomas Müller + * @author Kate Döen * * @license AGPL-3.0 * @@ -28,6 +29,7 @@ use OC\Log; use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; use OCP\AppFramework\Http\StreamResponse; use OCP\IRequest; @@ -46,15 +48,19 @@ public function __construct(string $appName, IRequest $request, Log $logger) { * * @NoCSRFRequired * - * @return StreamResponse + * @psalm-suppress MoreSpecificReturnType The value of Content-Disposition is not relevant + * @psalm-suppress LessSpecificReturnStatement The value of Content-Disposition is not relevant + * @return StreamResponse */ public function download() { if (!$this->log instanceof Log) { throw new \UnexpectedValueException('Log file not available'); } $resp = new StreamResponse($this->log->getLogPath()); - $resp->addHeader('Content-Type', 'application/octet-stream'); - $resp->addHeader('Content-Disposition', 'attachment; filename="nextcloud.log"'); + $resp->setHeaders([ + 'Content-Type' => 'application/octet-stream', + 'Content-Disposition' => 'attachment; filename="nextcloud.log"', + ]); return $resp; } } diff --git a/apps/settings/lib/Controller/PersonalSettingsController.php b/apps/settings/lib/Controller/PersonalSettingsController.php index 8781ecd214e2c..cca1a60dccd57 100644 --- a/apps/settings/lib/Controller/PersonalSettingsController.php +++ b/apps/settings/lib/Controller/PersonalSettingsController.php @@ -6,6 +6,7 @@ * @author Christoph Wurst * @author Joas Schilling * @author Robin Appelman + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -59,6 +60,7 @@ public function __construct( * @NoCSRFRequired * @NoAdminRequired * @NoSubAdminRequired + * @IgnoreAPI */ public function index(string $section): TemplateResponse { return $this->getIndexResponse('personal', $section); diff --git a/apps/settings/lib/Controller/ReasonsController.php b/apps/settings/lib/Controller/ReasonsController.php index 2ceb7005407ed..375cc1286829b 100644 --- a/apps/settings/lib/Controller/ReasonsController.php +++ b/apps/settings/lib/Controller/ReasonsController.php @@ -6,6 +6,7 @@ * @copyright Copyright (c) 2020, Roeland Jago Douma * * @author Jan C. Borchardt + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -34,6 +35,7 @@ class ReasonsController extends Controller { * @NoCSRFRequired * @NoAdminRequired * @NoSubAdminRequired + * @IgnoreAPI */ public function getPdf() { $data = file_get_contents(__DIR__ . '/../../data/Reasons to use Nextcloud.pdf'); diff --git a/apps/settings/lib/Controller/UsersController.php b/apps/settings/lib/Controller/UsersController.php index 89a850c19809a..790b91bff41c5 100644 --- a/apps/settings/lib/Controller/UsersController.php +++ b/apps/settings/lib/Controller/UsersController.php @@ -16,6 +16,7 @@ * @author Morris Jobke * @author Roeland Jago Douma * @author Vincent Petry + * @author Kate Döen * * @license AGPL-3.0 * @@ -145,6 +146,7 @@ public function __construct( /** * @NoCSRFRequired * @NoAdminRequired + * @IgnoreAPI * * Display users list template * @@ -157,6 +159,7 @@ public function usersListByGroup(): TemplateResponse { /** * @NoCSRFRequired * @NoAdminRequired + * @IgnoreAPI * * Display users list template * diff --git a/apps/settings/lib/Controller/WebAuthnController.php b/apps/settings/lib/Controller/WebAuthnController.php index 2692882301dc9..13ba725d383e8 100644 --- a/apps/settings/lib/Controller/WebAuthnController.php +++ b/apps/settings/lib/Controller/WebAuthnController.php @@ -7,6 +7,7 @@ * * @author Joas Schilling * @author Roeland Jago Douma + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -69,6 +70,7 @@ public function __construct(IRequest $request, ILogger $logger, Manager $webAuth * @PasswordConfirmationRequired * @UseSession * @NoCSRFRequired + * @IgnoreAPI */ public function startRegistration(): JSONResponse { $this->logger->debug('Starting WebAuthn registration'); diff --git a/apps/settings/openapi.json b/apps/settings/openapi.json index 6d1f8a1495044..6445d967dc2bf 100644 --- a/apps/settings/openapi.json +++ b/apps/settings/openapi.json @@ -19,7 +19,7 @@ "scheme": "bearer" } }, - "schemas": [] + "schemas": {} }, "paths": { "/index.php/settings/admin/log/download": { diff --git a/apps/sharebymail/lib/Capabilities.php b/apps/sharebymail/lib/Capabilities.php index 39d7172bb76a9..8be0ca380cf77 100644 --- a/apps/sharebymail/lib/Capabilities.php +++ b/apps/sharebymail/lib/Capabilities.php @@ -45,6 +45,27 @@ public function __construct(IManager $manager, $this->settingsManager = $settingsManager; } + /** + * @return array{ + * files_sharing: array{ + * sharebymail: array{ + * enabled: bool, + * send_password_by_mail: bool, + * upload_files_drop: array{ + * enabled: bool, + * }, + * password: array{ + * enabled: bool, + * enforced: bool, + * }, + * expire_date: array{ + * enabled: bool, + * enforced: bool, + * }, + * } + * } + * } + */ public function getCapabilities(): array { return [ 'files_sharing' => diff --git a/apps/sharebymail/openapi.json b/apps/sharebymail/openapi.json new file mode 100644 index 0000000000000..3c98f9e43ac8f --- /dev/null +++ b/apps/sharebymail/openapi.json @@ -0,0 +1,102 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "sharebymail", + "version": "0.0.1", + "description": "Share provider which allows you to share files by mail", + "license": { + "name": "agpl" + } + }, + "components": { + "securitySchemes": { + "basic_auth": { + "type": "http", + "scheme": "basic" + }, + "bearer_auth": { + "type": "http", + "scheme": "bearer" + } + }, + "schemas": { + "Capabilities": { + "type": "object", + "required": [ + "files_sharing" + ], + "properties": { + "files_sharing": { + "type": "object", + "required": [ + "sharebymail" + ], + "properties": { + "sharebymail": { + "type": "object", + "required": [ + "enabled", + "send_password_by_mail", + "upload_files_drop", + "password", + "expire_date" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "send_password_by_mail": { + "type": "boolean" + }, + "upload_files_drop": { + "type": "object", + "required": [ + "enabled" + ], + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "password": { + "type": "object", + "required": [ + "enabled", + "enforced" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "enforced": { + "type": "boolean" + } + } + }, + "expire_date": { + "type": "object", + "required": [ + "enabled", + "enforced" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "enforced": { + "type": "boolean" + } + } + } + } + } + } + } + } + } + } + }, + "paths": {}, + "tags": [] +} \ No newline at end of file diff --git a/apps/theming/lib/Capabilities.php b/apps/theming/lib/Capabilities.php index 5c063715f4387..fbb287aa41067 100644 --- a/apps/theming/lib/Capabilities.php +++ b/apps/theming/lib/Capabilities.php @@ -7,6 +7,7 @@ * @author Julien Veyssier * @author Julius Härtl * @author Morris Jobke + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -64,6 +65,25 @@ public function __construct(ThemingDefaults $theming, Util $util, IURLGenerator /** * Return this classes capabilities + * + * @return array{ + * theming: array{ + * name: string, + * url: string, + * slogan: string, + * color: string, + * color-text: string, + * color-element: string, + * color-element-bright: string, + * color-element-dark: string, + * logo: string, + * background: string, + * background-plain: bool, + * background-default: bool, + * logoheader: string, + * favicon: string, + * }, + * } */ public function getCapabilities() { $backgroundLogo = $this->config->getAppValue('theming', 'backgroundMime', ''); diff --git a/apps/theming/lib/Controller/IconController.php b/apps/theming/lib/Controller/IconController.php index 1b16293a7f34c..86b45fbbc032e 100644 --- a/apps/theming/lib/Controller/IconController.php +++ b/apps/theming/lib/Controller/IconController.php @@ -8,6 +8,7 @@ * @author Julius Härtl * @author Michael Weimann * @author Roeland Jago Douma + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -80,10 +81,15 @@ public function __construct( * @PublicPage * @NoCSRFRequired * - * @param $app string app name - * @param $image string image file name (svg required) - * @return FileDisplayResponse|NotFoundResponse + * Get a themed icon + * + * @param string $app ID of the app + * @param string $image image file name (svg required) + * @return FileDisplayResponse|NotFoundResponse * @throws \Exception + * + * 200: Themed icon returned + * 404: Themed icon not found */ public function getThemedIcon(string $app, string $image): Response { $color = $this->themingDefaults->getColorPrimary(); @@ -107,9 +113,12 @@ public function getThemedIcon(string $app, string $image): Response { * @PublicPage * @NoCSRFRequired * - * @param $app string app name - * @return FileDisplayResponse|DataDisplayResponse|NotFoundResponse + * @param string $app ID of the app + * @return DataDisplayResponse|FileDisplayResponse|NotFoundResponse * @throws \Exception + * + * 200: Favicon returned + * 404: Favicon not found */ public function getFavicon(string $app = 'core'): Response { $response = null; @@ -146,9 +155,12 @@ public function getFavicon(string $app = 'core'): Response { * @PublicPage * @NoCSRFRequired * - * @param $app string app name - * @return DataDisplayResponse|FileDisplayResponse|NotFoundResponse + * @param string $app ID of the app + * @return DataDisplayResponse|FileDisplayResponse|NotFoundResponse * @throws \Exception + * + * 200: Touch icon returned + * 404: Touch icon not found */ public function getTouchIcon(string $app = 'core'): Response { $response = null; diff --git a/apps/theming/lib/Controller/ThemingController.php b/apps/theming/lib/Controller/ThemingController.php index c247f2c9d566b..33af2b72183ce 100644 --- a/apps/theming/lib/Controller/ThemingController.php +++ b/apps/theming/lib/Controller/ThemingController.php @@ -18,6 +18,7 @@ * @author Robin Appelman * @author Roeland Jago Douma * @author Thomas Citharel + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -46,6 +47,7 @@ use OCP\AppFramework\Http\DataDisplayResponse; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\FileDisplayResponse; +use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\NotFoundResponse; use OCP\Files\IAppData; use OCP\Files\NotFoundException; @@ -314,10 +316,15 @@ public function undoAll(): DataResponse { * @NoCSRFRequired * @NoSameSiteCookieRequired * - * @param string $key - * @param bool $useSvg - * @return FileDisplayResponse|NotFoundResponse + * Get an image + * + * @param string $key Key of the image + * @param bool $useSvg Return image as SVG + * @return FileDisplayResponse|NotFoundResponse * @throws NotPermittedException + * + * 200: Image returned + * 404: Image not found */ public function getImage(string $key, bool $useSvg = true) { try { @@ -347,7 +354,15 @@ public function getImage(string $key, bool $useSvg = true) { * @NoSameSiteCookieRequired * @NoTwoFactorRequired * - * @return DataDisplayResponse|NotFoundResponse + * Get the CSS stylesheet for a theme + * + * @param string $themeId ID of the theme + * @param bool $plain Let the browser decide the CSS priority + * @param bool $withCustomCss Include custom CSS + * @return DataDisplayResponse|NotFoundResponse + * + * 200: Stylesheet returned + * 404: Theme not found */ public function getThemeStylesheet(string $themeId, bool $plain = false, bool $withCustomCss = false) { $themes = $this->themesService->getThemes(); @@ -387,9 +402,13 @@ public function getThemeStylesheet(string $themeId, bool $plain = false, bool $w * @NoCSRFRequired * @PublicPage * - * @return Http\JSONResponse + * Get the manifest for an app + * + * @param string $app ID of the app + * @psalm-suppress LessSpecificReturnStatement The content of the Manifest doesn't need to be described in the return type + * @return JSONResponse */ - public function getManifest($app) { + public function getManifest(string $app) { $cacheBusterValue = $this->config->getAppValue('theming', 'cachebuster', '0'); if ($app === 'core' || $app === 'settings') { $name = $this->themingDefaults->getName(); @@ -407,6 +426,10 @@ public function getManifest($app) { } $description = $info['summary'] ?? ''; } + /** + * @var string $description + * @var string $shortName + */ $responseJS = [ 'name' => $name, 'short_name' => $shortName, @@ -431,7 +454,7 @@ public function getManifest($app) { ], 'display' => 'standalone' ]; - $response = new Http\JSONResponse($responseJS); + $response = new JSONResponse($responseJS); $response->cacheFor(3600); return $response; } diff --git a/apps/theming/lib/Controller/UserThemeController.php b/apps/theming/lib/Controller/UserThemeController.php index 6a58366c4f688..19a4cf1802dc8 100644 --- a/apps/theming/lib/Controller/UserThemeController.php +++ b/apps/theming/lib/Controller/UserThemeController.php @@ -11,6 +11,7 @@ * @author Janis Köhr * @author John Molakvoæ * @author Roeland Jago Douma + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -32,6 +33,7 @@ use OCA\Theming\AppInfo\Application; use OCA\Theming\ITheme; +use OCA\Theming\ResponseDefinitions; use OCA\Theming\Service\BackgroundService; use OCA\Theming\Service\ThemesService; use OCA\Theming\ThemingDefaults; @@ -48,10 +50,13 @@ use OCP\IUserSession; use OCP\PreConditionNotMetException; +/** + * @psalm-import-type ThemingBackground from ResponseDefinitions + */ class UserThemeController extends OCSController { protected ?string $userId = null; - + private IConfig $config; private IUserSession $userSession; private ThemesService $themesService; @@ -84,15 +89,18 @@ public function __construct(string $appName, * Enable theme * * @param string $themeId the theme ID - * @return DataResponse - * @throws OCSBadRequestException|PreConditionNotMetException + * @return DataResponse + * @throws OCSBadRequestException Enabling theme is not possible + * @throws PreConditionNotMetException + * + * 200: Theme enabled successfully */ public function enableTheme(string $themeId): DataResponse { $theme = $this->validateTheme($themeId); // Enable selected theme $this->themesService->enableTheme($theme); - return new DataResponse(); + return new DataResponse(new \stdClass()); } /** @@ -101,15 +109,18 @@ public function enableTheme(string $themeId): DataResponse { * Disable theme * * @param string $themeId the theme ID - * @return DataResponse - * @throws OCSBadRequestException|PreConditionNotMetException + * @return DataResponse + * @throws OCSBadRequestException Disabling theme is not possible + * @throws PreConditionNotMetException + * + * 200: Theme disabled successfully */ public function disableTheme(string $themeId): DataResponse { $theme = $this->validateTheme($themeId); // Enable selected theme $this->themesService->disableTheme($theme); - return new DataResponse(); + return new DataResponse(new \stdClass()); } /** @@ -119,7 +130,8 @@ public function disableTheme(string $themeId): DataResponse { * * @param string $themeId the theme ID * @return ITheme - * @throws OCSBadRequestException|PreConditionNotMetException + * @throws OCSBadRequestException + * @throws PreConditionNotMetException */ private function validateTheme(string $themeId): ITheme { if ($themeId === '' || !$themeId) { @@ -143,6 +155,12 @@ private function validateTheme(string $themeId): ITheme { /** * @NoAdminRequired * @NoCSRFRequired + * + * Get the background image + * @return FileDisplayResponse|NotFoundResponse + * + * 200: Background image returned + * 404: Background image not found */ public function getBackground(): Http\Response { $file = $this->backgroundService->getBackground(); @@ -156,6 +174,10 @@ public function getBackground(): Http\Response { /** * @NoAdminRequired + * + * Delete the background + * + * @return JSONResponse */ public function deleteBackground(): JSONResponse { $currentVersion = (int)$this->config->getUserValue($this->userId, Application::APP_ID, 'userCacheBuster', '0'); @@ -169,6 +191,16 @@ public function deleteBackground(): JSONResponse { /** * @NoAdminRequired + * + * Set the background + * + * @param string $type Type of background + * @param string $value Path of the background image + * @param string|null $color Color for the background + * @return JSONResponse|JSONResponse + * + * 200: Background set successfully + * 400: Setting background is not possible */ public function setBackground(string $type = BackgroundService::BACKGROUND_DEFAULT, string $value = '', string $color = null): JSONResponse { $currentVersion = (int)$this->config->getUserValue($this->userId, Application::APP_ID, 'userCacheBuster', '0'); diff --git a/apps/theming/lib/ResponseDefinitions.php b/apps/theming/lib/ResponseDefinitions.php new file mode 100644 index 0000000000000..ddd8615e45502 --- /dev/null +++ b/apps/theming/lib/ResponseDefinitions.php @@ -0,0 +1,36 @@ + + * + * @author Kate Döen + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Theming; + +/** + * @psalm-type ThemingBackground = array{ + * backgroundImage: ?string, + * backgroundColor: string, + * version: int, + * } + */ +class ResponseDefinitions { +} diff --git a/apps/theming/openapi.json b/apps/theming/openapi.json index 9ba6919161f0e..99f3a7d96e4ce 100644 --- a/apps/theming/openapi.json +++ b/apps/theming/openapi.json @@ -187,13 +187,6 @@ "responses": { "200": { "description": "Stylesheet returned", - "headers": { - "Content-Disposition": { - "schema": { - "type": "string" - } - } - }, "content": { "text/css": { "schema": { @@ -255,13 +248,6 @@ "responses": { "200": { "description": "Image returned", - "headers": { - "Content-Disposition": { - "schema": { - "type": "string" - } - } - }, "content": { "*/*": { "schema": { @@ -368,7 +354,8 @@ ], "properties": { "src": { - "type": "string" + "type": "string", + "minLength": 1 }, "type": { "type": "string" @@ -421,13 +408,6 @@ "responses": { "200": { "description": "Favicon returned", - "headers": { - "Content-Disposition": { - "schema": { - "type": "string" - } - } - }, "content": { "image/x-icon": { "schema": { @@ -491,13 +471,6 @@ "responses": { "200": { "description": "Touch icon returned", - "headers": { - "Content-Disposition": { - "schema": { - "type": "string" - } - } - }, "content": { "image/png": { "schema": { @@ -576,13 +549,6 @@ "responses": { "200": { "description": "Themed icon returned", - "headers": { - "Content-Disposition": { - "schema": { - "type": "string" - } - } - }, "content": { "image/svg+xml": { "schema": { @@ -644,13 +610,6 @@ "responses": { "200": { "description": "Background image returned", - "headers": { - "Content-Disposition": { - "schema": { - "type": "string" - } - } - }, "content": { "*/*": { "schema": { diff --git a/apps/theming/tests/Controller/UserThemeControllerTest.php b/apps/theming/tests/Controller/UserThemeControllerTest.php index 851dbec38eb38..17cfcdbd59311 100644 --- a/apps/theming/tests/Controller/UserThemeControllerTest.php +++ b/apps/theming/tests/Controller/UserThemeControllerTest.php @@ -131,7 +131,7 @@ public function testEnableTheme($themeId, string $exception = null) { $this->expectException($exception); } - $expected = new DataResponse(); + $expected = new DataResponse(new \stdClass()); $this->assertEquals($expected, $this->userThemeController->enableTheme($themeId)); } @@ -151,7 +151,7 @@ public function testDisableTheme($themeId, string $exception = null) { $this->expectException($exception); } - $expected = new DataResponse(); + $expected = new DataResponse(new \stdClass()); $this->assertEquals($expected, $this->userThemeController->disableTheme($themeId)); } } diff --git a/apps/updatenotification/lib/Controller/APIController.php b/apps/updatenotification/lib/Controller/APIController.php index 26657eb66f0b8..8833f6e772e0c 100644 --- a/apps/updatenotification/lib/Controller/APIController.php +++ b/apps/updatenotification/lib/Controller/APIController.php @@ -27,6 +27,7 @@ namespace OCA\UpdateNotification\Controller; use OC\App\AppStore\Fetcher\AppFetcher; +use OCA\UpdateNotification\ResponseDefinitions; use OCP\App\AppPathNotFoundException; use OCP\App\IAppManager; use OCP\AppFramework\Http; @@ -37,6 +38,9 @@ use OCP\IUserSession; use OCP\L10N\IFactory; +/** + * @psalm-import-type UpdatenotificationApp from ResponseDefinitions + */ class APIController extends OCSController { /** @var IConfig */ @@ -86,8 +90,14 @@ public function __construct(string $appName, } /** - * @param string $newVersion - * @return DataResponse + * List available updates for apps + * + * @param string $newVersion Server version to check updates for + * + * @return DataResponse|DataResponse + * + * 200: Apps returned + * 404: New versions not found */ public function getAppList(string $newVersion): DataResponse { if (!$this->config->getSystemValue('appstoreenabled', true)) { @@ -157,13 +167,15 @@ public function getAppList(string $newVersion): DataResponse { * Get translated app name * * @param string $appId - * @return string[] + * @return UpdatenotificationApp */ protected function getAppDetails(string $appId): array { $app = $this->appManager->getAppInfo($appId, false, $this->language); + /** @var ?string $name */ + $name = $app['name']; return [ 'appId' => $appId, - 'appName' => $app['name'] ?? $appId, + 'appName' => $name ?? $appId, ]; } } diff --git a/apps/updatenotification/lib/ResponseDefinitions.php b/apps/updatenotification/lib/ResponseDefinitions.php new file mode 100644 index 0000000000000..01b16b81dd053 --- /dev/null +++ b/apps/updatenotification/lib/ResponseDefinitions.php @@ -0,0 +1,35 @@ + + * + * @author Kate Döen + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\UpdateNotification; + +/** + * @psalm-type UpdatenotificationApp = array{ + * appId: string, + * appName: string, + * } + */ +class ResponseDefinitions { +} diff --git a/apps/updatenotification/openapi.json b/apps/updatenotification/openapi.json new file mode 100644 index 0000000000000..7897a25373d9c --- /dev/null +++ b/apps/updatenotification/openapi.json @@ -0,0 +1,208 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "updatenotification", + "version": "0.0.1", + "description": "Displays update notifications for Nextcloud and provides the SSO for the updater.", + "license": { + "name": "agpl" + } + }, + "components": { + "securitySchemes": { + "basic_auth": { + "type": "http", + "scheme": "basic" + }, + "bearer_auth": { + "type": "http", + "scheme": "bearer" + } + }, + "schemas": { + "App": { + "type": "object", + "required": [ + "appId", + "appName" + ], + "properties": { + "appId": { + "type": "string" + }, + "appName": { + "type": "string" + } + } + }, + "OCSMeta": { + "type": "object", + "required": [ + "status", + "statuscode" + ], + "properties": { + "status": { + "type": "string" + }, + "statuscode": { + "type": "integer" + }, + "message": { + "type": "string" + }, + "totalitems": { + "type": "string" + }, + "itemsperpage": { + "type": "string" + } + } + } + } + }, + "paths": { + "/ocs/v2.php/apps/updatenotification/api/{apiVersion}/applist/{newVersion}": { + "get": { + "operationId": "api-get-app-list", + "summary": "List available updates for apps", + "description": "This endpoint requires admin access", + "tags": [ + "api" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v1" + ], + "default": "v1" + } + }, + { + "name": "newVersion", + "in": "path", + "description": "Server version to check updates for", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "required": true, + "schema": { + "type": "string", + "default": "true" + } + } + ], + "responses": { + "200": { + "description": "Apps returned", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "missing", + "available" + ], + "properties": { + "missing": { + "type": "array", + "items": { + "$ref": "#/components/schemas/App" + } + }, + "available": { + "type": "array", + "items": { + "$ref": "#/components/schemas/App" + } + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "New versions not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "appstore_disabled" + ], + "properties": { + "appstore_disabled": { + "type": "boolean" + }, + "already_on_latest": { + "type": "boolean" + } + } + } + } + } + } + } + } + } + } + } + } + } + }, + "tags": [] +} \ No newline at end of file diff --git a/apps/user_ldap/lib/Controller/ConfigAPIController.php b/apps/user_ldap/lib/Controller/ConfigAPIController.php index e408d03fcd562..f500d40adf812 100644 --- a/apps/user_ldap/lib/Controller/ConfigAPIController.php +++ b/apps/user_ldap/lib/Controller/ConfigAPIController.php @@ -29,6 +29,7 @@ use OCA\User_LDAP\Configuration; use OCA\User_LDAP\ConnectionFactory; use OCA\User_LDAP\Helper; +use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCS\OCSBadRequestException; use OCP\AppFramework\OCS\OCSException; @@ -76,42 +77,10 @@ public function __construct( } /** - * Creates a new (empty) configuration and returns the resulting prefix - * - * Example: curl -X POST -H "OCS-APIREQUEST: true" -u $admin:$password \ - * https://nextcloud.server/ocs/v2.php/apps/user_ldap/api/v1/config - * - * results in: - * - * - * - * - * ok - * 200 - * OK - * - * - * s40 - * - * - * - * Failing example: if an exception is thrown (e.g. Database connection lost) - * the detailed error will be logged. The output will then look like: - * - * - * - * - * failure - * 999 - * An issue occurred when creating the new config. - * - * - * - * - * For JSON output provide the format=json parameter + * Create a new (empty) configuration and return the resulting prefix * * @AuthorizedAdminSetting(settings=OCA\User_LDAP\Settings\Admin) - * @return DataResponse + * @return DataResponse * @throws OCSException */ public function create() { @@ -128,27 +97,15 @@ public function create() { } /** - * Deletes a LDAP configuration, if present. - * - * Example: - * curl -X DELETE -H "OCS-APIREQUEST: true" -u $admin:$password \ - * https://nextcloud.server/ocs/v2.php/apps/user_ldap/api/v1/config/s60 - * - * - * - * - * ok - * 200 - * OK - * - * - * + * Delete a LDAP configuration * * @AuthorizedAdminSetting(settings=OCA\User_LDAP\Settings\Admin) - * @param string $configID - * @return DataResponse - * @throws OCSBadRequestException + * @param string $configID ID of the config + * @return DataResponse * @throws OCSException + * @throws OCSNotFoundException Config not found + * + * 200: Config deleted successfully */ public function delete($configID) { try { @@ -163,32 +120,21 @@ public function delete($configID) { throw new OCSException('An issue occurred when deleting the config.'); } - return new DataResponse(); + return new DataResponse(new \stdClass()); } /** - * Modifies a configuration - * - * Example: - * curl -X PUT -d "configData[ldapHost]=ldaps://my.ldap.server&configData[ldapPort]=636" \ - * -H "OCS-APIREQUEST: true" -u $admin:$password \ - * https://nextcloud.server/ocs/v2.php/apps/user_ldap/api/v1/config/s60 - * - * - * - * - * ok - * 200 - * OK - * - * - * + * Modify a configuration * * @AuthorizedAdminSetting(settings=OCA\User_LDAP\Settings\Admin) - * @param string $configID - * @param array $configData - * @return DataResponse + * @param string $configID ID of the config + * @param array $configData New config + * @return DataResponse * @throws OCSException + * @throws OCSBadRequestException Modifying config is not possible + * @throws OCSNotFoundException Config not found + * + * 200: Config returned */ public function modify($configID, $configData) { try { @@ -216,79 +162,20 @@ public function modify($configID, $configData) { throw new OCSException('An issue occurred when modifying the config.'); } - return new DataResponse(); + return new DataResponse(new \stdClass()); } /** - * Retrieves a configuration - * - * - * - * - * ok - * 200 - * OK - * - * - * ldaps://my.ldap.server - * 7770 - * - * - * ou=small,dc=my,dc=ldap,dc=server - * ou=users,ou=small,dc=my,dc=ldap,dc=server - * ou=small,dc=my,dc=ldap,dc=server - * cn=root,dc=my,dc=ldap,dc=server - * clearTextWithShowPassword=1 - * 1 - * 0 - * - * displayname - * uid - * inetOrgPerson - * - * (&(objectclass=nextcloudUser)(nextcloudEnabled=TRUE)) - * 1 - * (&(|(objectclass=nextcloudGroup))) - * 0 - * nextcloudGroup - * - * cn - * memberUid - * (&(|(objectclass=inetOrgPerson))(uid=%uid)) - * 0 - * 0 - * 1 - * - * - * - * mail - * 20 - * auto - * auto - * - * 1 - * uid;sn;givenname - * - * 0 - * - * - * 1 - * uid - * uid - * - * 0 - * 0 - * 500 - * 1 - * - * - * + * Get a configuration * * @AuthorizedAdminSetting(settings=OCA\User_LDAP\Settings\Admin) - * @param string $configID - * @param bool|string $showPassword - * @return DataResponse + * @param string $configID ID of the config + * @param bool $showPassword Whether to show the password + * @return DataResponse, array{}> * @throws OCSException + * @throws OCSNotFoundException Config not found + * + * 200: Config returned */ public function show($configID, $showPassword = false) { try { @@ -296,7 +183,7 @@ public function show($configID, $showPassword = false) { $config = new Configuration($configID); $data = $config->getConfiguration(); - if (!(int)$showPassword) { + if (!$showPassword) { $data['ldapAgentPassword'] = '***'; } foreach ($data as $key => $value) { diff --git a/apps/user_ldap/openapi.json b/apps/user_ldap/openapi.json new file mode 100644 index 0000000000000..52ec806f5b7b8 --- /dev/null +++ b/apps/user_ldap/openapi.json @@ -0,0 +1,390 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "user_ldap", + "version": "0.0.1", + "description": "This application enables administrators to connect Nextcloud to an LDAP-based user directory.", + "license": { + "name": "agpl" + } + }, + "components": { + "securitySchemes": { + "basic_auth": { + "type": "http", + "scheme": "basic" + }, + "bearer_auth": { + "type": "http", + "scheme": "bearer" + } + }, + "schemas": { + "OCSMeta": { + "type": "object", + "required": [ + "status", + "statuscode" + ], + "properties": { + "status": { + "type": "string" + }, + "statuscode": { + "type": "integer" + }, + "message": { + "type": "string" + }, + "totalitems": { + "type": "string" + }, + "itemsperpage": { + "type": "string" + } + } + } + } + }, + "paths": { + "/ocs/v2.php/apps/user_ldap/api/v1/config": { + "post": { + "operationId": "configapi-create", + "summary": "Create a new (empty) configuration and return the resulting prefix", + "description": "This endpoint requires admin access", + "tags": [ + "configapi" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "OCS-APIRequest", + "in": "header", + "required": true, + "schema": { + "type": "string", + "default": "true" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "configID" + ], + "properties": { + "configID": { + "type": "string" + } + } + } + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/user_ldap/api/v1/config/{configID}": { + "get": { + "operationId": "configapi-show", + "summary": "Get a configuration", + "description": "This endpoint requires admin access", + "tags": [ + "configapi" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "showPassword", + "in": "query", + "description": "Whether to show the password", + "schema": { + "type": "integer", + "default": 0 + } + }, + { + "name": "configID", + "in": "path", + "description": "ID of the config", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "required": true, + "schema": { + "type": "string", + "default": "true" + } + } + ], + "responses": { + "200": { + "description": "Config returned", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "additionalProperties": { + "type": "object" + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Config not found", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + }, + "put": { + "operationId": "configapi-modify", + "summary": "Modify a configuration", + "description": "This endpoint requires admin access", + "tags": [ + "configapi" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "configData", + "in": "query", + "description": "New config", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "configID", + "in": "path", + "description": "ID of the config", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "required": true, + "schema": { + "type": "string", + "default": "true" + } + } + ], + "responses": { + "200": { + "description": "Config returned", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "additionalProperties": true + } + } + } + } + } + } + } + }, + "400": { + "description": "Modifying config is not possible", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + }, + "404": { + "description": "Config not found", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + }, + "delete": { + "operationId": "configapi-delete", + "summary": "Delete a LDAP configuration", + "description": "This endpoint requires admin access", + "tags": [ + "configapi" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "configID", + "in": "path", + "description": "ID of the config", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "required": true, + "schema": { + "type": "string", + "default": "true" + } + } + ], + "responses": { + "200": { + "description": "Config deleted successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "additionalProperties": true + } + } + } + } + } + } + } + }, + "404": { + "description": "Config not found", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + }, + "tags": [] +} \ No newline at end of file diff --git a/apps/user_status/lib/Capabilities.php b/apps/user_status/lib/Capabilities.php index 67e73f2733caa..85652e17b4e67 100644 --- a/apps/user_status/lib/Capabilities.php +++ b/apps/user_status/lib/Capabilities.php @@ -6,6 +6,7 @@ * @copyright Copyright (c) 2020, Georg Ehrke * * @author Georg Ehrke + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -40,6 +41,9 @@ public function __construct(IEmojiHelper $emojiHelper) { $this->emojiHelper = $emojiHelper; } + /** + * @return array{user_status: array{enabled: bool, restore: bool, supports_emoji: bool}} + */ public function getCapabilities() { return [ 'user_status' => [ diff --git a/apps/user_status/lib/Controller/HeartbeatController.php b/apps/user_status/lib/Controller/HeartbeatController.php index e0b735f044f7b..776789927668b 100644 --- a/apps/user_status/lib/Controller/HeartbeatController.php +++ b/apps/user_status/lib/Controller/HeartbeatController.php @@ -6,6 +6,7 @@ * @copyright Copyright (c) 2020, Georg Ehrke * * @author Georg Ehrke + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -26,6 +27,7 @@ namespace OCA\UserStatus\Controller; use OCA\UserStatus\Db\UserStatus; +use OCA\UserStatus\ResponseDefinitions; use OCA\UserStatus\Service\StatusService; use OCP\AppFramework\Controller; use OCP\AppFramework\Db\DoesNotExistException; @@ -39,6 +41,9 @@ use OCP\User\Events\UserLiveStatusEvent; use OCP\UserStatus\IUserStatus; +/** + * @psalm-import-type UserStatusPrivate from ResponseDefinitions + */ class HeartbeatController extends OCSController { /** @var IEventDispatcher */ @@ -67,19 +72,25 @@ public function __construct(string $appName, } /** + * Keep the current status alive + * * @NoAdminRequired * - * @param string $status - * @return DataResponse + * @param string $status Only online, away + * + * @return DataResponse|DataResponse + * 200: Status successfully updated + * 204: User has no status to keep alive + * 400: Invalid status to update */ public function heartbeat(string $status): DataResponse { if (!\in_array($status, [IUserStatus::ONLINE, IUserStatus::AWAY], true)) { - return new DataResponse([], Http::STATUS_BAD_REQUEST); + return new DataResponse(new \stdClass(), Http::STATUS_BAD_REQUEST); } $user = $this->userSession->getUser(); if ($user === null) { - return new DataResponse([], Http::STATUS_INTERNAL_SERVER_ERROR); + return new DataResponse(new \stdClass(), Http::STATUS_INTERNAL_SERVER_ERROR); } $event = new UserLiveStatusEvent( @@ -92,7 +103,7 @@ public function heartbeat(string $status): DataResponse { $userStatus = $event->getUserStatus(); if (!$userStatus) { - return new DataResponse([], Http::STATUS_NO_CONTENT); + return new DataResponse(new \stdClass(), Http::STATUS_NO_CONTENT); } /** @psalm-suppress UndefinedInterfaceMethod */ diff --git a/apps/user_status/lib/Controller/PredefinedStatusController.php b/apps/user_status/lib/Controller/PredefinedStatusController.php index ea1ff5209b854..0a108a56bf6c8 100644 --- a/apps/user_status/lib/Controller/PredefinedStatusController.php +++ b/apps/user_status/lib/Controller/PredefinedStatusController.php @@ -6,6 +6,7 @@ * @copyright Copyright (c) 2020, Georg Ehrke * * @author Georg Ehrke + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -25,15 +26,17 @@ */ namespace OCA\UserStatus\Controller; +use OCA\UserStatus\ResponseDefinitions; use OCA\UserStatus\Service\PredefinedStatusService; +use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCSController; use OCP\IRequest; /** - * Class DefaultStatusController - * * @package OCA\UserStatus\Controller + * + * @psalm-import-type UserStatusPredefined from ResponseDefinitions */ class PredefinedStatusController extends OCSController { @@ -55,9 +58,11 @@ public function __construct(string $appName, } /** + * Get all predefined messages + * * @NoAdminRequired * - * @return DataResponse + * @return DataResponse */ public function findAll():DataResponse { // Filtering out the invisible one, that should only be set by API diff --git a/apps/user_status/lib/Controller/StatusesController.php b/apps/user_status/lib/Controller/StatusesController.php index d30389e1716c6..0e4deca13b708 100644 --- a/apps/user_status/lib/Controller/StatusesController.php +++ b/apps/user_status/lib/Controller/StatusesController.php @@ -7,6 +7,7 @@ * * @author Christoph Wurst * @author Georg Ehrke + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -27,14 +28,19 @@ namespace OCA\UserStatus\Controller; use OCA\UserStatus\Db\UserStatus; +use OCA\UserStatus\ResponseDefinitions; use OCA\UserStatus\Service\StatusService; use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCS\OCSNotFoundException; use OCP\AppFramework\OCSController; use OCP\IRequest; use OCP\UserStatus\IUserStatus; +/** + * @psalm-import-type UserStatusPublic from ResponseDefinitions + */ class StatusesController extends OCSController { /** @var StatusService */ @@ -55,11 +61,13 @@ public function __construct(string $appName, } /** + * Find statuses of users + * * @NoAdminRequired * - * @param int|null $limit - * @param int|null $offset - * @return DataResponse + * @param int|null $limit Maximum number of statuses to find + * @param int|null $offset Offset for finding statuses + * @return DataResponse */ public function findAll(?int $limit = null, ?int $offset = null): DataResponse { $allStatuses = $this->service->findAll($limit, $offset); @@ -70,11 +78,15 @@ public function findAll(?int $limit = null, ?int $offset = null): DataResponse { } /** + * Find the status of a user + * * @NoAdminRequired * - * @param string $userId - * @return DataResponse - * @throws OCSNotFoundException + * @param string $userId ID of the user + * @return DataResponse + * @throws OCSNotFoundException The user was not found + * + * 200: The status was found successfully */ public function find(string $userId): DataResponse { try { @@ -88,7 +100,7 @@ public function find(string $userId): DataResponse { /** * @param UserStatus $status - * @return array{userId: string, message: string, icon: string, clearAt: int, status: string} + * @return UserStatusPublic */ private function formatStatus(UserStatus $status): array { $visibleStatus = $status->getStatus(); diff --git a/apps/user_status/lib/Controller/UserStatusController.php b/apps/user_status/lib/Controller/UserStatusController.php index 2d96cd90a40d3..9491bef80b3b1 100644 --- a/apps/user_status/lib/Controller/UserStatusController.php +++ b/apps/user_status/lib/Controller/UserStatusController.php @@ -8,6 +8,7 @@ * @author Georg Ehrke * @author Joas Schilling * @author Simon Spannagel + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -33,8 +34,10 @@ use OCA\UserStatus\Exception\InvalidStatusIconException; use OCA\UserStatus\Exception\InvalidStatusTypeException; use OCA\UserStatus\Exception\StatusMessageTooLongException; +use OCA\UserStatus\ResponseDefinitions; use OCA\UserStatus\Service\StatusService; use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCS\OCSBadRequestException; use OCP\AppFramework\OCS\OCSNotFoundException; @@ -42,6 +45,9 @@ use OCP\ILogger; use OCP\IRequest; +/** + * @psalm-import-type UserStatusPrivate from ResponseDefinitions + */ class UserStatusController extends OCSController { /** @var string */ @@ -74,10 +80,14 @@ public function __construct(string $appName, } /** + * Get the status of the current user + * * @NoAdminRequired * - * @return DataResponse - * @throws OCSNotFoundException + * @return DataResponse + * @throws OCSNotFoundException The user was not found + * + * 200: The status was found successfully */ public function getStatus(): DataResponse { try { @@ -90,11 +100,15 @@ public function getStatus(): DataResponse { } /** + * Update the status type of the current user + * * @NoAdminRequired * - * @param string $statusType - * @return DataResponse - * @throws OCSBadRequestException + * @param string $statusType The new status type + * @return DataResponse + * @throws OCSBadRequestException The status type is invalid + * + * 200: The status was updated successfully */ public function setStatus(string $statusType): DataResponse { try { @@ -109,12 +123,16 @@ public function setStatus(string $statusType): DataResponse { } /** + * Set the message to a predefined message for the current user + * * @NoAdminRequired * - * @param string $messageId - * @param int|null $clearAt - * @return DataResponse - * @throws OCSBadRequestException + * @param string $messageId ID of the predefined message + * @param int|null $clearAt When the message should be cleared + * @return DataResponse + * @throws OCSBadRequestException The clearAt or message-id is invalid + * + * 200: The message was updated successfully */ public function setPredefinedMessage(string $messageId, ?int $clearAt): DataResponse { @@ -132,13 +150,17 @@ public function setPredefinedMessage(string $messageId, } /** + * Set the message to a custom message for the current user + * * @NoAdminRequired * - * @param string|null $statusIcon - * @param string|null $message - * @param int|null $clearAt - * @return DataResponse - * @throws OCSBadRequestException + * @param string|null $statusIcon Icon of the status + * @param string|null $message Message of the status + * @param int|null $clearAt When the message should be cleared + * @return DataResponse + * @throws OCSBadRequestException The clearAt or icon is invalid or the message is too long + * + * 200: The message was updated successfully */ public function setCustomMessage(?string $statusIcon, ?string $message, @@ -165,31 +187,39 @@ public function setCustomMessage(?string $statusIcon, } /** + * Clear the message of the current user + * * @NoAdminRequired * - * @return DataResponse + * @return DataResponse */ public function clearMessage(): DataResponse { $this->service->clearMessage($this->userId); - return new DataResponse([]); + return new DataResponse(new \stdClass()); } /** + * Revert the status to the previous status + * * @NoAdminRequired * - * @return DataResponse + * @param string $messageId ID of the message to delete + * + * @return DataResponse + * + * 200: Status reverted */ public function revertStatus(string $messageId): DataResponse { $backupStatus = $this->service->revertUserStatus($this->userId, $messageId, true); if ($backupStatus) { return new DataResponse($this->formatStatus($backupStatus)); } - return new DataResponse([]); + return new DataResponse(null); } /** * @param UserStatus $status - * @return array + * @return UserStatusPrivate */ private function formatStatus(UserStatus $status): array { return [ diff --git a/apps/user_status/lib/Db/UserStatus.php b/apps/user_status/lib/Db/UserStatus.php index 8907c4a2c1bbf..d4e24e7559772 100644 --- a/apps/user_status/lib/Db/UserStatus.php +++ b/apps/user_status/lib/Db/UserStatus.php @@ -42,13 +42,13 @@ * @method void setStatusTimestamp(int $statusTimestamp) * @method bool getIsUserDefined() * @method void setIsUserDefined(bool $isUserDefined) - * @method string getMessageId() + * @method string|null getMessageId() * @method void setMessageId(string|null $messageId) - * @method string getCustomIcon() + * @method string|null getCustomIcon() * @method void setCustomIcon(string|null $customIcon) - * @method string getCustomMessage() + * @method string|null getCustomMessage() * @method void setCustomMessage(string|null $customMessage) - * @method int getClearAt() + * @method int|null getClearAt() * @method void setClearAt(int|null $clearAt) * @method setIsBackup(bool $true): void * @method getIsBackup(): bool diff --git a/apps/user_status/lib/ResponseDefinitions.php b/apps/user_status/lib/ResponseDefinitions.php new file mode 100644 index 0000000000000..2c20bd9f126c2 --- /dev/null +++ b/apps/user_status/lib/ResponseDefinitions.php @@ -0,0 +1,59 @@ + + * + * @author Kate Döen + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\UserStatus; + +/** + * @psalm-type UserStatusClearAtTimeType = "day"|"week" + * + * @psalm-type UserStatusClearAt = array{ + * type: "period"|"end-of", + * time: int|UserStatusClearAtTimeType, + * } + * + * @psalm-type UserStatusPredefined = array{ + * id: string, + * icon: string, + * message: string, + * clearAt: ?UserStatusClearAt, + * visible: ?bool, + * } + * + * @psalm-type UserStatusPublic = array{ + * userId: string, + * message: ?string, + * icon: ?string, + * clearAt: ?int, + * status: string, + * } + * + * @psalm-type UserStatusPrivate = UserStatusPublic&array{ + * messageId: ?string, + * messageIsPredefined: bool, + * statusIsUserDefined: bool, + * } + */ +class ResponseDefinitions { +} diff --git a/apps/user_status/tests/Unit/Controller/UserStatusControllerTest.php b/apps/user_status/tests/Unit/Controller/UserStatusControllerTest.php index ed0919eb9a519..b903d464f8fcc 100644 --- a/apps/user_status/tests/Unit/Controller/UserStatusControllerTest.php +++ b/apps/user_status/tests/Unit/Controller/UserStatusControllerTest.php @@ -332,7 +332,7 @@ public function testClearMessage(): void { ->with('john.doe'); $response = $this->controller->clearMessage(); - $this->assertEquals([], $response->getData()); + $this->assertEquals(\stdClass, $response->getData()); } private function getUserStatus(): UserStatus { diff --git a/apps/weather_status/lib/Capabilities.php b/apps/weather_status/lib/Capabilities.php index 60cbb4602fdc5..b7362a886882d 100644 --- a/apps/weather_status/lib/Capabilities.php +++ b/apps/weather_status/lib/Capabilities.php @@ -43,6 +43,9 @@ class Capabilities implements ICapability { public function __construct() { } + /** + * @return array{weather_status: array{enabled: bool}} + */ public function getCapabilities() { return [ Application::APP_ID => [ diff --git a/apps/weather_status/lib/Controller/WeatherStatusController.php b/apps/weather_status/lib/Controller/WeatherStatusController.php index 01bdf78f4107e..1a5524967b598 100644 --- a/apps/weather_status/lib/Controller/WeatherStatusController.php +++ b/apps/weather_status/lib/Controller/WeatherStatusController.php @@ -25,6 +25,7 @@ */ namespace OCA\WeatherStatus\Controller; +use OCA\WeatherStatus\ResponseDefinitions; use OCA\WeatherStatus\Service\WeatherStatusService; use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; @@ -32,6 +33,9 @@ use OCP\ILogger; use OCP\IRequest; +/** + * @psalm-import-type WeatherStatusForecast from ResponseDefinitions + */ class WeatherStatusController extends OCSController { /** @var string */ @@ -59,7 +63,7 @@ public function __construct(string $appName, * * Try to use the address set in user personal settings as weather location * - * @return DataResponse with success state and address information + * @return DataResponse */ public function usePersonalAddress(): DataResponse { return new DataResponse($this->service->usePersonalAddress()); @@ -73,7 +77,7 @@ public function usePersonalAddress(): DataResponse { * - use the user defined address * * @param int $mode New mode - * @return DataResponse success state + * @return DataResponse */ public function setMode(int $mode): DataResponse { return new DataResponse($this->service->setMode($mode)); @@ -88,7 +92,7 @@ public function setMode(int $mode): DataResponse { * @param string|null $address Any approximative or exact address * @param float|null $lat Latitude in decimal degree format * @param float|null $lon Longitude in decimal degree format - * @return DataResponse with success state and address information + * @return DataResponse */ public function setLocation(?string $address, ?float $lat, ?float $lon): DataResponse { $currentWeather = $this->service->setLocation($address, $lat, $lon); @@ -100,7 +104,7 @@ public function setLocation(?string $address, ?float $lat, ?float $lon): DataRes * * Get stored user location * - * @return DataResponse which contains coordinates, formatted address and current weather status mode + * @return DataResponse */ public function getLocation(): DataResponse { $location = $this->service->getLocation(); @@ -112,7 +116,10 @@ public function getLocation(): DataResponse { * * Get forecast for current location * - * @return DataResponse which contains success state and filtered forecast data + * @return DataResponse|DataResponse + * + * 200: Forecast returned + * 404: Forecast not found */ public function getForecast(): DataResponse { $forecast = $this->service->getForecast(); @@ -128,7 +135,7 @@ public function getForecast(): DataResponse { * * Get favorites list * - * @return DataResponse which contains the favorite list + * @return DataResponse */ public function getFavorites(): DataResponse { return new DataResponse($this->service->getFavorites()); @@ -139,8 +146,8 @@ public function getFavorites(): DataResponse { * * Set favorites list * - * @param array $favorites - * @return DataResponse success state + * @param string[] $favorites Favorite addresses + * @return DataResponse */ public function setFavorites(array $favorites): DataResponse { return new DataResponse($this->service->setFavorites($favorites)); diff --git a/apps/weather_status/lib/ResponseDefinitions.php b/apps/weather_status/lib/ResponseDefinitions.php new file mode 100644 index 0000000000000..08f8049b96483 --- /dev/null +++ b/apps/weather_status/lib/ResponseDefinitions.php @@ -0,0 +1,87 @@ + + * + * @author Kate Döen + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\WeatherStatus; + +/** + * https://api.met.no/doc/ForecastJSON + * @psalm-type WeatherStatusForecast = array{ + * time: string, + * data: array{ + * instant: array{ + * details: array{ + * air_pressure_at_sea_level: float, + * air_temperature: float, + * cloud_area_fraction: float, + * cloud_area_fraction_high: float, + * cloud_area_fraction_low: float, + * cloud_area_fraction_medium: float, + * dew_point_temperature: float, + * fog_area_fraction: float, + * relative_humidity: float, + * ultraviolet_index_clear_sky: float, + * wind_from_direction: float, + * wind_speed: float, + * wind_speed_of_gust: float, + * }, + * }, + * next_12_hours: array{ + * summary: array{ + * symbol_code: string, + * }, + * details: array{ + * probability_of_precipitation: float, + * }, + * }, + * next_1_hours: array{ + * summary: array{ + * symbol_code: string, + * }, + * details: array{ + * precipitation_amount: float, + * precipitation_amount_max: float, + * precipitation_amount_min: float, + * probability_of_precipitation: float, + * probability_of_thunder: float, + * }, + * }, + * next_6_hours: array{ + * summary: array{ + * symbol_code: string, + * }, + * details: array{ + * air_temperature_max: float, + * air_temperature_min: float, + * precipitation_amount: float, + * precipitation_amount_max: float, + * precipitation_amount_min: float, + * probability_of_precipitation: float, + * }, + * }, + * }, + * } + */ +class ResponseDefinitions { +} diff --git a/apps/weather_status/lib/Service/WeatherStatusService.php b/apps/weather_status/lib/Service/WeatherStatusService.php index b334a5e3ba310..d54c7baeb1f08 100644 --- a/apps/weather_status/lib/Service/WeatherStatusService.php +++ b/apps/weather_status/lib/Service/WeatherStatusService.php @@ -131,8 +131,7 @@ public function setMode(int $mode): array { /** * Get favorites list - * @param array $favorites - * @return array success state + * @return string[] */ public function getFavorites(): array { $favoritesJson = $this->config->getUserValue($this->userId, Application::APP_ID, 'favorites', ''); @@ -141,7 +140,7 @@ public function getFavorites(): array { /** * Set favorites list - * @param array $favorites + * @param string[] $favorites * @return array success state */ public function setFavorites(array $favorites): array { diff --git a/build/psalm-baseline.xml b/build/psalm-baseline.xml index 59132110977a4..eddbeed05d57b 100644 --- a/build/psalm-baseline.xml +++ b/build/psalm-baseline.xml @@ -63,6 +63,14 @@ IEventListener + + + $uids + + + ]]> + + IEventListener @@ -89,7 +97,7 @@ - 'OCA\DAV\Connector\Sabre::addPlugin' + dispatch @@ -144,11 +152,17 @@ array array + + Reader::read($objectData) + $objectData $uris $uris + + VCalendar + atomic(function () use ($calendarId, $syncToken, $syncLevel, $limit, $calendarType) { // Current synctoken @@ -268,6 +282,9 @@ + + DTEND]]> + hasTime isFloating @@ -275,6 +292,12 @@ + + $emailAddresses + + + ]]> + getDateTime getDateTime @@ -292,6 +315,15 @@ + + $vevents + VObject\Reader::read($calendarData, + VObject\Reader::OPTION_FORGIVING) + + + VObject\Component\VCalendar|null + VObject\Component\VEvent[] + getDateTime getDateTime @@ -321,7 +353,7 @@ - is_array($modified['old']) + @@ -332,9 +364,12 @@ - [$aclPlugin, 'propFind'] - [$aclPlugin, 'propFind'] + + + + DTEND]]> + get getChildren @@ -365,12 +400,12 @@ - !is_array($newProps['filters']['comps']) - !is_array($newProps['filters']['params']) - !is_array($newProps['filters']['props']) - !isset($newProps['filters']['comps']) || !is_array($newProps['filters']['comps']) - !isset($newProps['filters']['params']) || !is_array($newProps['filters']['params']) - !isset($newProps['filters']['props']) || !is_array($newProps['filters']['props']) + + + + + + @@ -383,6 +418,12 @@ getKey()]]> getKey()]]> + + Reader::read($cardData) + + + VCard + @@ -393,8 +434,14 @@ false + + Reader::read($cardData) + + + VCard + - $addressBooks[$row['id']][$readOnlyPropertyName] === 0 + @@ -406,6 +453,15 @@ string + + $type, + 'body' => $val + ]]]> + + + false|array{body: string, Content-Type: string} + $type @@ -452,6 +508,14 @@ bool + + + $data + + + array{bool, string} + + tryTokenLogin @@ -467,6 +531,12 @@ \Sabre\DAV\INode[] + + node]]> + + + Folder + null null @@ -477,9 +547,15 @@ + + node]]> + $data + + \OCP\Files\File + @@ -490,7 +566,7 @@ bool - new PreconditionFailed('Cannot filter by non-existing tag', 0, $e) + \OCA\Circles\Api\v1\Circles @@ -761,8 +837,8 @@ - 'OCA\DAV\Connector\Sabre::addPlugin' - 'OCA\DAV\Connector\Sabre::authInit' + + dispatch @@ -820,12 +896,30 @@ getSize + + + $addressBookNode->getName(), + 'displayName' => $addressBookInfo['{DAV:}displayname'], + 'description' => $addressBookInfo['{' . CardDAVPlugin::NS_CARDDAV . '}addressbook-description'], + 'vCards' => $vCards, + ]]]> + + + array{name: string, displayName: string, description: ?string, vCards: VCard[]} + + + + + ]]> + + $userSession - get_class($res) === 'OpenSSLAsymmetricKey' + @@ -845,10 +939,16 @@ - new Exceptions\PrivateKeyMissingException('please try to log-out and log-in again', 0) + + + files->getMount($path)->getStorage()]]> + + + \OC\Files\Storage\Storage|null + $userSession @@ -870,8 +970,14 @@ $shareId $shareId $shareId - (int)$data['id'] + + + $nodes[0] + + + \OCP\Files\File|\OCP\Files\Folder + @@ -899,14 +1005,30 @@ string - (int)$share['id'] + + + + $result + + + ]]> + + IEventListener + + + dbHandler->getAllServer()]]> + + + ]]> + + $files_list @@ -916,6 +1038,13 @@ fileEncrypted[$fileId]]]> + + $folder + fileEncrypted[$fileId]]]> + + + Folder + fileIsEncrypted]]> fileIsEncrypted]]> @@ -934,6 +1063,12 @@ + + root->get('appdata_'.$instanceId)]]> + + + \OCP\Files\Folder + null null @@ -1003,7 +1138,7 @@ - self::class . '::' . $eventName + dispatch @@ -1025,6 +1160,14 @@ getUniqueStorages + + + storageClass]]> + + + ]]> + + put @@ -1083,7 +1226,7 @@ - 'OCA\\Files_External::loadAdditionalBackends' + dispatch @@ -1110,6 +1253,14 @@ $files_list + + + $mount + + + Mount + + $cacheData @@ -1198,7 +1349,7 @@ - $_['hideFileList'] !== true + @@ -1246,7 +1397,7 @@ - 'OCA\Files_Trashbin::moveToTrash' + mountPoint]]> @@ -1401,8 +1552,14 @@ getId()]]> - (int)$data['id'] + + + $nodes[0] + + + \OCP\Files\File|\OCP\Files\Folder + @@ -1495,7 +1652,7 @@ - 'OCA\\User_LDAP\\User\\User::postLDAPBackendAdded' + dispatch @@ -1802,6 +1959,16 @@ getAllAliases + + + $found + $found + + + array{shareId: int, fileTarget: string, initiator: string, receiver: string, owner: string, mountOwner: string}[] + array{shareId: int, fileTarget: string, initiator: string, receiver: string, owner: string, mountOwner: string}[] + + section @@ -1867,7 +2034,7 @@ - 'OC\AccountManager::userUpdated' + dispatch @@ -1884,6 +2051,12 @@ providerClasses]]> settingsClasses]]> + + settings]]> + + + ActivitySettings[] + @@ -2045,7 +2218,7 @@ - $action['url-postfix'] + @@ -2054,9 +2227,48 @@ + + newInstance()]]> + newInstanceArgs(array_map(function (ReflectionParameter $parameter) { + $parameterType = $parameter->getType(); + + $resolveName = $parameter->getName(); + + // try to find out if it is a class or a simple parameter + if ($parameterType !== null && ($parameterType instanceof ReflectionNamedType) && !$parameterType->isBuiltin()) { + $resolveName = $parameterType->getName(); + } + + try { + $builtIn = $parameter->hasType() && ($parameter->getType() instanceof ReflectionNamedType) + && $parameter->getType()->isBuiltin(); + return $this->query($resolveName, !$builtIn); + } catch (QueryException $e) { + // Service not found, use the default value when available + if ($parameter->isDefaultValueAvailable()) { + return $parameter->getDefaultValue(); + } + + if ($parameterType !== null && ($parameterType instanceof ReflectionNamedType) && !$parameterType->isBuiltin()) { + $resolveName = $parameter->getName(); + try { + return $this->query($resolveName); + } catch (QueryException $e2) { + // don't lose the error we got while trying to query by type + throw new QueryException($e->getMessage(), (int) $e->getCode(), $e); + } + } + + throw $e; + } + }, $constructor->getParameters()))]]> + ArrayAccess + + \stdClass + getCode()]]> @@ -2111,7 +2323,7 @@ - + $row['provider_id'], @@ -2119,10 +2331,10 @@ 'enabled' => 1 === (int) $row['enabled'], ]; }, $rows)]]> - - - int[] - + + + ]]> + @@ -2152,11 +2364,6 @@ providers]]> - - - $provider['provider_id'] - - $jobList @@ -2175,6 +2382,26 @@ + + container->get($registration->getService()); + } catch (Throwable $e) { + $this->logger->error('Could not load calendar provider ' . $registration->getService() . ': ' . $e->getMessage(), [ + 'exception' => $e, + ]); + return []; + } + + return $provider->getCalendars($principalUri, $calendarUris); + }, $context->getCalendarProviders()) + )]]> + + + ICreateFromString[] + getParams - $params['collation'] + @@ -2280,8 +2507,8 @@ $params - $params['adapter'] - $params['tablePrefix'] + + adapter->lastInsertId($seqName)]]> @@ -2307,6 +2534,12 @@ $offset $offset + + $s + + + IMigrationStep + @@ -2327,7 +2560,7 @@ getParams - $params['collation'] + @@ -2403,10 +2636,21 @@ + + manager->getFileForToken($this->data['user_id'], $this->data['file_id'], $this->data['file_path'])]]> + getShareForToken + + + $uniqueUserIds, 'public' => $public]]]> + + + array{users: string[], public: bool} + + deleteUserKey @@ -2427,6 +2671,16 @@ dispatch + + + event->setArgument($key, $value)]]> + event->setArguments($args)]]> + + + setArgument + setArguments + + $eventName @@ -2446,6 +2700,14 @@ $providerId + + + folder]]> + + + Folder + + $parentData @@ -2522,10 +2784,10 @@ $user - get_class($provider) !== 'OCA\Files_Sharing\MountProvider' + - get_class($provider) === 'OCA\Files_Sharing\MountProvider' + @@ -2536,6 +2798,12 @@ array + + cacheInfoCache[$fileId]]]> + + + array{int, string, int} + @@ -2543,6 +2811,16 @@ + + getStorage()]]> + findByNumericId($id)]]> + findByStorageId($id)]]> + + + Mount\MountPoint[] + Mount\MountPoint[] + \OC\Files\Storage\Storage|null + addStorageWrapper @@ -2569,25 +2847,37 @@ + + root->get($this->getFullPath($path))]]> + root->getByIdInPath((int)$id, $this->getPath())]]> + createNode($file->getPath(), $file); + }, $files)]]> + $node + + \OC\Files\Node\Node + \OC\Files\Node\Node[] + \OC\Files\Node\Node[] + - '\OCP\Files::postCopy' - '\OCP\Files::postCreate' - '\OCP\Files::postDelete' - '\OCP\Files::postRename' - '\OCP\Files::postTouch' - '\OCP\Files::postWrite' - '\OCP\Files::preCopy' - '\OCP\Files::preCreate' - '\OCP\Files::preDelete' - '\OCP\Files::preRename' - '\OCP\Files::preTouch' - '\OCP\Files::preWrite' - '\OCP\Files::read' + + + + + + + + + + + + + dispatch @@ -2625,9 +2915,17 @@ __call(__FUNCTION__, func_get_args())]]> + + + $node + + + Folder + + - '\OCP\Files::' . $hook + FileInfo @@ -2635,6 +2933,12 @@ getChecksum + + parent]]> + + + INode|IRootFolder + fileInfo]]> @@ -2650,9 +2954,21 @@ - + + $folders + createNode($fullPath, $fileInfo, false)]]> + mountManager->findByNumericId($numericId)]]> + mountManager->findByStorageId($storageId)]]> + mountManager->findIn($mountPoint)]]> + user]]> + + + MountPoint[] + Node + \OC\Files\Mount\MountPoint[] + \OC\Files\Mount\MountPoint[] \OC\User\User - + user]]> @@ -2682,7 +2998,7 @@ Promise\promise_for( new Credentials($key, $secret) ) - \Aws\or_chain([self::class, 'legacySignatureProvider'], ClientResolver::_default_signature_provider()) + @@ -2859,6 +3175,14 @@ int + + + $mounts + + + \OC\Files\Mount\MountPoint[] + + $mtime @@ -2891,20 +3215,26 @@ - IGroup::class . '::postAddUser' - IGroup::class . '::postDelete' - IGroup::class . '::postRemoveUser' - IGroup::class . '::preAddUser' - IGroup::class . '::preDelete' - IGroup::class . '::preRemoveUser' + + + + + + bool $hide + + $users + $user + + \OC\User\User[] + emitter]]> emitter]]> @@ -2926,6 +3256,15 @@ + + $groups + array_values($groups) + array_values($groups) + + + \OC\Group\Group[] + \OC\Group\Group[] + createGroup getGroupDetails @@ -2951,8 +3290,8 @@ false - $app['path'] - $app['path'] + + null @@ -3121,6 +3460,9 @@ false|resource + + null|string + ISimpleFile @@ -3272,6 +3614,22 @@ new GenericEvent($user) + + get(IFile::class)]]> + get(IGroupManager::class)]]> + get(INavigationManager::class)]]> + get(IUserManager::class)]]> + get(IUserSession::class)]]> + get(\OCP\Encryption\IManager::class)]]> + + + \OC\Encryption\File + \OC\Encryption\Manager + \OC\Group\Manager + \OC\NavigationManager + \OC\User\Manager + \OC\User\Session + \OC\OCSClient @@ -3306,8 +3664,8 @@ - $content !== '' - $type === 'pdo' + + $vendor @@ -3337,7 +3695,7 @@ getId()]]> getId()]]> - (int)$data['id'] + set @@ -3349,12 +3707,12 @@ $id - 'OCP\Share::postAcceptShare' - 'OCP\Share::postShare' - 'OCP\Share::postUnshare' - 'OCP\Share::postUnshareFromSelf' - 'OCP\Share::preShare' - 'OCP\Share::preUnshare' + + + + + + dispatch @@ -3413,6 +3771,14 @@ getLazyRootFolder + + + node]]> + + + getNode + + $stream @@ -3543,7 +3909,7 @@ boolean|null - IUser::class . '::firstLogin' + server]]> @@ -3555,11 +3921,11 @@ - IUser::class . '::changeUser' - IUser::class . '::postDelete' - IUser::class . '::postSetPassword' - IUser::class . '::preDelete' - IUser::class . '::preSetPassword' + + + + + dispatch @@ -3590,8 +3956,8 @@ ManagerEvent::EVENT_APP_UPDATE - $dir['path'] - $dir['url'] + + null @@ -3667,6 +4033,14 @@ $column + + + headers)]]> + + + array{X-Request-Id: string, Cache-Control: string, Content-Security-Policy: string, Feature-Policy: string, X-Robots-Tag: string, Last-Modified?: string, ETag?: string, ...H} + + EVENT_FAILED @@ -3678,6 +4052,14 @@ \ArrayAccess + + + $step + + + array{0: int, 1: int, 2: int} + + mixed @@ -3709,4 +4091,12 @@ \Iterator + + + mode]]> + + + null|IPreview::MODE_FILL|IPreview::MODE_COVER + + diff --git a/core/Controller/AppPasswordController.php b/core/Controller/AppPasswordController.php index 90020330ea19e..07bdc1e143313 100644 --- a/core/Controller/AppPasswordController.php +++ b/core/Controller/AppPasswordController.php @@ -8,6 +8,7 @@ * @author Christoph Wurst * @author Daniel Kesselberg * @author Roeland Jago Douma + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -31,6 +32,7 @@ use OC\Authentication\Exceptions\InvalidTokenException; use OC\Authentication\Token\IProvider; use OC\Authentication\Token\IToken; +use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCS\OCSForbiddenException; use OCP\Authentication\Exceptions\CredentialsUnavailableException; @@ -57,7 +59,12 @@ public function __construct( /** * @NoAdminRequired * - * @throws OCSForbiddenException + * Create app password + * + * @return DataResponse + * @throws OCSForbiddenException Creating app password is not allowed + * + * 200: App password returned */ public function getAppPassword(): DataResponse { // We do not allow the creation of new tokens if this is an app password @@ -102,6 +109,13 @@ public function getAppPassword(): DataResponse { /** * @NoAdminRequired + * + * Delete app password + * + * @return DataResponse + * @throws OCSForbiddenException Deleting app password is not allowed + * + * 200: App password deleted successfully */ public function deleteAppPassword(): DataResponse { if (!$this->session->exists('app_password')) { @@ -117,11 +131,18 @@ public function deleteAppPassword(): DataResponse { } $this->tokenProvider->invalidateTokenById($token->getUID(), $token->getId()); - return new DataResponse(); + return new DataResponse(new \stdClass()); } /** * @NoAdminRequired + * + * Rotate app password + * + * @return DataResponse + * @throws OCSForbiddenException Rotating app password is not allowed + * + * 200: App password returned */ public function rotateAppPassword(): DataResponse { if (!$this->session->exists('app_password')) { diff --git a/core/Controller/AutoCompleteController.php b/core/Controller/AutoCompleteController.php index 29a0788ad57ec..b5421e086ad27 100644 --- a/core/Controller/AutoCompleteController.php +++ b/core/Controller/AutoCompleteController.php @@ -30,6 +30,8 @@ */ namespace OC\Core\Controller; +use OCA\Core\ResponseDefinitions; +use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCSController; use OCP\Collaboration\AutoComplete\AutoCompleteEvent; @@ -39,6 +41,9 @@ use OCP\IRequest; use OCP\Share\IShare; +/** + * @psalm-import-type CoreAutocompleteResult from ResponseDefinitions + */ class AutoCompleteController extends OCSController { public function __construct( string $appName, @@ -52,7 +57,17 @@ public function __construct( /** * @NoAdminRequired + * + * Autocomplete a query + * + * @param string $search Text to search for + * @param string|null $itemType Type of the items to search for + * @param string|null $itemId ID of the items to search for * @param string|null $sorter can be piped, top prio first, e.g.: "commenters|share-recipients" + * @param int[] $shareTypes Types of shares to search for + * @param int $limit Maximum number of results to return + * + * @return DataResponse */ public function get(string $search, ?string $itemType, ?string $itemId, ?string $sorter = null, array $shareTypes = [IShare::TYPE_USER], int $limit = 10): DataResponse { // if enumeration/user listings are disabled, we'll receive an empty @@ -89,18 +104,37 @@ public function get(string $search, ?string $itemType, ?string $itemId, ?string return new DataResponse($results); } + /** + * @return CoreAutocompleteResult[] + */ protected function prepareResultArray(array $results): array { $output = []; + /** @var string $type */ foreach ($results as $type => $subResult) { foreach ($subResult as $result) { + /** @var ?string $icon */ + $icon = $result['icon']; + + /** @var string $label */ + $label = $result['label']; + + /** @var ?string $subline */ + $subline = $result['subline']; + + /** @var ?string $status */ + $status = is_string($result['status']) ? $result['status'] : null; + + /** @var ?string $shareWithDisplayNameUnique */ + $shareWithDisplayNameUnique = $result['shareWithDisplayNameUnique']; + $output[] = [ 'id' => (string) $result['value']['shareWith'], - 'label' => $result['label'], - 'icon' => $result['icon'] ?? '', + 'label' => $label, + 'icon' => $icon ?? '', 'source' => $type, - 'status' => $result['status'] ?? '', - 'subline' => $result['subline'] ?? '', - 'shareWithDisplayNameUnique' => $result['shareWithDisplayNameUnique'] ?? '', + 'status' => $status ?? '', + 'subline' => $subline ?? '', + 'shareWithDisplayNameUnique' => $shareWithDisplayNameUnique ?? '', ]; } } diff --git a/core/Controller/AvatarController.php b/core/Controller/AvatarController.php index ba1792af7089b..9a12308955bd8 100644 --- a/core/Controller/AvatarController.php +++ b/core/Controller/AvatarController.php @@ -12,6 +12,7 @@ * @author Roeland Jago Douma * @author Thomas Müller * @author Vincent Petry + * @author Kate Döen * * @license AGPL-3.0 * @@ -72,7 +73,14 @@ public function __construct( * @NoSameSiteCookieRequired * @PublicPage * - * @return JSONResponse|FileDisplayResponse + * Get the dark avatar + * + * @param string $userId ID of the user + * @param int $size Size of the avatar + * @return FileDisplayResponse|JSONResponse + * + * 200: Avatar returned + * 404: Avatar not found */ public function getAvatarDark(string $userId, int $size) { if ($size <= 64) { @@ -96,7 +104,7 @@ public function getAvatarDark(string $userId, int $size) { ['Content-Type' => $avatarFile->getMimeType(), 'X-NC-IsCustomAvatar' => (int)$avatar->isCustomAvatar()] ); } catch (\Exception $e) { - return new JSONResponse([], Http::STATUS_NOT_FOUND); + return new JSONResponse(new \stdClass(), Http::STATUS_NOT_FOUND); } // Cache for 1 day @@ -111,7 +119,14 @@ public function getAvatarDark(string $userId, int $size) { * @NoSameSiteCookieRequired * @PublicPage * - * @return JSONResponse|FileDisplayResponse + * Get the avatar + * + * @param string $userId ID of the user + * @param int $size Size of the avatar + * @return FileDisplayResponse|JSONResponse + * + * 200: Avatar returned + * 404: Avatar not found */ public function getAvatar(string $userId, int $size) { if ($size <= 64) { @@ -135,7 +150,7 @@ public function getAvatar(string $userId, int $size) { ['Content-Type' => $avatarFile->getMimeType(), 'X-NC-IsCustomAvatar' => (int)$avatar->isCustomAvatar()] ); } catch (\Exception $e) { - return new JSONResponse([], Http::STATUS_NOT_FOUND); + return new JSONResponse(new \stdClass(), Http::STATUS_NOT_FOUND); } // Cache for 1 day diff --git a/core/Controller/CSRFTokenController.php b/core/Controller/CSRFTokenController.php index 95e67371b5d7e..d34aaf5e7cd6d 100644 --- a/core/Controller/CSRFTokenController.php +++ b/core/Controller/CSRFTokenController.php @@ -7,6 +7,7 @@ * * @author Christoph Wurst * @author Roeland Jago Douma + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -45,6 +46,7 @@ public function __construct( * @NoAdminRequired * @NoCSRFRequired * @PublicPage + * @IgnoreAPI */ public function index(): JSONResponse { if (!$this->request->passesStrictCookieCheck()) { diff --git a/core/Controller/ClientFlowLoginController.php b/core/Controller/ClientFlowLoginController.php index 082d5b3f92ebb..df9fe510f973a 100644 --- a/core/Controller/ClientFlowLoginController.php +++ b/core/Controller/ClientFlowLoginController.php @@ -12,6 +12,7 @@ * @author Roeland Jago Douma * @author RussellAult * @author Sergej Nikolaev + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -106,6 +107,7 @@ private function stateTokenForbiddenResponse(): StandaloneTemplateResponse { /** * @PublicPage * @NoCSRFRequired + * @IgnoreAPI */ #[UseSession] public function showAuthPickerPage(string $clientIdentifier = '', string $user = '', int $direct = 0): StandaloneTemplateResponse { @@ -173,6 +175,7 @@ public function showAuthPickerPage(string $clientIdentifier = '', string $user = * @NoAdminRequired * @NoCSRFRequired * @NoSameSiteCookieRequired + * @IgnoreAPI */ #[UseSession] public function grantPage(string $stateToken = '', diff --git a/core/Controller/ClientFlowLoginV2Controller.php b/core/Controller/ClientFlowLoginV2Controller.php index 8a21148f5895f..98b93f2158f12 100644 --- a/core/Controller/ClientFlowLoginV2Controller.php +++ b/core/Controller/ClientFlowLoginV2Controller.php @@ -31,6 +31,7 @@ use OC\Core\Db\LoginFlowV2; use OC\Core\Exception\LoginFlowV2NotFoundException; use OC\Core\Service\LoginFlowV2Service; +use OCA\Core\ResponseDefinitions; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\UseSession; @@ -47,6 +48,10 @@ use OCP\IUserSession; use OCP\Security\ISecureRandom; +/** + * @psalm-import-type CoreLoginFlowV2Credentials from ResponseDefinitions + * @psalm-import-type CoreLoginFlowV2 from ResponseDefinitions + */ class ClientFlowLoginV2Controller extends Controller { public const TOKEN_NAME = 'client.flow.v2.login.token'; public const STATE_NAME = 'client.flow.v2.state.token'; @@ -69,20 +74,29 @@ public function __construct( /** * @NoCSRFRequired * @PublicPage + * + * Poll the login flow credentials + * + * @param string $token Token of the flow + * @return JSONResponse|JSONResponse + * + * 200: Login flow credentials returned + * 404: Login flow not found or completed */ public function poll(string $token): JSONResponse { try { $creds = $this->loginFlowV2Service->poll($token); } catch (LoginFlowV2NotFoundException $e) { - return new JSONResponse([], Http::STATUS_NOT_FOUND); + return new JSONResponse(new \stdClass(), Http::STATUS_NOT_FOUND); } - return new JSONResponse($creds); + return new JSONResponse($creds->jsonSerialize()); } /** * @NoCSRFRequired * @PublicPage + * @IgnoreAPI */ #[UseSession] public function landing(string $token, $user = ''): Response { @@ -100,6 +114,7 @@ public function landing(string $token, $user = ''): Response { /** * @NoCSRFRequired * @PublicPage + * @IgnoreAPI */ #[UseSession] public function showAuthPickerPage($user = ''): StandaloneTemplateResponse { @@ -133,6 +148,7 @@ public function showAuthPickerPage($user = ''): StandaloneTemplateResponse { * @NoAdminRequired * @NoCSRFRequired * @NoSameSiteCookieRequired + * @IgnoreAPI */ #[UseSession] public function grantPage(?string $stateToken): StandaloneTemplateResponse { @@ -267,6 +283,10 @@ private function handleFlowDone(bool $result): StandaloneTemplateResponse { /** * @NoCSRFRequired * @PublicPage + * + * Init a login flow + * + * @return JSONResponse */ public function init(): JSONResponse { // Get client user agent diff --git a/core/Controller/CollaborationResourcesController.php b/core/Controller/CollaborationResourcesController.php index da90f27c9efa0..2733a0af4bcce 100644 --- a/core/Controller/CollaborationResourcesController.php +++ b/core/Controller/CollaborationResourcesController.php @@ -8,6 +8,7 @@ * @author Joas Schilling * @author Julius Härtl * @author Roeland Jago Douma + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -29,6 +30,7 @@ namespace OC\Core\Controller; use Exception; +use OCA\Core\ResponseDefinitions; use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCSController; @@ -41,6 +43,10 @@ use OCP\IUserSession; use Psr\Log\LoggerInterface; +/** + * @psalm-import-type CoreOpenGraphObject from ResponseDefinitions + * @psalm-import-type CoreCollection from ResponseDefinitions + */ class CollaborationResourcesController extends OCSController { public function __construct( string $appName, @@ -70,14 +76,19 @@ protected function getCollection(int $collectionId): ICollection { /** * @NoAdminRequired * - * @param int $collectionId - * @return DataResponse + * Get a collection + * + * @param int $collectionId ID of the collection + * @return DataResponse|DataResponse + * + * 200: Collection returned + * 404: Collection not found */ public function listCollection(int $collectionId): DataResponse { try { $collection = $this->getCollection($collectionId); } catch (CollectionException $e) { - return new DataResponse([], Http::STATUS_NOT_FOUND); + return new DataResponse(new \stdClass(), Http::STATUS_NOT_FOUND); } return $this->respondCollection($collection); @@ -86,14 +97,19 @@ public function listCollection(int $collectionId): DataResponse { /** * @NoAdminRequired * - * @param string $filter - * @return DataResponse + * Search for collections + * + * @param string $filter Filter collections + * @return DataResponse|DataResponse + * + * 200: Collections returned + * 404: Collection not found */ public function searchCollections(string $filter): DataResponse { try { $collections = $this->manager->searchCollections($this->userSession->getUser(), $filter); } catch (CollectionException $e) { - return new DataResponse([], Http::STATUS_NOT_FOUND); + return new DataResponse(new \stdClass(), Http::STATUS_NOT_FOUND); } return new DataResponse($this->prepareCollections($collections)); @@ -102,22 +118,27 @@ public function searchCollections(string $filter): DataResponse { /** * @NoAdminRequired * - * @param int $collectionId - * @param string $resourceType - * @param string $resourceId - * @return DataResponse + * Add a resource to a collection + * + * @param int $collectionId ID of the collection + * @param string $resourceType Name of the resource + * @param string $resourceId ID of the resource + * @return DataResponse|DataResponse + * + * 200: Collection returned + * 404: Collection not found or resource inaccessible */ public function addResource(int $collectionId, string $resourceType, string $resourceId): DataResponse { try { $collection = $this->getCollection($collectionId); } catch (CollectionException $e) { - return new DataResponse([], Http::STATUS_NOT_FOUND); + return new DataResponse(new \stdClass(), Http::STATUS_NOT_FOUND); } $resource = $this->manager->createResource($resourceType, $resourceId); if (!$resource->canAccess($this->userSession->getUser())) { - return new DataResponse([], Http::STATUS_NOT_FOUND); + return new DataResponse(new \stdClass(), Http::STATUS_NOT_FOUND); } try { @@ -131,22 +152,27 @@ public function addResource(int $collectionId, string $resourceType, string $res /** * @NoAdminRequired * - * @param int $collectionId - * @param string $resourceType - * @param string $resourceId - * @return DataResponse + * Remove a resource from a collection + * + * @param int $collectionId ID of the collection + * @param string $resourceType Name of the resource + * @param string $resourceId ID of the resource + * @return DataResponse|DataResponse + * + * 200: Collection returned + * 404: Collection or resource not found */ public function removeResource(int $collectionId, string $resourceType, string $resourceId): DataResponse { try { $collection = $this->getCollection($collectionId); } catch (CollectionException $e) { - return new DataResponse([], Http::STATUS_NOT_FOUND); + return new DataResponse(new \stdClass(), Http::STATUS_NOT_FOUND); } try { $resource = $this->manager->getResourceForUser($resourceType, $resourceId, $this->userSession->getUser()); } catch (CollectionException $e) { - return new DataResponse([], Http::STATUS_NOT_FOUND); + return new DataResponse(new \stdClass(), Http::STATUS_NOT_FOUND); } $collection->removeResource($resource); @@ -157,9 +183,14 @@ public function removeResource(int $collectionId, string $resourceType, string $ /** * @NoAdminRequired * - * @param string $resourceType - * @param string $resourceId - * @return DataResponse + * Get collections by resource + * + * @param string $resourceType Type of the resource + * @param string $resourceId ID of the resource + * @return DataResponse|DataResponse + * + * 200: Collections returned + * 404: Resource not accessible */ public function getCollectionsByResource(string $resourceType, string $resourceId): DataResponse { try { @@ -169,7 +200,7 @@ public function getCollectionsByResource(string $resourceType, string $resourceI } if (!$resource->canAccess($this->userSession->getUser())) { - return new DataResponse([], Http::STATUS_NOT_FOUND); + return new DataResponse(new \stdClass(), Http::STATUS_NOT_FOUND); } return new DataResponse($this->prepareCollections($resource->getCollections())); @@ -178,24 +209,30 @@ public function getCollectionsByResource(string $resourceType, string $resourceI /** * @NoAdminRequired * - * @param string $baseResourceType - * @param string $baseResourceId - * @param string $name - * @return DataResponse + * Create a collection for a resource + * + * @param string $baseResourceType Type of the base resource + * @param string $baseResourceId ID of the base resource + * @param string $name Name of the collection + * @return DataResponse|DataResponse + * + * 200: Collection returned + * 400: Creating collection is not possible + * 404: Resource inaccessible */ public function createCollectionOnResource(string $baseResourceType, string $baseResourceId, string $name): DataResponse { if (!isset($name[0]) || isset($name[64])) { - return new DataResponse([], Http::STATUS_BAD_REQUEST); + return new DataResponse(new \stdClass(), Http::STATUS_BAD_REQUEST); } try { $resource = $this->manager->createResource($baseResourceType, $baseResourceId); } catch (CollectionException $e) { - return new DataResponse([], Http::STATUS_NOT_FOUND); + return new DataResponse(new \stdClass(), Http::STATUS_NOT_FOUND); } if (!$resource->canAccess($this->userSession->getUser())) { - return new DataResponse([], Http::STATUS_NOT_FOUND); + return new DataResponse(new \stdClass(), Http::STATUS_NOT_FOUND); } $collection = $this->manager->newCollection($name); @@ -207,15 +244,20 @@ public function createCollectionOnResource(string $baseResourceType, string $bas /** * @NoAdminRequired * - * @param int $collectionId - * @param string $collectionName - * @return DataResponse + * Rename a collection + * + * @param int $collectionId ID of the collection + * @param string $collectionName New name + * @return DataResponse|DataResponse + * + * 200: Collection returned + * 404: Collection not found */ public function renameCollection(int $collectionId, string $collectionName): DataResponse { try { $collection = $this->getCollection($collectionId); } catch (CollectionException $exception) { - return new DataResponse([], Http::STATUS_NOT_FOUND); + return new DataResponse(new \stdClass(), Http::STATUS_NOT_FOUND); } $collection->setName($collectionName); @@ -223,17 +265,23 @@ public function renameCollection(int $collectionId, string $collectionName): Dat return $this->respondCollection($collection); } + /** + * @return DataResponse|DataResponse + */ protected function respondCollection(ICollection $collection): DataResponse { try { return new DataResponse($this->prepareCollection($collection)); } catch (CollectionException $e) { - return new DataResponse([], Http::STATUS_NOT_FOUND); + return new DataResponse(new \stdClass(), Http::STATUS_NOT_FOUND); } catch (Exception $e) { $this->logger->critical($e->getMessage(), ['exception' => $e, 'app' => 'core']); - return new DataResponse([], Http::STATUS_INTERNAL_SERVER_ERROR); + return new DataResponse(new \stdClass(), Http::STATUS_INTERNAL_SERVER_ERROR); } } + /** + * @return CoreCollection[] + */ protected function prepareCollections(array $collections): array { $result = []; @@ -249,6 +297,9 @@ protected function prepareCollections(array $collections): array { return $result; } + /** + * @return CoreCollection + */ protected function prepareCollection(ICollection $collection): array { if (!$collection->canAccess($this->userSession->getUser())) { throw new CollectionException('Can not access collection'); @@ -261,7 +312,10 @@ protected function prepareCollection(ICollection $collection): array { ]; } - protected function prepareResources(array $resources): ?array { + /** + * @return CoreOpenGraphObject[] + */ + protected function prepareResources(array $resources): array { $result = []; foreach ($resources as $resource) { @@ -276,6 +330,9 @@ protected function prepareResources(array $resources): ?array { return $result; } + /** + * @return CoreOpenGraphObject + */ protected function prepareResource(IResource $resource): array { if (!$resource->canAccess($this->userSession->getUser())) { throw new ResourceException('Can not access resource'); diff --git a/core/Controller/CssController.php b/core/Controller/CssController.php index 7aec5850aea5a..0897b991a28de 100644 --- a/core/Controller/CssController.php +++ b/core/Controller/CssController.php @@ -11,6 +11,7 @@ * @author Morris Jobke * @author Roeland Jago Douma * @author Thomas Citharel + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -61,6 +62,7 @@ public function __construct( * @PublicPage * @NoCSRFRequired * @NoSameSiteCookieRequired + * @IgnoreAPI * * @param string $fileName css filename with extension * @param string $appName css folder name diff --git a/core/Controller/ErrorController.php b/core/Controller/ErrorController.php index 550b320a98940..5f003606676d5 100644 --- a/core/Controller/ErrorController.php +++ b/core/Controller/ErrorController.php @@ -6,6 +6,7 @@ * @copyright Copyright (c) 2022 Julius Härtl * * @author Julius Härtl + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -33,6 +34,7 @@ class ErrorController extends \OCP\AppFramework\Controller { /** * @PublicPage * @NoCSRFRequired + * @IgnoreAPI */ public function error403(): TemplateResponse { $response = new TemplateResponse( @@ -48,6 +50,7 @@ public function error403(): TemplateResponse { /** * @PublicPage * @NoCSRFRequired + * @IgnoreAPI */ public function error404(): TemplateResponse { $response = new TemplateResponse( diff --git a/core/Controller/GuestAvatarController.php b/core/Controller/GuestAvatarController.php index 6f06451b796a7..3270a1f7f5aae 100644 --- a/core/Controller/GuestAvatarController.php +++ b/core/Controller/GuestAvatarController.php @@ -3,6 +3,7 @@ * @copyright Copyright (c) 2019, Michael Weimann * * @author Michael Weimann + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -25,6 +26,7 @@ use OCP\AppFramework\Controller; use OCP\AppFramework\Http; use OCP\AppFramework\Http\FileDisplayResponse; +use OCP\AppFramework\Http\Response; use OCP\IAvatarManager; use OCP\IRequest; use Psr\Log\LoggerInterface; @@ -46,14 +48,18 @@ public function __construct( } /** - * Returns a guest avatar image response. + * Returns a guest avatar image response * * @PublicPage * @NoCSRFRequired * * @param string $guestName The guest name, e.g. "Albert" * @param string $size The desired avatar size, e.g. 64 for 64x64px - * @return FileDisplayResponse|Http\Response + * @param bool|null $darkTheme Return dark avatar + * @return FileDisplayResponse|Response + * + * 200: Custom avatar returned + * 201: Avatar returned */ public function getAvatar(string $guestName, string $size, ?bool $darkTheme = false) { $size = (int) $size; @@ -95,8 +101,17 @@ public function getAvatar(string $guestName, string $size, ?bool $darkTheme = fa } /** + * Returns a dark guest avatar image response + * * @PublicPage * @NoCSRFRequired + * + * @param string $guestName The guest name, e.g. "Albert" + * @param string $size The desired avatar size, e.g. 64 for 64x64px + * @return FileDisplayResponse|Response + * + * 200: Custom avatar returned + * 201: Avatar returned */ public function getAvatarDark(string $guestName, string $size) { return $this->getAvatar($guestName, $size, true); diff --git a/core/Controller/HoverCardController.php b/core/Controller/HoverCardController.php index cfe95be043120..8485523d34930 100644 --- a/core/Controller/HoverCardController.php +++ b/core/Controller/HoverCardController.php @@ -5,6 +5,7 @@ * @copyright 2021 Joas Schilling * * @author Joas Schilling + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -25,6 +26,7 @@ namespace OC\Core\Controller; use OC\Contacts\ContactsMenu\Manager; +use OCA\Core\ResponseDefinitions; use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\Contacts\ContactsMenu\IEntry; @@ -32,6 +34,9 @@ use OCP\IUserSession; use OCP\Share\IShare; +/** + * @psalm-import-type CoreContactsAction from ResponseDefinitions + */ class HoverCardController extends \OCP\AppFramework\OCSController { public function __construct( IRequest $request, @@ -43,29 +48,34 @@ public function __construct( /** * @NoAdminRequired + * + * Get the user details for a hovercard + * + * @param string $userId ID of the user + * @return DataResponse|DataResponse + * + * 200: User details returned + * 404: User not found */ public function getUser(string $userId): DataResponse { $contact = $this->manager->findOne($this->userSession->getUser(), IShare::TYPE_USER, $userId); if (!$contact) { - return new DataResponse([], Http::STATUS_NOT_FOUND); + return new DataResponse(new \stdClass(), Http::STATUS_NOT_FOUND); } - $data = $this->entryToArray($contact); + $data = $contact->jsonSerialize(); $actions = $data['actions']; if ($data['topAction']) { array_unshift($actions, $data['topAction']); } + /** @var CoreContactsAction[] $actions */ return new DataResponse([ 'userId' => $userId, 'displayName' => $contact->getFullName(), 'actions' => $actions, ]); } - - protected function entryToArray(IEntry $entry): array { - return json_decode(json_encode($entry), true); - } } diff --git a/core/Controller/JsController.php b/core/Controller/JsController.php index 0ad78d5f87f0c..ed71d4656cb28 100644 --- a/core/Controller/JsController.php +++ b/core/Controller/JsController.php @@ -11,6 +11,7 @@ * @author Morris Jobke * @author Roeland Jago Douma * @author Thomas Citharel + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -61,6 +62,7 @@ public function __construct( * @PublicPage * @NoCSRFRequired * @NoSameSiteCookieRequired + * @IgnoreAPI * * @param string $fileName js filename with extension * @param string $appName js folder name diff --git a/core/Controller/LoginController.php b/core/Controller/LoginController.php index 1bee366b00fbe..51a0188977e9c 100644 --- a/core/Controller/LoginController.php +++ b/core/Controller/LoginController.php @@ -16,6 +16,7 @@ * @author Michael Weimann * @author Rayn0r * @author Roeland Jago Douma + * @author Kate Döen * * @license AGPL-3.0 * @@ -112,6 +113,7 @@ public function logout() { /** * @PublicPage * @NoCSRFRequired + * @IgnoreAPI * * @param string $user * @param string $redirect_url @@ -263,6 +265,7 @@ private function generateRedirect(?string $redirectUrl): RedirectResponse { * @PublicPage * @NoCSRFRequired * @BruteForceProtection(action=login) + * @IgnoreAPI * * @return RedirectResponse */ diff --git a/core/Controller/LostController.php b/core/Controller/LostController.php index 7de93b7107a4a..4fc302027d25b 100644 --- a/core/Controller/LostController.php +++ b/core/Controller/LostController.php @@ -17,6 +17,7 @@ * @author Roeland Jago Douma * @author Thomas Müller * @author Victor Dubiniuk + * @author Kate Döen * * @license AGPL-3.0 * @@ -104,6 +105,7 @@ public function __construct( * @NoCSRFRequired * @BruteForceProtection(action=passwordResetEmail) * @AnonRateThrottle(limit=10, period=300) + * @IgnoreAPI */ public function resetform(string $token, string $userId): TemplateResponse { try { diff --git a/core/Controller/NavigationController.php b/core/Controller/NavigationController.php index 62c4dd9875674..875ab01d545fa 100644 --- a/core/Controller/NavigationController.php +++ b/core/Controller/NavigationController.php @@ -3,6 +3,7 @@ * @copyright Copyright (c) 2018 Julius Härtl * * @author Julius Härtl + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -22,6 +23,7 @@ */ namespace OC\Core\Controller; +use OCA\Core\ResponseDefinitions; use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCSController; @@ -29,6 +31,9 @@ use OCP\IRequest; use OCP\IURLGenerator; +/** + * @psalm-import-type CoreNavigationEntry from ResponseDefinitions + */ class NavigationController extends OCSController { public function __construct( string $appName, @@ -42,6 +47,14 @@ public function __construct( /** * @NoAdminRequired * @NoCSRFRequired + * + * Get the apps navigation + * + * @param bool $absolute Rewrite URLs to absolute ones + * @return DataResponse|DataResponse + * + * 200: Apps navigation returned + * 304: No apps navigation changed */ public function getAppsNavigation(bool $absolute = false): DataResponse { $navigation = $this->navigationManager->getAll(); @@ -51,7 +64,7 @@ public function getAppsNavigation(bool $absolute = false): DataResponse { $navigation = array_values($navigation); $etag = $this->generateETag($navigation); if ($this->request->getHeader('If-None-Match') === $etag) { - return new DataResponse([], Http::STATUS_NOT_MODIFIED); + return new DataResponse(new \stdClass(), Http::STATUS_NOT_MODIFIED); } $response = new DataResponse($navigation); $response->setETag($etag); @@ -61,6 +74,14 @@ public function getAppsNavigation(bool $absolute = false): DataResponse { /** * @NoAdminRequired * @NoCSRFRequired + * + * Get the settings navigation + * + * @param bool $absolute Rewrite URLs to absolute ones + * @return DataResponse|DataResponse + * + * 200: Apps navigation returned + * 304: No apps navigation changed */ public function getSettingsNavigation(bool $absolute = false): DataResponse { $navigation = $this->navigationManager->getAll('settings'); @@ -70,7 +91,7 @@ public function getSettingsNavigation(bool $absolute = false): DataResponse { $navigation = array_values($navigation); $etag = $this->generateETag($navigation); if ($this->request->getHeader('If-None-Match') === $etag) { - return new DataResponse([], Http::STATUS_NOT_MODIFIED); + return new DataResponse(new \stdClass(), Http::STATUS_NOT_MODIFIED); } $response = new DataResponse($navigation); $response->setETag($etag); diff --git a/core/Controller/OCJSController.php b/core/Controller/OCJSController.php index 48bd842e6e25e..561f96dd51b6e 100644 --- a/core/Controller/OCJSController.php +++ b/core/Controller/OCJSController.php @@ -8,6 +8,7 @@ * @author Lukas Reschke * @author Morris Jobke * @author Roeland Jago Douma + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -83,6 +84,7 @@ public function __construct( * @NoCSRFRequired * @NoTwoFactorRequired * @PublicPage + * @IgnoreAPI */ public function getConfig(): DataDisplayResponse { $data = $this->helper->getConfig(); diff --git a/core/Controller/OCSController.php b/core/Controller/OCSController.php index 036cdfc2d60f8..8620303b7ff8c 100644 --- a/core/Controller/OCSController.php +++ b/core/Controller/OCSController.php @@ -8,6 +8,7 @@ * @author Julius Härtl * @author Lukas Reschke * @author Roeland Jago Douma + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -29,6 +30,7 @@ use OC\CapabilitiesManager; use OC\Security\IdentityProof\Manager; +use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\IRequest; use OCP\IUserManager; @@ -48,6 +50,7 @@ public function __construct( /** * @PublicPage + * @IgnoreAPI */ public function getConfig(): DataResponse { $data = [ @@ -63,14 +66,18 @@ public function getConfig(): DataResponse { /** * @PublicPage + * + * Get the capabilities + * + * @return DataResponse}, array{}> */ public function getCapabilities(): DataResponse { $result = []; [$major, $minor, $micro] = \OCP\Util::getVersion(); $result['version'] = [ - 'major' => $major, - 'minor' => $minor, - 'micro' => $micro, + 'major' => (int)$major, + 'minor' => (int)$minor, + 'micro' => (int)$micro, 'string' => \OC_Util::getVersionString(), 'edition' => '', 'extendedSupport' => \OCP\Util::hasExtendedSupport() @@ -90,6 +97,7 @@ public function getCapabilities(): DataResponse { /** * @PublicPage * @BruteForceProtection(action=login) + * @IgnoreAPI */ public function personCheck(string $login = '', string $password = ''): DataResponse { if ($login !== '' && $password !== '') { @@ -110,6 +118,7 @@ public function personCheck(string $login = '', string $password = ''): DataResp /** * @PublicPage + * @IgnoreAPI */ public function getIdentityProof(string $cloudId): DataResponse { $userObject = $this->userManager->get($cloudId); diff --git a/core/Controller/PreviewController.php b/core/Controller/PreviewController.php index 38373e2d14776..ac9f1040f9477 100644 --- a/core/Controller/PreviewController.php +++ b/core/Controller/PreviewController.php @@ -54,7 +54,20 @@ public function __construct( * @NoAdminRequired * @NoCSRFRequired * - * @return DataResponse|FileDisplayResponse + * Get a preview by file path + * + * @param string $file Path of the file + * @param int $x Width of the preview + * @param int $y Height of the preview + * @param bool $a Whether to crop the preview + * @param bool $forceIcon Force returning an icon + * @param string $mode How to crop the image + * @return FileDisplayResponse|DataResponse + * + * 200: Preview returned + * 400: Getting preview is not possible + * 403: Getting preview is not allowed + * 404: Preview not found */ public function getPreview( string $file = '', @@ -64,14 +77,14 @@ public function getPreview( bool $forceIcon = true, string $mode = 'fill'): Http\Response { if ($file === '' || $x === 0 || $y === 0) { - return new DataResponse([], Http::STATUS_BAD_REQUEST); + return new DataResponse(new \stdClass(), Http::STATUS_BAD_REQUEST); } try { $userFolder = $this->root->getUserFolder($this->userId); $node = $userFolder->get($file); } catch (NotFoundException $e) { - return new DataResponse([], Http::STATUS_NOT_FOUND); + return new DataResponse(new \stdClass(), Http::STATUS_NOT_FOUND); } return $this->fetchPreview($node, $x, $y, $a, $forceIcon, $mode); @@ -81,7 +94,20 @@ public function getPreview( * @NoAdminRequired * @NoCSRFRequired * - * @return DataResponse|FileDisplayResponse + * Get a preview by file ID + * + * @param int $fileId ID of the file + * @param int $x Width of the preview + * @param int $y Height of the preview + * @param bool $a Whether to crop the preview + * @param bool $forceIcon Force returning an icon + * @param string $mode How to crop the image + * @return FileDisplayResponse|DataResponse + * + * 200: Preview returned + * 400: Getting preview is not possible + * 403: Getting preview is not allowed + * 404: Preview not found */ public function getPreviewByFileId( int $fileId = -1, @@ -91,14 +117,14 @@ public function getPreviewByFileId( bool $forceIcon = true, string $mode = 'fill') { if ($fileId === -1 || $x === 0 || $y === 0) { - return new DataResponse([], Http::STATUS_BAD_REQUEST); + return new DataResponse(new \stdClass(), Http::STATUS_BAD_REQUEST); } $userFolder = $this->root->getUserFolder($this->userId); $nodes = $userFolder->getById($fileId); if (\count($nodes) === 0) { - return new DataResponse([], Http::STATUS_NOT_FOUND); + return new DataResponse(new \stdClass(), Http::STATUS_NOT_FOUND); } $node = array_pop($nodes); @@ -107,7 +133,7 @@ public function getPreviewByFileId( } /** - * @return DataResponse|FileDisplayResponse + * @return FileDisplayResponse|DataResponse */ private function fetchPreview( Node $node, @@ -117,10 +143,10 @@ private function fetchPreview( bool $forceIcon, string $mode) : Http\Response { if (!($node instanceof File) || (!$forceIcon && !$this->preview->isAvailable($node))) { - return new DataResponse([], Http::STATUS_NOT_FOUND); + return new DataResponse(new \stdClass(), Http::STATUS_NOT_FOUND); } if (!$node->isReadable()) { - return new DataResponse([], Http::STATUS_FORBIDDEN); + return new DataResponse(new \stdClass(), Http::STATUS_FORBIDDEN); } $storage = $node->getStorage(); @@ -129,7 +155,7 @@ private function fetchPreview( $share = $storage->getShare(); $attributes = $share->getAttributes(); if ($attributes !== null && $attributes->getAttribute('permissions', 'download') === false) { - return new DataResponse([], Http::STATUS_FORBIDDEN); + return new DataResponse(new \stdClass(), Http::STATUS_FORBIDDEN); } } @@ -141,9 +167,9 @@ private function fetchPreview( $response->cacheFor(3600 * 24, false, true); return $response; } catch (NotFoundException $e) { - return new DataResponse([], Http::STATUS_NOT_FOUND); + return new DataResponse(new \stdClass(), Http::STATUS_NOT_FOUND); } catch (\InvalidArgumentException $e) { - return new DataResponse([], Http::STATUS_BAD_REQUEST); + return new DataResponse(new \stdClass(), Http::STATUS_BAD_REQUEST); } } } diff --git a/core/Controller/ProfileApiController.php b/core/Controller/ProfileApiController.php index e66d4f21c2bf6..f65cf724eb243 100644 --- a/core/Controller/ProfileApiController.php +++ b/core/Controller/ProfileApiController.php @@ -6,6 +6,7 @@ * @copyright 2021 Christopher Ng * * @author Christopher Ng + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -27,6 +28,7 @@ namespace OC\Core\Controller; use OC\Core\Db\ProfileConfigMapper; +use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCS\OCSBadRequestException; use OCP\AppFramework\OCS\OCSForbiddenException; @@ -53,6 +55,18 @@ public function __construct( * @NoSubAdminRequired * @PasswordConfirmationRequired * @UserRateThrottle(limit=40, period=600) + * + * Update the visiblity of a parameter + * + * @param string $targetUserId ID of the user + * @param string $paramId ID of the parameter + * @param string $visibility New visibility + * @return DataResponse + * @throws OCSBadRequestException Updating visibility is not possible + * @throws OCSForbiddenException Not allowed to edit other users visibility + * @throws OCSNotFoundException User not found + * + * 200: Visibility updated successfully */ public function setVisibility(string $targetUserId, string $paramId, string $visibility): DataResponse { $requestingUser = $this->userSession->getUser(); @@ -77,6 +91,6 @@ public function setVisibility(string $targetUserId, string $paramId, string $vis $config->setVisibility($paramId, $visibility); $this->configMapper->update($config); - return new DataResponse(); + return new DataResponse(new \stdClass()); } } diff --git a/core/Controller/ProfilePageController.php b/core/Controller/ProfilePageController.php index 23dbf104b6b1a..9b12f77fc6505 100644 --- a/core/Controller/ProfilePageController.php +++ b/core/Controller/ProfilePageController.php @@ -6,6 +6,7 @@ * @copyright 2021 Christopher Ng * * @author Christopher Ng + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -59,6 +60,7 @@ public function __construct( * @NoCSRFRequired * @NoAdminRequired * @NoSubAdminRequired + * @IgnoreAPI */ public function index(string $targetUserId): TemplateResponse { $profileNotFoundTemplate = new TemplateResponse( diff --git a/core/Controller/RecommendedAppsController.php b/core/Controller/RecommendedAppsController.php index 5765b946613ca..7a59d75bae09e 100644 --- a/core/Controller/RecommendedAppsController.php +++ b/core/Controller/RecommendedAppsController.php @@ -6,6 +6,7 @@ * @copyright 2019 Christoph Wurst * * @author Christoph Wurst + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -43,6 +44,7 @@ public function __construct( /** * @NoCSRFRequired + * @IgnoreAPI * @return Response */ public function index(): Response { diff --git a/core/Controller/ReferenceApiController.php b/core/Controller/ReferenceApiController.php index 3df2e41c2a9fb..c16ca348d8889 100644 --- a/core/Controller/ReferenceApiController.php +++ b/core/Controller/ReferenceApiController.php @@ -5,6 +5,7 @@ * @copyright Copyright (c) 2022 Julius Härtl * * @author Julius Härtl + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -24,11 +25,18 @@ namespace OC\Core\Controller; +use OCA\Core\ResponseDefinitions; +use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\Collaboration\Reference\IDiscoverableReferenceProvider; use OCP\Collaboration\Reference\IReferenceManager; +use OCP\Collaboration\Reference\Reference; use OCP\IRequest; +/** + * @psalm-import-type CoreReference from ResponseDefinitions + * @psalm-import-type CoreReferenceProvider from ResponseDefinitions + */ class ReferenceApiController extends \OCP\AppFramework\OCSController { public function __construct( string $appName, @@ -41,6 +49,13 @@ public function __construct( /** * @NoAdminRequired + * + * Extract references from a text + * + * @param string $text Text to extract from + * @param bool $resolve Resolve the references + * @param int $limit Maximum amount of references to extract + * @return DataResponse}, array{}> */ public function extract(string $text, bool $resolve = false, int $limit = 1): DataResponse { $references = $this->referenceManager->extractReferences($text); @@ -52,7 +67,7 @@ public function extract(string $text, bool $resolve = false, int $limit = 1): Da break; } - $result[$reference] = $resolve ? $this->referenceManager->resolveReference($reference) : null; + $result[$reference] = $resolve ? $this->referenceManager->resolveReference($reference)->jsonSerialize() : null; } return new DataResponse([ @@ -62,11 +77,17 @@ public function extract(string $text, bool $resolve = false, int $limit = 1): Da /** * @NoAdminRequired + * + * Resolve a reference + * + * @param string $reference Reference to resolve + * @return DataResponse}, array{}> */ public function resolveOne(string $reference): DataResponse { - $resolvedReference = $this->referenceManager->resolveReference(trim($reference)); + /** @var ?CoreReference $resolvedReference */ + $resolvedReference = $this->referenceManager->resolveReference(trim($reference))?->jsonSerialize(); - $response = new DataResponse(['references' => [ $reference => $resolvedReference ]]); + $response = new DataResponse(['references' => [$reference => $resolvedReference]]); $response->cacheFor(3600, false, true); return $response; } @@ -74,7 +95,11 @@ public function resolveOne(string $reference): DataResponse { /** * @NoAdminRequired * - * @param string[] $references + * Resolve multiple references + * + * @param string[] $references References to resolve + * @param int $limit Maximum amount of references to resolve + * @return DataResponse}, array{}> */ public function resolve(array $references, int $limit = 1): DataResponse { $result = []; @@ -84,16 +109,20 @@ public function resolve(array $references, int $limit = 1): DataResponse { break; } - $result[$reference] = $this->referenceManager->resolveReference($reference); + $result[$reference] = $this->referenceManager->resolveReference($reference)?->jsonSerialize(); } return new DataResponse([ - 'references' => array_filter($result) + 'references' => $result ]); } /** * @NoAdminRequired + * + * Get the providers + * + * @return DataResponse */ public function getProvidersInfo(): DataResponse { $providers = $this->referenceManager->getDiscoverableProviders(); @@ -105,6 +134,12 @@ public function getProvidersInfo(): DataResponse { /** * @NoAdminRequired + * + * Touch a provider + * + * @param string $providerId ID of the provider + * @param int|null $timestamp Timestamp of the last usage + * @return DataResponse */ public function touchProvider(string $providerId, ?int $timestamp = null): DataResponse { if ($this->userId !== null) { diff --git a/core/Controller/ReferenceController.php b/core/Controller/ReferenceController.php index a8a489aeeabc1..a76598c0d8e64 100644 --- a/core/Controller/ReferenceController.php +++ b/core/Controller/ReferenceController.php @@ -5,6 +5,7 @@ * @copyright Copyright (c) 2022 Julius Härtl * * @author Julius Härtl + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -24,7 +25,6 @@ namespace OC\Core\Controller; -use OCP\AppFramework\Http\Response; use OCP\Collaboration\Reference\IReferenceManager; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; @@ -48,10 +48,16 @@ public function __construct( /** * @PublicPage * @NoCSRFRequired + * + * Get a preview for a reference + * * @param string $referenceId the reference cache key - * @return Response + * @return DataDownloadResponse|DataResponse + * + * 200: Preview returned + * 404: Reference not found */ - public function preview(string $referenceId): Response { + public function preview(string $referenceId) { $reference = $this->referenceManager->getReferenceByCacheKey($referenceId); try { diff --git a/core/Controller/TranslationApiController.php b/core/Controller/TranslationApiController.php index c1628c24555aa..3f70a5bcb0530 100644 --- a/core/Controller/TranslationApiController.php +++ b/core/Controller/TranslationApiController.php @@ -6,6 +6,7 @@ * @copyright Copyright (c) 2022 Julius Härtl * * @author Julius Härtl + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -47,10 +48,14 @@ public function __construct( /** * @PublicPage + * + * Get the list of supported languages + * + * @return DataResponse */ public function languages(): DataResponse { return new DataResponse([ - 'languages' => $this->translationManager->getLanguages(), + 'languages' => array_map(fn ($lang) => $lang->jsonSerialize(), $this->translationManager->getLanguages()), 'languageDetection' => $this->translationManager->canDetectLanguage(), ]); } @@ -59,6 +64,17 @@ public function languages(): DataResponse { * @PublicPage * @UserRateThrottle(limit=25, period=120) * @AnonRateThrottle(limit=10, period=120) + * + * Translate a text + * + * @param string $text Text to be translated + * @param string|null $fromLanguage Language to translate from + * @param string $toLanguage Language to translate to + * @return DataResponse|DataResponse + * + * 200: Translated text returned + * 400: Language not detected or unable to translate + * 412: Translating is not possible */ public function translate(string $text, ?string $fromLanguage, string $toLanguage): DataResponse { try { diff --git a/core/Controller/TwoFactorChallengeController.php b/core/Controller/TwoFactorChallengeController.php index 40b100c41bdab..90f033d361bcb 100644 --- a/core/Controller/TwoFactorChallengeController.php +++ b/core/Controller/TwoFactorChallengeController.php @@ -7,6 +7,7 @@ * @author Joas Schilling * @author Lukas Reschke * @author Roeland Jago Douma + * @author Kate Döen * * @license AGPL-3.0 * @@ -82,6 +83,7 @@ private function splitProvidersAndBackupCodes(array $providers): array { * @NoAdminRequired * @NoCSRFRequired * @TwoFactorSetUpDoneRequired + * @IgnoreAPI * * @param string $redirect_url * @return StandaloneTemplateResponse @@ -108,6 +110,7 @@ public function selectChallenge($redirect_url) { * @NoAdminRequired * @NoCSRFRequired * @TwoFactorSetUpDoneRequired + * @IgnoreAPI * * @param string $challengeProviderId * @param string $redirect_url @@ -159,6 +162,7 @@ public function showChallenge($challengeProviderId, $redirect_url) { * @NoAdminRequired * @NoCSRFRequired * @TwoFactorSetUpDoneRequired + * @IgnoreAPI * * @UserRateThrottle(limit=5, period=100) * @@ -204,6 +208,7 @@ public function solveChallenge($challengeProviderId, $challenge, $redirect_url = /** * @NoAdminRequired * @NoCSRFRequired + * @IgnoreAPI */ public function setupProviders(): StandaloneTemplateResponse { $user = $this->userSession->getUser(); @@ -220,6 +225,7 @@ public function setupProviders(): StandaloneTemplateResponse { /** * @NoAdminRequired * @NoCSRFRequired + * @IgnoreAPI */ public function setupProvider(string $providerId) { $user = $this->userSession->getUser(); @@ -251,6 +257,7 @@ public function setupProvider(string $providerId) { /** * @NoAdminRequired * @NoCSRFRequired + * @IgnoreAPI * * @todo handle the extreme edge case of an invalid provider ID and redirect to the provider selection page */ diff --git a/core/Controller/UnifiedSearchController.php b/core/Controller/UnifiedSearchController.php index 7e73ac8100fd1..ed262fcb4bd18 100644 --- a/core/Controller/UnifiedSearchController.php +++ b/core/Controller/UnifiedSearchController.php @@ -8,6 +8,7 @@ * @author Christoph Wurst * @author Joas Schilling * @author John Molakvoæ + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -29,6 +30,7 @@ use OC\Search\SearchComposer; use OC\Search\SearchQuery; +use OCA\Core\ResponseDefinitions; use OCP\AppFramework\OCSController; use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; @@ -39,6 +41,10 @@ use OCP\Search\ISearchQuery; use Symfony\Component\Routing\Exception\ResourceNotFoundException; +/** + * @psalm-import-type CoreUnifiedSearchProvider from ResponseDefinitions + * @psalm-import-type CoreUnifiedSearchResult from ResponseDefinitions + */ class UnifiedSearchController extends OCSController { public function __construct( IRequest $request, @@ -54,7 +60,10 @@ public function __construct( * @NoAdminRequired * @NoCSRFRequired * + * Get the providers for unified search + * * @param string $from the url the user is currently at + * @return DataResponse */ public function getProviders(string $from = ''): DataResponse { [$route, $parameters] = $this->getRouteInformation($from); @@ -69,14 +78,19 @@ public function getProviders(string $from = ''): DataResponse { * @NoAdminRequired * @NoCSRFRequired * - * @param string $providerId - * @param string $term - * @param int|null $sortOrder - * @param int|null $limit - * @param int|string|null $cursor - * @param string $from + * Search + * + * @param string $providerId ID of the provider + * @param string $term Term to search + * @param int|null $sortOrder Order of entries + * @param int|null $limit Maximum amount of entries + * @param int|string|null $cursor Offset for searching + * @param string $from The current user URL + * + * @return DataResponse|DataResponse * - * @return DataResponse + * 200: Search entries returned + * 400: Searching is not possible */ public function search(string $providerId, string $term = '', @@ -85,7 +99,7 @@ public function search(string $providerId, $cursor = null, string $from = ''): DataResponse { if (empty(trim($term))) { - return new DataResponse(null, Http::STATUS_BAD_REQUEST); + return new DataResponse(new \stdClass(), Http::STATUS_BAD_REQUEST); } [$route, $routeParameters] = $this->getRouteInformation($from); @@ -101,7 +115,7 @@ public function search(string $providerId, $route, $routeParameters ) - ) + )->jsonSerialize() ); } diff --git a/core/Controller/UnsupportedBrowserController.php b/core/Controller/UnsupportedBrowserController.php index 8cdc190deea52..dcff05827d396 100644 --- a/core/Controller/UnsupportedBrowserController.php +++ b/core/Controller/UnsupportedBrowserController.php @@ -6,6 +6,7 @@ * @copyright 2021 John Molakvoæ * * @author John Molakvoæ + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -40,6 +41,7 @@ public function __construct(IRequest $request) { /** * @PublicPage * @NoCSRFRequired + * @IgnoreAPI * * @return Response */ diff --git a/core/Controller/WalledGardenController.php b/core/Controller/WalledGardenController.php index 0079cc5a69a73..1f34c16dcafd8 100644 --- a/core/Controller/WalledGardenController.php +++ b/core/Controller/WalledGardenController.php @@ -4,6 +4,7 @@ * * @author Christoph Wurst * @author Roeland Jago Douma + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -31,6 +32,7 @@ class WalledGardenController extends Controller { /** * @PublicPage * @NoCSRFRequired + * @IgnoreAPI */ public function get(): Response { $resp = new Response(); diff --git a/core/Controller/WellKnownController.php b/core/Controller/WellKnownController.php index 2e317ae01b5d9..0ac692c50259d 100644 --- a/core/Controller/WellKnownController.php +++ b/core/Controller/WellKnownController.php @@ -6,6 +6,7 @@ * @copyright 2020 Christoph Wurst * * @author Christoph Wurst + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -43,6 +44,7 @@ public function __construct( /** * @PublicPage * @NoCSRFRequired + * @IgnoreAPI * * @return Response */ diff --git a/core/Controller/WhatsNewController.php b/core/Controller/WhatsNewController.php index 0791ce616f560..4a839cc7103a3 100644 --- a/core/Controller/WhatsNewController.php +++ b/core/Controller/WhatsNewController.php @@ -4,6 +4,7 @@ * * @author Arthur Schiwon * @author Christoph Wurst + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -54,6 +55,13 @@ public function __construct( /** * @NoAdminRequired + * + * Get the changes + * + * @return DataResponse|DataResponse + * + * 200: Changes returned + * 204: No changes */ public function get():DataResponse { $user = $this->userSession->getUser(); @@ -64,7 +72,7 @@ public function get():DataResponse { $currentVersion = $this->whatsNewService->normalizeVersion($this->config->getSystemValue('version')); if (version_compare($lastRead, $currentVersion, '>=')) { - return new DataResponse([], Http::STATUS_NO_CONTENT); + return new DataResponse(new \stdClass(), Http::STATUS_NO_CONTENT); } try { @@ -85,15 +93,22 @@ public function get():DataResponse { } while ($lang !== 'en' && $iterator->valid()); return new DataResponse($resultData); } catch (DoesNotExistException $e) { - return new DataResponse([], Http::STATUS_NO_CONTENT); + return new DataResponse(new \stdClass(), Http::STATUS_NO_CONTENT); } } /** * @NoAdminRequired * + * Dismiss the changes + * + * @param string $version Version to dismiss the changes for + * + * @return DataResponse * @throws \OCP\PreConditionNotMetException * @throws DoesNotExistException + * + * 200: Changes dismissed */ public function dismiss(string $version):DataResponse { $user = $this->userSession->getUser(); @@ -104,6 +119,6 @@ public function dismiss(string $version):DataResponse { // checks whether it's a valid version, throws an Exception otherwise $this->whatsNewService->getChangesForVersion($version); $this->config->setUserValue($user->getUID(), 'core', 'whatsNewLastRead', $version); - return new DataResponse(); + return new DataResponse(new \stdClass()); } } diff --git a/core/Controller/WipeController.php b/core/Controller/WipeController.php index 6ffb950ca8613..2337fe42dc630 100644 --- a/core/Controller/WipeController.php +++ b/core/Controller/WipeController.php @@ -6,6 +6,7 @@ * @copyright Copyright (c) 2019, Roeland Jago Douma * * @author Roeland Jago Douma + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -48,9 +49,14 @@ public function __construct( * * @AnonRateThrottle(limit=10, period=300) * - * @param string $token + * Check if the device should be wiped * - * @return JSONResponse + * @param string $token App password + * + * @return JSONResponse|JSONResponse + * + * 200: Device should be wiped + * 404: Device should not be wiped */ public function checkWipe(string $token): JSONResponse { try { @@ -60,9 +66,9 @@ public function checkWipe(string $token): JSONResponse { ]); } - return new JSONResponse([], Http::STATUS_NOT_FOUND); + return new JSONResponse(new \stdClass(), Http::STATUS_NOT_FOUND); } catch (InvalidTokenException $e) { - return new JSONResponse([], Http::STATUS_NOT_FOUND); + return new JSONResponse(new \stdClass(), Http::STATUS_NOT_FOUND); } } @@ -74,19 +80,24 @@ public function checkWipe(string $token): JSONResponse { * * @AnonRateThrottle(limit=10, period=300) * - * @param string $token + * Finish the wipe + * + * @param string $token App password + * + * @return JSONResponse * - * @return JSONResponse + * 200: Wipe finished successfully + * 404: Device should not be wiped */ public function wipeDone(string $token): JSONResponse { try { if ($this->remoteWipe->finish($token)) { - return new JSONResponse([]); + return new JSONResponse(new \stdClass()); } - return new JSONResponse([], Http::STATUS_NOT_FOUND); + return new JSONResponse(new \stdClass(), Http::STATUS_NOT_FOUND); } catch (InvalidTokenException $e) { - return new JSONResponse([], Http::STATUS_NOT_FOUND); + return new JSONResponse(new \stdClass(), Http::STATUS_NOT_FOUND); } } } diff --git a/core/ResponseDefinitions.php b/core/ResponseDefinitions.php new file mode 100644 index 0000000000000..9ca194ef8fb91 --- /dev/null +++ b/core/ResponseDefinitions.php @@ -0,0 +1,131 @@ + + * + * @author Kate Döen + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Core; + +/** + * @psalm-type CoreLoginFlowV2Credentials = array{ + * server: string, + * loginName: string, + * appPassword: string, + * } + * + * @psalm-type CoreLoginFlowV2 = array{ + * poll: array{ + * token: string, + * endpoint: string, + * }, + * login: string, + * } + * + * @psalm-type CoreNavigationEntry = array{ + * id: string, + * order: int|string, + * href: string, + * icon: string, + * type: string, + * name: string, + * active: bool, + * classes: string, + * unread: int, + * } + * + * @psalm-type CoreContactsAction = array{ + * title: string, + * icon: string, + * hyperlink: string, + * appId: string, + * } + * + * @psalm-type CoreOpenGraphObject = array{ + * richObjectType: string, + * richObject: array, + * openGraphObject: array{ + * id: string, + * name: string, + * description: ?string, + * thumb: ?string, + * link: string, + * }, + * accessible: bool, + * } + * + * @psalm-type CoreCollection = array{ + * id: int, + * name: string, + * resources: CoreOpenGraphObject[], + * } + * + * @psalm-type CoreReference = array{ + * richObjectType: string, + * richObject: array, + * openGraphObject: CoreOpenGraphObject, + * accessible: bool, + * } + * + * @psalm-type CoreReferenceProvider = array{ + * id: string, + * title: string, + * icon_url: string, + * order: int, + * search_providers_ids: ?string[] + * } + * + * @psalm-type CoreUnifiedSearchProvider = array{ + * id: string, + * name: string, + * order: int, + * } + * + * @psalm-type CoreUnifiedSearchResultEntry = array{ + * thumbnailUrl: string, + * title: string, + * subline: string, + * resourceUrl: string, + * icon: string, + * rounded: bool, + * attributes: string[], + * } + * + * @psalm-type CoreUnifiedSearchResult = array{ + * name: string, + * isPaginated: bool, + * entries: CoreUnifiedSearchResultEntry[], + * cursor: int|string|null, + * } + * + * @psalm-type CoreAutocompleteResult = array{ + * id: string, + * label: string, + * icon: string, + * source: string, + * status: string, + * subline: string, + * shareWithDisplayNameUnique: string, + * } + */ +class ResponseDefinitions { +} diff --git a/core/openapi.json b/core/openapi.json index 06e0047a19006..08c67e4c07a80 100644 --- a/core/openapi.json +++ b/core/openapi.json @@ -64,7 +64,8 @@ ], "properties": { "id": { - "type": "string" + "type": "integer", + "format": "int64" }, "name": { "type": "string" @@ -72,10 +73,7 @@ "resources": { "type": "array", "items": { - "type": "object", - "additionalProperties": { - "type": "object" - } + "$ref": "#/components/schemas/OpenGraphObject" } } } @@ -225,7 +223,7 @@ } } }, - "Reference": { + "OpenGraphObject": { "type": "object", "required": [ "richObjectType", @@ -277,6 +275,32 @@ } } }, + "Reference": { + "type": "object", + "required": [ + "richObjectType", + "richObject", + "openGraphObject", + "accessible" + ], + "properties": { + "richObjectType": { + "type": "string" + }, + "richObject": { + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "openGraphObject": { + "$ref": "#/components/schemas/OpenGraphObject" + }, + "accessible": { + "type": "boolean" + } + } + }, "ReferenceProvider": { "type": "object", "required": [ @@ -485,11 +509,6 @@ "200": { "description": "Avatar returned", "headers": { - "Content-Disposition": { - "schema": { - "type": "string" - } - }, "X-NC-IsCustomAvatar": { "schema": { "type": "integer", @@ -561,11 +580,6 @@ "200": { "description": "Avatar returned", "headers": { - "Content-Disposition": { - "schema": { - "type": "string" - } - }, "X-NC-IsCustomAvatar": { "schema": { "type": "integer", @@ -635,13 +649,6 @@ "responses": { "200": { "description": "Custom avatar returned", - "headers": { - "Content-Disposition": { - "schema": { - "type": "string" - } - } - }, "content": { "*/*": { "schema": { @@ -653,13 +660,6 @@ }, "201": { "description": "Avatar returned", - "headers": { - "Content-Disposition": { - "schema": { - "type": "string" - } - } - }, "content": { "*/*": { "schema": { @@ -724,13 +724,6 @@ "responses": { "200": { "description": "Custom avatar returned", - "headers": { - "Content-Disposition": { - "schema": { - "type": "string" - } - } - }, "content": { "*/*": { "schema": { @@ -742,13 +735,6 @@ }, "201": { "description": "Avatar returned", - "headers": { - "Content-Disposition": { - "schema": { - "type": "string" - } - } - }, "content": { "*/*": { "schema": { @@ -895,7 +881,7 @@ { "name": "a", "in": "query", - "description": "Not crop the preview", + "description": "Whether to crop the preview", "schema": { "type": "integer", "default": 0 @@ -923,13 +909,6 @@ "responses": { "200": { "description": "Preview returned", - "headers": { - "Content-Disposition": { - "schema": { - "type": "string" - } - } - }, "content": { "*/*": { "schema": { @@ -962,7 +941,7 @@ } }, "404": { - "description": "File not found", + "description": "Preview not found", "content": { "application/json": { "schema": { @@ -978,7 +957,7 @@ "/index.php/core/preview.png": { "get": { "operationId": "preview-get-preview", - "summary": "Get a preview by file ID", + "summary": "Get a preview by file path", "tags": [ "preview" ], @@ -1023,7 +1002,7 @@ { "name": "a", "in": "query", - "description": "Not crop the preview", + "description": "Whether to crop the preview", "schema": { "type": "integer", "default": 0 @@ -1051,13 +1030,6 @@ "responses": { "200": { "description": "Preview returned", - "headers": { - "Content-Disposition": { - "schema": { - "type": "string" - } - } - }, "content": { "*/*": { "schema": { @@ -1090,7 +1062,7 @@ } }, "404": { - "description": "File not found", + "description": "Preview not found", "content": { "application/json": { "schema": { @@ -1133,15 +1105,8 @@ "responses": { "200": { "description": "Preview returned", - "headers": { - "Content-Disposition": { - "schema": { - "type": "string" - } - } - }, "content": { - "image/*": { + "*/*": { "schema": { "type": "string", "format": "binary" @@ -1757,8 +1722,7 @@ "required": [ "changelogURL", "product", - "version", - "whatsNew" + "version" ], "properties": { "changelogURL": { @@ -1978,7 +1942,7 @@ } }, "403": { - "description": "Not allowed to create app password", + "description": "Creating app password is not allowed", "content": { "text/plain": { "schema": { @@ -2056,7 +2020,7 @@ } }, "403": { - "description": "Not allowed to rotate app password", + "description": "Rotating app password is not allowed", "content": { "text/plain": { "schema": { @@ -2127,7 +2091,7 @@ } }, "403": { - "description": "Not allowed to delete app password", + "description": "Deleting app password is not allowed", "content": { "text/plain": { "schema": { @@ -3374,7 +3338,15 @@ "references": { "type": "object", "additionalProperties": { - "$ref": "#/components/schemas/Reference" + "nullable": true, + "oneOf": [ + { + "$ref": "#/components/schemas/Reference" + }, + { + "type": "object" + } + ] } } } @@ -3473,8 +3445,15 @@ "references": { "type": "object", "additionalProperties": { - "$ref": "#/components/schemas/Reference", - "nullable": true + "nullable": true, + "oneOf": [ + { + "$ref": "#/components/schemas/Reference" + }, + { + "type": "object" + } + ] } } } diff --git a/lib/private/AppConfig.php b/lib/private/AppConfig.php index 96fa6bf746721..84f0d5b9e5add 100644 --- a/lib/private/AppConfig.php +++ b/lib/private/AppConfig.php @@ -175,7 +175,7 @@ private function getAppValues($app) { /** * Get all apps using the config * - * @return array an array of app ids + * @return string[] an array of app ids * * This function returns a list of all apps that have at least one * entry in the appconfig table. diff --git a/lib/private/CapabilitiesManager.php b/lib/private/CapabilitiesManager.php index 9e63798475bfb..7885a98869d6f 100644 --- a/lib/private/CapabilitiesManager.php +++ b/lib/private/CapabilitiesManager.php @@ -50,7 +50,7 @@ public function __construct(LoggerInterface $logger) { * * @param bool $public get public capabilities only * @throws \InvalidArgumentException - * @return array + * @return array */ public function getCapabilities(bool $public = false, bool $initialState = false) : array { $capabilities = []; diff --git a/lib/private/Contacts/ContactsMenu/Actions/LinkAction.php b/lib/private/Contacts/ContactsMenu/Actions/LinkAction.php index 9fc021435a486..e0d3515f4213a 100644 --- a/lib/private/Contacts/ContactsMenu/Actions/LinkAction.php +++ b/lib/private/Contacts/ContactsMenu/Actions/LinkAction.php @@ -76,6 +76,9 @@ public function getAppId(): string { return $this->appId; } + /** + * @return array{title: string, icon: string, hyperlink: string, appId: string} + */ public function jsonSerialize(): array { return [ 'title' => $this->name, diff --git a/lib/private/Contacts/ContactsMenu/Entry.php b/lib/private/Contacts/ContactsMenu/Entry.php index 649c83ae7d821..f1cb4f9c52fa9 100644 --- a/lib/private/Contacts/ContactsMenu/Entry.php +++ b/lib/private/Contacts/ContactsMenu/Entry.php @@ -141,6 +141,9 @@ public function getProperty(string $key): mixed { return $this->properties[$key]; } + /** + * @return array{id: int|string|null, fullName: string, avatar: string|null, topAction: mixed, actions: array, lastMessage: '', emailAddresses: string[], profileTitle: string|null, profileUrl: string|null} + */ public function jsonSerialize(): array { $topAction = !empty($this->actions) ? $this->actions[0]->jsonSerialize() : null; $otherActions = array_map(function (IAction $action) { diff --git a/lib/private/Updater/ChangesCheck.php b/lib/private/Updater/ChangesCheck.php index 2c1eb321ee0ae..ee4d1f1fceee2 100644 --- a/lib/private/Updater/ChangesCheck.php +++ b/lib/private/Updater/ChangesCheck.php @@ -51,6 +51,7 @@ public function __construct(IClientService $clientService, ChangesMapper $mapper /** * @throws DoesNotExistException + * @return array{changelogURL: string, whatsNew: array} */ public function getChangesForVersion(string $version): array { $version = $this->normalizeVersion($version); diff --git a/lib/private/legacy/OC_Helper.php b/lib/private/legacy/OC_Helper.php index 8c8be0e1069d0..d96cb7bb4e985 100644 --- a/lib/private/legacy/OC_Helper.php +++ b/lib/private/legacy/OC_Helper.php @@ -54,6 +54,18 @@ /** * Collection of useful functions + * + * @psalm-type StorageInfo = array{ + * free: float|int, + * mountPoint: string, + * mountType: string, + * owner: string, + * ownerDisplayName: string, + * quota: float|int, + * relative: float|int, + * total: float|int, + * used: float|int, + * } */ class OC_Helper { private static $templateManager; @@ -458,7 +470,8 @@ public static function findBinaryPath(string $program): ?string { * @param \OCP\Files\FileInfo $rootInfo (optional) * @param bool $includeMountPoints whether to include mount points in the size calculation * @param bool $useCache whether to use the cached quota values - * @return array + * @psalm-suppress LessSpecificReturnStatement Legacy code outputs weird types - manually validated that they are correct + * @return StorageInfo * @throws \OCP\Files\NotFoundException */ public static function getStorageInfo($path, $rootInfo = null, $includeMountPoints = true, $useCache = true) { @@ -491,8 +504,9 @@ public static function getStorageInfo($path, $rootInfo = null, $includeMountPoin } $used = $rootInfo->getSize($includeMountPoints); if ($used < 0) { - $used = 0; + $used = 0.0; } + /** @var int|float $quota */ $quota = \OCP\Files\FileInfo::SPACE_UNLIMITED; $mount = $rootInfo->getMountPoint(); $storage = $mount->getStorage(); @@ -523,6 +537,9 @@ public static function getStorageInfo($path, $rootInfo = null, $includeMountPoin } try { $free = $sourceStorage->free_space($rootInfo->getInternalPath()); + if (is_bool($free)) { + $free = 0.0; + } } catch (\Exception $e) { if ($path === "") { throw $e; @@ -549,6 +566,7 @@ public static function getStorageInfo($path, $rootInfo = null, $includeMountPoin $relative = 0; } + /** @var string $ownerId */ $ownerId = $storage->getOwner($path); $ownerDisplayName = ''; if ($ownerId) { @@ -580,15 +598,20 @@ public static function getStorageInfo($path, $rootInfo = null, $includeMountPoin /** * Get storage info including all mount points and quota + * + * @psalm-suppress LessSpecificReturnStatement Legacy code outputs weird types - manually validated that they are correct + * @return StorageInfo */ private static function getGlobalStorageInfo(int|float $quota, IUser $user, IMountPoint $mount): array { $rootInfo = \OC\Files\Filesystem::getFileInfo('', 'ext'); + /** @var int|float $used */ $used = $rootInfo['size']; if ($used < 0) { - $used = 0; + $used = 0.0; } $total = $quota; + /** @var int|float $free */ $free = $quota - $used; if ($total > 0) { @@ -598,7 +621,7 @@ private static function getGlobalStorageInfo(int|float $quota, IUser $user, IMou // prevent division by zero or error codes (negative values) $relative = round(($used / $total) * 10000) / 100; } else { - $relative = 0; + $relative = 0.0; } if (substr_count($mount->getMountPoint(), '/') < 3) { diff --git a/lib/public/Collaboration/Reference/Reference.php b/lib/public/Collaboration/Reference/Reference.php index 6b92a0fae52eb..0dcb665713c99 100644 --- a/lib/public/Collaboration/Reference/Reference.php +++ b/lib/public/Collaboration/Reference/Reference.php @@ -27,6 +27,7 @@ /** * @since 25.0.0 + * @psalm-type OpenGraphObject = array{id: string, name: string, description: ?string, thumb: ?string, link: string} */ class Reference implements IReference { protected string $reference; @@ -176,6 +177,7 @@ public function getRichObjectType(): string { /** * @inheritdoc * @since 25.0.0 + * @return array */ public function getRichObject(): array { if ($this->richObject === null) { @@ -187,6 +189,7 @@ public function getRichObject(): array { /** * @inheritdoc * @since 25.0.0 + * @return OpenGraphObject */ public function getOpenGraphObject(): array { return [ @@ -237,6 +240,7 @@ public static function fromCache(array $cache): IReference { /** * @inheritdoc * @since 25.0.0 + * @return array{richObjectType: string, richObject: array, openGraphObject: OpenGraphObject, accessible: bool} */ public function jsonSerialize() { return [ diff --git a/lib/public/Files/Template/TemplateFileCreator.php b/lib/public/Files/Template/TemplateFileCreator.php index 3a1e62c6f5c55..43e96b6f21b9c 100644 --- a/lib/public/Files/Template/TemplateFileCreator.php +++ b/lib/public/Files/Template/TemplateFileCreator.php @@ -31,10 +31,13 @@ */ final class TemplateFileCreator implements \JsonSerializable { protected $appId; + /** @var string[] $mimetypes */ protected $mimetypes = []; protected $actionName; protected $fileExtension; + /** @var ?string $iconClass */ protected $iconClass; + /** @var ?float $ratio */ protected $ratio = null; protected $order = 100; /** @@ -124,6 +127,7 @@ public function getActionLabel(): string { /** * @since 21.0.0 + * @return array{app: string, label: string, extension: string, iconClass: ?string, mimetypes: string[], ratio: ?float, actionLabel: string} */ public function jsonSerialize(): array { return [ diff --git a/lib/public/IAppConfig.php b/lib/public/IAppConfig.php index 7ab70fb04bfb4..cf387a8a44ccc 100644 --- a/lib/public/IAppConfig.php +++ b/lib/public/IAppConfig.php @@ -62,7 +62,7 @@ public function getFilteredValues($app); /** * Get all apps using the config - * @return array an array of app ids + * @return string[] an array of app ids * * This function returns a list of all apps that have at least one * entry in the appconfig table. diff --git a/lib/public/Translation/LanguageTuple.php b/lib/public/Translation/LanguageTuple.php index 9defb17e4b6ed..27f932f0a6480 100644 --- a/lib/public/Translation/LanguageTuple.php +++ b/lib/public/Translation/LanguageTuple.php @@ -45,6 +45,7 @@ public function __construct( /** * @since 26.0.0 + * @return array{from: string, fromLabel: string, to: string, toLabel: string} */ public function jsonSerialize(): array { return [ diff --git a/psalm.xml b/psalm.xml index 831b875d5a000..87cecf3e2d294 100644 --- a/psalm.xml +++ b/psalm.xml @@ -83,6 +83,10 @@ + + + + diff --git a/tests/Core/Controller/AppPasswordControllerTest.php b/tests/Core/Controller/AppPasswordControllerTest.php index 47220fcf5aba0..b2437f070dd78 100644 --- a/tests/Core/Controller/AppPasswordControllerTest.php +++ b/tests/Core/Controller/AppPasswordControllerTest.php @@ -243,6 +243,6 @@ public function testDeleteAppPasswordSuccess() { $result = $this->controller->deleteAppPassword(); - $this->assertEquals(new DataResponse(), $result); + $this->assertEquals(new DataResponse(new \stdClass()), $result); } } diff --git a/tests/Core/Controller/ClientFlowLoginV2ControllerTest.php b/tests/Core/Controller/ClientFlowLoginV2ControllerTest.php index a1f50e328dd00..6977350f7a685 100644 --- a/tests/Core/Controller/ClientFlowLoginV2ControllerTest.php +++ b/tests/Core/Controller/ClientFlowLoginV2ControllerTest.php @@ -94,7 +94,7 @@ public function testPollInvalid() { $result = $this->controller->poll('token'); - $this->assertSame([], $result->getData()); + $this->assertSame(\stdClass, $result->getData()); $this->assertSame(Http::STATUS_NOT_FOUND, $result->getStatus()); } diff --git a/tests/Core/Controller/PreviewControllerTest.php b/tests/Core/Controller/PreviewControllerTest.php index 7815505d5a8bb..52d26fdeaed93 100644 --- a/tests/Core/Controller/PreviewControllerTest.php +++ b/tests/Core/Controller/PreviewControllerTest.php @@ -68,21 +68,21 @@ protected function setUp(): void { public function testInvalidFile() { $res = $this->controller->getPreview(''); - $expected = new DataResponse([], Http::STATUS_BAD_REQUEST); + $expected = new DataResponse(new \stdClass(), Http::STATUS_BAD_REQUEST); $this->assertEquals($expected, $res); } public function testInvalidWidth() { $res = $this->controller->getPreview('file', 0); - $expected = new DataResponse([], Http::STATUS_BAD_REQUEST); + $expected = new DataResponse(new \stdClass(), Http::STATUS_BAD_REQUEST); $this->assertEquals($expected, $res); } public function testInvalidHeight() { $res = $this->controller->getPreview('file', 10, 0); - $expected = new DataResponse([], Http::STATUS_BAD_REQUEST); + $expected = new DataResponse(new \stdClass(), Http::STATUS_BAD_REQUEST); $this->assertEquals($expected, $res); } @@ -98,7 +98,7 @@ public function testFileNotFound() { ->willThrowException(new NotFoundException()); $res = $this->controller->getPreview('file'); - $expected = new DataResponse([], Http::STATUS_NOT_FOUND); + $expected = new DataResponse(new \stdClass(), Http::STATUS_NOT_FOUND); $this->assertEquals($expected, $res); } @@ -115,7 +115,7 @@ public function testNotAFile() { ->willReturn($folder); $res = $this->controller->getPreview('file'); - $expected = new DataResponse([], Http::STATUS_NOT_FOUND); + $expected = new DataResponse(new \stdClass(), Http::STATUS_NOT_FOUND); $this->assertEquals($expected, $res); } @@ -136,7 +136,7 @@ public function testNoPreviewAndNoIcon() { ->willReturn(false); $res = $this->controller->getPreview('file', 10, 10, true, false); - $expected = new DataResponse([], Http::STATUS_NOT_FOUND); + $expected = new DataResponse(new \stdClass(), Http::STATUS_NOT_FOUND); $this->assertEquals($expected, $res); } @@ -160,7 +160,7 @@ public function testForbiddenFile() { ->willReturn(false); $res = $this->controller->getPreview('file', 10, 10, true, true); - $expected = new DataResponse([], Http::STATUS_FORBIDDEN); + $expected = new DataResponse(new \stdClass(), Http::STATUS_FORBIDDEN); $this->assertEquals($expected, $res); } @@ -192,7 +192,7 @@ public function testNoPreview() { ->willThrowException(new NotFoundException()); $res = $this->controller->getPreview('file', 10, 10, true, true, 'myMode'); - $expected = new DataResponse([], Http::STATUS_NOT_FOUND); + $expected = new DataResponse(new \stdClass(), Http::STATUS_NOT_FOUND); $this->assertEquals($expected, $res); } diff --git a/tests/Core/Controller/WipeControllerTest.php b/tests/Core/Controller/WipeControllerTest.php index 5c06a867b7931..9801917e6c1e9 100644 --- a/tests/Core/Controller/WipeControllerTest.php +++ b/tests/Core/Controller/WipeControllerTest.php @@ -82,7 +82,7 @@ public function testCheckWipe(bool $valid, bool $couldPerform, bool $result) { if (!$valid || !$couldPerform) { $this->assertSame(Http::STATUS_NOT_FOUND, $result->getStatus()); - $this->assertSame([], $result->getData()); + $this->assertSame(\stdClass, $result->getData()); } else { $this->assertSame(Http::STATUS_OK, $result->getStatus()); $this->assertSame(['wipe' => true], $result->getData()); @@ -111,10 +111,10 @@ public function testWipeDone(bool $valid, bool $couldPerform, bool $result) { if (!$valid || !$couldPerform) { $this->assertSame(Http::STATUS_NOT_FOUND, $result->getStatus()); - $this->assertSame([], $result->getData()); + $this->assertSame(\stdClass, $result->getData()); } else { $this->assertSame(Http::STATUS_OK, $result->getStatus()); - $this->assertSame([], $result->getData()); + $this->assertSame(\stdClass, $result->getData()); } } }