Skip to content

Commit

Permalink
make ApiController endpoints openapi compatible
Browse files Browse the repository at this point in the history
Signed-off-by: Christian Hartmann <chris-hartmann@gmx.de>
  • Loading branch information
Chartman123 committed Oct 27, 2024
1 parent 2bcf91b commit 6168503
Show file tree
Hide file tree
Showing 11 changed files with 3,567 additions and 248 deletions.
4 changes: 4 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ js/
# Handled by transifex
l10n/

# OpenAPI
openapi.json

# PHP
lib/
**/*.php
composer.json
composer.lock
1 change: 1 addition & 0 deletions lib/Capabilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public function __construct(
/**
* Provide App Capabilities
* @inheritdoc
* @return array{forms: array{version: string, apiVersions: array<string>}}
*/
public function getCapabilities() {
return [
Expand Down
364 changes: 219 additions & 145 deletions lib/Controller/ApiController.php

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions lib/Controller/ConfigController.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@

use Psr\Log\LoggerInterface;

#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
class ConfigController extends ApiController {
public function __construct(
protected $appName,
Expand Down
1 change: 1 addition & 0 deletions lib/Controller/PageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
use OCP\IUserSession;
use OCP\Util;

#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
class PageController extends Controller {
private const TEMPLATE_MAIN = 'main';

Expand Down
76 changes: 50 additions & 26 deletions lib/Controller/ShareApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,21 @@
use OCA\Forms\Db\FormMapper;
use OCA\Forms\Db\Share;
use OCA\Forms\Db\ShareMapper;
use OCA\Forms\ResponseDefinitions;
use OCA\Forms\Service\CirclesService;
use OCA\Forms\Service\ConfigService;
use OCA\Forms\Service\FormsService;

use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\IMapperException;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\ApiRoute;
use OCP\AppFramework\Http\Attribute\CORS;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCS\OCSBadRequestException;
use OCP\AppFramework\OCS\OCSException;
use OCP\AppFramework\OCS\OCSForbiddenException;
use OCP\AppFramework\OCS\OCSNotFoundException;
use OCP\AppFramework\OCSController;
use OCP\Files\IRootFolder;
use OCP\IGroup;
Expand All @@ -56,6 +58,9 @@
use OCP\Share\IShare;
use Psr\Log\LoggerInterface;

/**
* @psalm-import-type FormsShare from ResponseDefinitions
*/
class ShareApiController extends OCSController {
private IUser $currentUser;

Expand Down Expand Up @@ -85,9 +90,23 @@ public function __construct(
* @param int $formId The form to share
* @param int $shareType Nextcloud-ShareType
* @param string $shareWith ID of user/group/... to share with. For Empty shareWith and shareType Link, this will be set as RandomID.
* @return DataResponse
* @throws OCSBadRequestException
* @throws OCSForbiddenException
* @param array<string> $permissions the permissions granted on the share, defaults to `submit`
* Possible values:
* - `submit` user can submit
* - `results` user can see the results
* - `results_delete` user can see and delete results
* @return DataResponse<array<FormsShare>, Http::STATUS_CREATED, array<>>
* @throws OCSBadRequestException Invalid shareType
* @throws OCSBadRequestException Invalid permission given
* @throws OCSBadRequestException Invalid user to share with
* @throws OCSBadRequestException Invalid group to share with
* @throws OCSBadRequestException Invalid team to share with
* @throws OCSBadRequestException Unknown shareType
* @throws OCSBadRequestException Share hash exists, please try again
* @throws OCSBadRequestException Teams app is disabled
* @throws OCSForbiddenException Link share not allowed
* @throws OCSForbiddenException This form is not owned by the current user
* @throws OCSNotFoundException Could not find form
*/
#[CORS()]
#[NoAdminRequired()]
Expand All @@ -109,20 +128,20 @@ public function newShare(int $formId, int $shareType, string $shareWith = '', ar
// Block LinkShares if not allowed
if ($shareType === IShare::TYPE_LINK && !$this->configService->getAllowPublicLink()) {
$this->logger->debug('Link Share not allowed.');
throw new OCSForbiddenException('Link Share not allowed.');
throw new OCSForbiddenException('Link share not allowed.');
}

try {
$form = $this->formMapper->findById($formId);
} catch (IMapperException $e) {
$this->logger->debug('Could not find form', ['exception' => $e]);
throw new OCSBadRequestException('Could not find form');
throw new OCSNotFoundException('Could not find form');
}

// Check for permission to share form
if ($form->getOwnerId() !== $this->currentUser->getUID()) {
$this->logger->debug('This form is not owned by the current user');
throw new OCSForbiddenException();
throw new OCSForbiddenException('This form is not owned by the current user');
}

if (!$this->validatePermissions($permissions, $shareType)) {
Expand Down Expand Up @@ -157,11 +176,11 @@ public function newShare(int $formId, int $shareType, string $shareWith = '', ar
// Check if hash already exists. (Unfortunately not possible here by unique index on db.)
try {
// Try loading a share to the hash.
$nonex = $this->shareMapper->findPublicShareByHash($shareWith);
$this->shareMapper->findPublicShareByHash($shareWith);

// If we come here, a share has been found --> The share hash already exists, thus aborting.
$this->logger->debug('Share Hash already exists.');
throw new OCSException('Share Hash exists. Please retry.');
$this->logger->debug('Share hash already exists.');
throw new OCSBadRequestException('Share hash exists, please retry.');
} catch (DoesNotExistException $e) {
// Just continue, this is what we expect to happen (share hash not existing yet).
}
Expand All @@ -170,7 +189,7 @@ public function newShare(int $formId, int $shareType, string $shareWith = '', ar
case IShare::TYPE_CIRCLE:
if (!$this->circlesService->isCirclesEnabled()) {
$this->logger->debug('Teams app is disabled, sharing to teams not possible.');
throw new OCSException('Teams app is disabled.');
throw new OCSBadRequestException('Teams app is disabled.');
}
$circle = $this->circlesService->getCircle($shareWith);
if (is_null($circle)) {
Expand All @@ -182,7 +201,7 @@ public function newShare(int $formId, int $shareType, string $shareWith = '', ar
default:
// This passed the check for used shareTypes, but has not been found here.
$this->logger->warning('Unknown, but used shareType: {shareType}. Please file an issue on GitHub.', [ 'shareType' => $shareType ]);
throw new OCSException('Unknown shareType.');
throw new OCSBadRequestException('Unknown shareType.');
}

$share = new Share();
Expand All @@ -202,18 +221,22 @@ public function newShare(int $formId, int $shareType, string $shareWith = '', ar
$shareData = $share->read();
$shareData['displayName'] = $this->formsService->getShareDisplayName($shareData);

return new DataResponse($shareData);
return new DataResponse($shareData, Http::STATUS_CREATED);
}

/**
* Update permissions of a share
*
* @param int $formId of the form
* @param int $shareId of the share to update
* @param array $keyValuePairs Array of key=>value pairs to update.
* @return DataResponse
* @throws OCSBadRequestException
* @throws OCSForbiddenException
* @param array{key: string, values: mixed} $keyValuePairs Array of key=>value pairs to update.
* @return DataResponse<array<int>, Http::STATUS_OK, array<>>
* @throws OCSBadRequestException Share doesn't belong to given Form
* @throws OCSBadRequestException Invalid permission given
* @throws OCSForbiddenException This form is not owned by the current user
* @throws OCSForbiddenException Empty keyValuePairs, will not update
* @throws OCSForbiddenException Not allowed to update other properties than permissions
* @throws OCSNotFoundException Could not find share
*/
#[CORS()]
#[NoAdminRequired()]
Expand All @@ -230,7 +253,7 @@ public function updateShare(int $formId, int $shareId, array $keyValuePairs): Da
$form = $this->formMapper->findById($formId);
} catch (IMapperException $e) {
$this->logger->debug('Could not find share', ['exception' => $e]);
throw new OCSBadRequestException('Could not find share');
throw new OCSNotFoundException('Could not find share');
}

if ($formId !== $formShare->getFormId()) {
Expand All @@ -240,19 +263,19 @@ public function updateShare(int $formId, int $shareId, array $keyValuePairs): Da

if ($form->getOwnerId() !== $this->currentUser->getUID()) {
$this->logger->debug('This form is not owned by the current user');
throw new OCSForbiddenException();
throw new OCSForbiddenException('This form is not owned by the current user');
}

// Don't allow empty array
if (sizeof($keyValuePairs) === 0) {
$this->logger->info('Empty keyValuePairs, will not update.');
throw new OCSForbiddenException();
throw new OCSForbiddenException('Empty keyValuePairs, will not update');
}

//Don't allow to change other properties than permissions
if (count($keyValuePairs) > 1 || !key_exists('permissions', $keyValuePairs)) {
$this->logger->debug('Not allowed to update other properties than permissions');
throw new OCSForbiddenException();
throw new OCSForbiddenException('Not allowed to update other properties than permissions');
}

if (!$this->validatePermissions($keyValuePairs['permissions'], $formShare->getShareType())) {
Expand Down Expand Up @@ -302,9 +325,10 @@ public function updateShare(int $formId, int $shareId, array $keyValuePairs): Da
*
* @param int $formId of the form
* @param int $shareId of the share to delete
* @return DataResponse
* @throws OCSBadRequestException
* @throws OCSForbiddenException
* @return DataResponse<array<int>, Http::STATUS_OK, array<>>
* @throws OCSBadRequestException Share doesn't belong to given Form
* @throws OCSForbiddenException This form is not owned by the current user
* @throws OCSNotFoundException Could not find share
*/
#[CORS()]
#[NoAdminRequired()]
Expand All @@ -320,7 +344,7 @@ public function deleteShare(int $formId, int $shareId): DataResponse {
$form = $this->formMapper->findById($formId);
} catch (IMapperException $e) {
$this->logger->debug('Could not find share', ['exception' => $e]);
throw new OCSBadRequestException('Could not find share');
throw new OCSNotFoundException('Could not find share');
}

if ($formId !== $share->getFormId()) {
Expand All @@ -330,7 +354,7 @@ public function deleteShare(int $formId, int $shareId): DataResponse {

if ($form->getOwnerId() !== $this->currentUser->getUID()) {
$this->logger->debug('This form is not owned by the current user');
throw new OCSForbiddenException();
throw new OCSForbiddenException('This form is not owned by the current user');
}

$this->shareMapper->delete($share);
Expand Down
113 changes: 113 additions & 0 deletions lib/ResponseDefinitions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<?php
/**
* @copyright Copyright (c) 2024 Christian Hartmann <chris-hartmann@gmx.de>
*
* @author Christian Hartmann <chris-hartmann@gmx.de>
*
* @license AGPL-3.0-or-later
*
* 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 <http://www.gnu.org/licenses/>.
*
*/

namespace OCA\Forms;

/**
* @psalm-type FormsForm = array{
* "id": int,
* "hash": string,
* "title": string,
* "description": string,
* "ownerId": string,
* "created": int,
* "access": stdClass,
* "expires": int,
* "isAnonymous": bool,
* "submitMultiple": bool,
* "showExpiration": bool,
* "canSubmit": bool,
* "permissions": array<string>,
* "questions": array<FormsQuestion>,
* "state": int,
* "shares": array<string>,
* "submissions": array<FormsSubmission>,
* }
*
* @psalm-type FormsPartialForm = array{
* "id": int,
* "hash": string,
* "title": string,
* "expires": int,
* "permissions": array<string>,
* "partial": bool,
* "state": int
* }
*
* @psalm-type FormsQuestion = array{
* "id": int,
* "formId": int,
* "order": int,
* "type": string,
* "isRequired": bool,
* "text": string,
* "name": string,
* "options": array<FormsOption>,
* "accept": array<string>,
* "extraSettings": stdClass
* }
*
* @psalm-type FormsOption = array{
* "id": int,
* "questionId": int,
* "text": string,
* "order": ?int
* }
*
* @psalm-type FormsSubmissions = {
* "submissions": array<FormsSubmission>,
* "questions": array<FormsQuestion>
* }
*
* @psalm-type FormsSubmission = array{
* "id": int,
* "formId": int,
* "userId": string,
* "timestamp": int,
* "answers": array<FormsAnswer>,
* "userDisplayName": string
* }
*
* @psalm-type FormsAnswer = array{
* "id": int,
* "submissionId": int,
* "questionId": int,
* "text": string
* }
*
* @psalm-type FormsUploadedFile = array{
* "uploadedFileId": int,
* "fileName": string
* }
*
* @psalm-type FormsShare = array{
* "id": int,
* "formId": int,
* "shareType": int,
* "shareWith": string,
* "permissions": array<string>,
* "displayName": string
* }
*/
class ResponseDefinitions {
}
Loading

0 comments on commit 6168503

Please sign in to comment.