diff --git a/REUSE.toml b/REUSE.toml index a1bcfa2109..e9218cbe28 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -18,7 +18,7 @@ SPDX-FileCopyrightText = "2020-2026 Nextcloud GmbH and Nextcloud contributors" SPDX-License-Identifier = "AGPL-3.0-or-later" [[annotations]] -path = ["cypress/fixtures/**", "openapi.json", "playwright/support/fixtures/files/**", "tests/phpunit.xml", "tests/psalm-baseline.xml", "tests/Integration/features/fixtures/**", "vendor-bin/**"] +path = ["cypress/fixtures/**", "openapi.json", "openapi-administration.json", "openapi-full.json", "playwright/support/fixtures/files/**", "tests/phpunit.xml", "tests/psalm-baseline.xml", "tests/Integration/features/fixtures/**", "vendor-bin/**"] precedence = "aggregate" SPDX-FileCopyrightText = "2020-2026 Nextcloud GmbH and Nextcloud contributors" SPDX-License-Identifier = "AGPL-3.0-or-later" diff --git a/appinfo/routes.php b/appinfo/routes.php index b5a6e47e8f..21afb44f6e 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -21,6 +21,10 @@ ['name' => 'publicStart#showShare', 'url' => '/p/{token}/{path}', 'verb' => 'GET', 'requirements' => ['path' => '.*'], 'defaults' => ['path' => '']], + // Static site archive download route for the external static site generator (not ocs due to simple file download) + ['name' => 'static_site_archive#download', 'url' => '/api/v{apiVersion}/p/static-sites/{staticSiteId}/archive', 'verb' => 'GET', + 'requirements' => ['apiVersion' => '(1.0)']], + // Vue.js router route (Vue.js frontend) ['name' => 'start#indexPath', 'url' => '/{path}', 'verb' => 'GET', 'requirements' => ['path' => '.*'], @@ -67,6 +71,20 @@ ['name' => 'share#deleteCollectiveShare', 'url' => '/api/v{apiVersion}/collectives/{collectiveId}/shares/{token}', 'verb' => 'DELETE', 'requirements' => ['apiVersion' => '(1.0)', 'collectiveId' => '\d+']], + // Collective static sites API + ['name' => 'static_site#index', 'url' => '/api/v{apiVersion}/collectives/{collectiveId}/static-sites', 'verb' => 'GET', + 'requirements' => ['apiVersion' => '(1.0)', 'collectiveId' => '\d+']], + ['name' => 'static_site#create', 'url' => '/api/v{apiVersion}/collectives/{collectiveId}/static-sites', 'verb' => 'POST', + 'requirements' => ['apiVersion' => '(1.0)', 'collectiveId' => '\d+']], + ['name' => 'static_site#update', 'url' => '/api/v{apiVersion}/collectives/{collectiveId}/static-sites/{id}', 'verb' => 'PUT', + 'requirements' => ['apiVersion' => '(1.0)', 'collectiveId' => '\d+', 'id' => '\d+']], + ['name' => 'static_site#delete', 'url' => '/api/v{apiVersion}/collectives/{collectiveId}/static-sites/{id}', 'verb' => 'DELETE', + 'requirements' => ['apiVersion' => '(1.0)', 'collectiveId' => '\d+', 'id' => '\d+']], + + // External static site generator API (admin only) + ['name' => 'static_site_generator#health', 'url' => '/api/v{apiVersion}/static-site-generator/health', 'verb' => 'GET', + 'requirements' => ['apiVersion' => '(1.0)']], + // Pages search API ['name' => 'search#searchRecentPages', 'url' => '/api/v{apiVersion}/collectives/search/recent', 'verb' => 'GET', 'requirements' => ['apiVersion' => '(1.0)']], diff --git a/composer.json b/composer.json index d6d8b21a94..d15e16edef 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ "patrickschur/language-detection": "^5.3", "symfony/string": "^6.0", "symfony/translation-contracts": "^3.6", + "symfony/uid": "^6.4", "wamania/php-stemmer": "^4.0" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 4c9d877e04..73383e4e6b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2e2fc4b0f5deb638c1870f86eaf1d107", + "content-hash": "d1da5b3b7a443b7ce5477a94176adcb7", "packages": [ { "name": "dflydev/dot-access-data", @@ -1080,6 +1080,89 @@ ], "time": "2026-04-10T16:19:22+00:00" }, + { + "name": "symfony/polyfill-uuid", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-uuid.git", + "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/26dfec253c4cf3e51b541b52ddf7e42cb0908e94", + "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-uuid": "*" + }, + "suggest": { + "ext-uuid": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Uuid\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for uuid functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, { "name": "symfony/string", "version": "v6.4.43", @@ -1251,6 +1334,84 @@ ], "time": "2026-06-05T06:23:12+00:00" }, + { + "name": "symfony/uid", + "version": "v6.4.47", + "source": { + "type": "git", + "url": "https://github.com/symfony/uid.git", + "reference": "2266f787b76bbbc339f4fdb40111ca461782f282" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/uid/zipball/2266f787b76bbbc339f4fdb40111ca461782f282", + "reference": "2266f787b76bbbc339f4fdb40111ca461782f282", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-uuid": "^1.15" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Uid\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to generate and represent UIDs", + "homepage": "https://symfony.com", + "keywords": [ + "UID", + "ulid", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/uid/tree/v6.4.47" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-23T16:05:37+00:00" + }, { "name": "wamania/php-stemmer", "version": "v4.0.0", @@ -1362,7 +1523,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": {}, + "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 05a0aab2c1..e53c4e9052 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -74,6 +74,7 @@ public function __construct(array $urlParams = []) { public function register(IRegistrationContext $context): void { require_once(__DIR__ . '/../../vendor/autoload.php'); + $context->registerConfigLexicon(ConfigLexicon::class); $context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedListener::class); $context->registerEventListener(NodeRenamedEvent::class, NodeRenamedListener::class); $context->registerEventListener(NodeWrittenEvent::class, NodeWrittenListener::class); diff --git a/lib/ConfigLexicon.php b/lib/ConfigLexicon.php index b8fe4f09a2..4db0e0956c 100644 --- a/lib/ConfigLexicon.php +++ b/lib/ConfigLexicon.php @@ -31,6 +31,13 @@ class ConfigLexicon implements ILexicon { public const USER_FOLDER = 'user_folder'; + public const PUBLISH_ENABLED = 'publish_enabled'; + public const PUBLISH_ENABLED_DEFAULT = 'false'; + public const STATIC_SITE_MAX_SIZE = 'static_site_max_size'; + public const STATIC_SITE_MAX_SIZE_DEFAULT = 100 * 1024 * 1024; + public const STATIC_SITE_GENERATOR_URL = 'static_site_generator_url'; + public const STATIC_SITE_GENERATOR_TOKEN = 'static_site_generator_token'; + #[\Override] public function getStrictness(): Strictness { return Strictness::IGNORE; @@ -79,6 +86,35 @@ public function getAppConfigs(): array { lazy: false, flags: IAppConfig::FLAG_INTERNAL, ), + new Entry( + key: self::PUBLISH_ENABLED, + type: ValueType::STRING, + defaultRaw: self::PUBLISH_ENABLED_DEFAULT, + definition: 'Whether the publish feature is enabled (`true` or `false`)', + lazy: false, + ), + new Entry( + key: self::STATIC_SITE_MAX_SIZE, + type: ValueType::INT, + defaultRaw: self::STATIC_SITE_MAX_SIZE_DEFAULT, + definition: 'Maximum size of all files of a static site in bytes', + lazy: true, + ), + new Entry( + key: self::STATIC_SITE_GENERATOR_URL, + type: ValueType::STRING, + defaultRaw: '', + definition: 'Base URL of the external static site generator', + lazy: true, + ), + new Entry( + key: self::STATIC_SITE_GENERATOR_TOKEN, + type: ValueType::STRING, + defaultRaw: '', + definition: 'Bearer token shared with the external static site generator', + lazy: true, + flags: IAppConfig::FLAG_SENSITIVE, + ), ]; } diff --git a/lib/Controller/StaticSiteArchiveController.php b/lib/Controller/StaticSiteArchiveController.php new file mode 100644 index 0000000000..00ac93e44a --- /dev/null +++ b/lib/Controller/StaticSiteArchiveController.php @@ -0,0 +1,69 @@ +staticSiteGeneratorClient->isValidToken($this->request->getHeader('Authorization'))) { + $response = new Response(Http::STATUS_UNAUTHORIZED); + $response->throttle(); + return $response; + } + + try { + $archive = $this->staticSiteService->getArchive($staticSiteId); + } catch (NotFoundException) { + return new Response(Http::STATUS_NOT_FOUND); + } + + $response = new FileDisplayResponse($archive); + $response->addHeader('Content-Type', 'application/gzip'); + $response->addHeader('Content-Disposition', 'attachment; filename="' . $archive->getName() . '"'); + return $response; + } +} diff --git a/lib/Controller/StaticSiteController.php b/lib/Controller/StaticSiteController.php new file mode 100644 index 0000000000..4a76bc28b9 --- /dev/null +++ b/lib/Controller/StaticSiteController.php @@ -0,0 +1,186 @@ +, array{}> + * @throws OCSForbiddenException Not permitted + * @throws OCSNotFoundException Collective not found + * + * 200: Static sites returned + */ + #[NoAdminRequired] + public function index(int $collectiveId): DataResponse { + $uid = $this->getUid(); + $staticSites = $this->handleErrorResponse( + fn (): array => $this->staticSiteService->getStaticSites($collectiveId, $uid), + $this->logger, + ); + return new DataResponse($staticSites); + } + + /** + * Start a static site export for a selection of pages of a collective + * + * @param int $collectiveId ID of the collective + * @param list $pageIds IDs of the pages to publish + * @param ?string $title Title of the static site, defaults to the collective name + * @param ?string $slug Slug of the static site (lowercase ASCII letters and numbers, separated by single hyphens), defaults to the slugified collective name + * + * @return DataResponse + * @throws OCSBadRequestException No pages selected, invalid title or slug, slug already in use + * @throws OCSForbiddenException Not permitted + * @throws OCSNotFoundException Collective or page not found + * @throws OCSException Archive of the static site couldn't be provided or the publish service is not available or didn't accept it + * + * 200: Static site created + */ + #[NoAdminRequired] + public function create(int $collectiveId, array $pageIds, ?string $title = null, ?string $slug = null): DataResponse { + $uid = $this->getUid(); + $staticSite = $this->handleArchiveErrorResponse( + fn (): StaticSite => $this->staticSiteService->create($collectiveId, $pageIds, $uid, $title, $slug), + ['collectiveId' => $collectiveId], + ); + return new DataResponse($staticSite); + } + + /** + * Update title and page selection of a static site and publish it again + * + * Every call starts a new publication, even if nothing changed. The slug can't be changed. + * + * @param int $collectiveId ID of the collective + * @param int $id ID of the static site + * @param list $pageIds IDs of the pages to publish + * @param string $title Title of the static site + * + * @return DataResponse + * @throws OCSBadRequestException No pages selected, invalid title or publication in progress + * @throws OCSForbiddenException Not permitted + * @throws OCSNotFoundException Collective, static site or page not found + * @throws OCSException Archive of the static site couldn't be provided or the publish service is not available or didn't accept it + * + * 200: Static site updated + */ + #[NoAdminRequired] + public function update(int $collectiveId, int $id, array $pageIds, string $title): DataResponse { + $uid = $this->getUid(); + $staticSite = $this->handleArchiveErrorResponse( + fn (): StaticSite => $this->staticSiteService->update($collectiveId, $id, $pageIds, $title, $uid), + ['collectiveId' => $collectiveId, 'id' => $id], + ); + return new DataResponse($staticSite); + } + + /** + * Delete a static site + * + * @param int $collectiveId ID of the collective + * @param int $id ID of the static site + * + * @return DataResponse, array{}> + * @throws OCSForbiddenException Not permitted + * @throws OCSNotFoundException Collective or static site not found + * + * 200: Static site deleted + */ + #[NoAdminRequired] + public function delete(int $collectiveId, int $id): DataResponse { + $uid = $this->getUid(); + $this->handleErrorResponse( + fn () => $this->staticSiteService->delete($collectiveId, $id, $uid), + $this->logger, + ); + return new DataResponse([]); + } + + /** + * Returns a translated error for publish failures to tell the user whether trying again later may help. (e.g. publish service is not available) + * + * Requests to the static site generator are logged by its client, other failures are logged once with context. + * + * @throws OCSBadRequestException + * @throws OCSForbiddenException + * @throws OCSNotFoundException + * @throws OCSException + */ + private function handleArchiveErrorResponse(Closure $callback, array $context): StaticSite { + try { + return $this->handleErrorResponse($callback, $this->logger); + } catch (StaticSiteGeneratorNotConfiguredException) { + $this->logger->warning('Static site generator is not configured', $context); + throw new OCSException( + $this->l10n->t('Publishing websites is not set up. Please contact your administrator.'), + Http::STATUS_SERVICE_UNAVAILABLE, + ); + } catch (StaticSiteGeneratorUnavailableException) { + throw new OCSException( + $this->l10n->t('The publish service is currently not available. Please try again later.'), + Http::STATUS_SERVICE_UNAVAILABLE, + ); + } catch (StaticSiteGeneratorException) { + throw new OCSException( + $this->l10n->t('The publish service rejected the website. Please contact your administrator.'), + Http::STATUS_BAD_GATEWAY, + ); + } catch (ServiceException $e) { + $this->logger->error('Failed to provide static site archive', $context + ['exception' => $e]); + throw new OCSException( + $this->l10n->t('The website could not be prepared. Please contact your administrator.'), + Http::STATUS_INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/lib/Controller/StaticSiteGeneratorController.php b/lib/Controller/StaticSiteGeneratorController.php new file mode 100644 index 0000000000..fe661d922a --- /dev/null +++ b/lib/Controller/StaticSiteGeneratorController.php @@ -0,0 +1,50 @@ +|DataResponse + * + * 200: Static site generator is healthy + * 502: Static site generator is not configured, not reachable or not healthy + */ + // TODO(Publish): Change when ext service provides a test endpoint to validate url and shared secret + public function health(): DataResponse { + try { + $this->client->checkHealth(); + } catch (StaticSiteGeneratorException $e) { + return new DataResponse(['message' => $e->getMessage()], Http::STATUS_BAD_GATEWAY); + } + return new DataResponse(['status' => 'ok']); + } +} diff --git a/lib/Db/StaticSite.php b/lib/Db/StaticSite.php new file mode 100644 index 0000000000..e901148fd8 --- /dev/null +++ b/lib/Db/StaticSite.php @@ -0,0 +1,124 @@ +status === self::STATUS_PENDING && $this->updatedAt < $now - self::PENDING_TIMEOUT) + && in_array($this->status, self::IN_PROGRESS_STATUSES, true); + } + + /** + * @throws UnexpectedValueException Stored page selection is corrupted + */ + public function getSelectedPageIds(): array { + try { + $pageIds = json_decode($this->selectedPages ?? '[]', true, 512, JSON_THROW_ON_ERROR); + } catch (JsonException $e) { + throw new UnexpectedValueException('Invalid page selection stored for static site ' . $this->id, 0, $e); + } + if (!is_array($pageIds)) { + throw new UnexpectedValueException('Invalid page selection stored for static site ' . $this->id); + } + return $pageIds; + } + + /** + * @throws UnprocessableEntityException + */ + public function setSelectedPageIds(array $pageIds): void { + $this->setSelectedPages(self::encodePageIds($pageIds)); + } + + /** + * @throws UnprocessableEntityException + */ + public static function encodePageIds(array $pageIds): string { + try { + return json_encode(array_values($pageIds), JSON_THROW_ON_ERROR); + } catch (JsonException $e) { + throw new UnprocessableEntityException('Invalid page IDs: ' . $e->getMessage(), 0, $e); + } + } + + public function jsonSerialize(): array { + return [ + 'id' => $this->id, + 'collectiveId' => $this->collectiveId, + 'staticSiteId' => $this->staticSiteId, + 'title' => $this->title, + 'slug' => $this->slug, + 'selectedPageIds' => $this->getSelectedPageIds(), + 'publishedUrl' => $this->publishedUrl, + 'status' => $this->status, + 'createdBy' => $this->createdBy, + 'createdAt' => $this->createdAt, + 'updatedAt' => $this->updatedAt, + ]; + } +} diff --git a/lib/Db/StaticSiteMapper.php b/lib/Db/StaticSiteMapper.php new file mode 100644 index 0000000000..443d494c3e --- /dev/null +++ b/lib/Db/StaticSiteMapper.php @@ -0,0 +1,205 @@ + + */ +class StaticSiteMapper extends QBMapper { + public function __construct( + IDBConnection $db, + private readonly ITimeFactory $timeFactory, + ) { + parent::__construct($db, 'collectives_st_sites', StaticSite::class); + } + + /** + * @return StaticSite[] + */ + public function findByCollectiveId(int $collectiveId): array { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->tableName) + ->where( + $qb->expr()->eq('collective_id', $qb->createNamedParameter($collectiveId, IQueryBuilder::PARAM_INT)) + ) + ->orderBy('created_at', 'DESC'); + return $this->findEntities($qb); + } + + /** + * @throws DoesNotExistException + * @throws MultipleObjectsReturnedException + * @throws Exception + */ + public function findOneByStaticSiteId(string $staticSiteId): StaticSite { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->tableName) + ->where( + $qb->expr()->eq('static_site_id', $qb->createNamedParameter($staticSiteId, IQueryBuilder::PARAM_STR)) + ); + return $this->findEntity($qb); + } + + /** + * @param list $pageIds + * + * @throws UnprocessableEntityException + * @throws Exception + */ + public function create(int $collectiveId, array $pageIds, string $title, string $slug, string $createdBy): StaticSite { + $now = $this->timeFactory->getTime(); + + $staticSite = new StaticSite(); + $staticSite->setCollectiveId($collectiveId); + $staticSite->setStaticSiteId(Uuid::v7()->toRfc4122()); + $staticSite->setTitle($title); + $staticSite->setSlug($slug); + $staticSite->setSelectedPageIds($pageIds); + $staticSite->setStatus(StaticSite::STATUS_PENDING); + $staticSite->setCreatedBy($createdBy); + $staticSite->setCreatedAt($now); + $staticSite->setUpdatedAt($now); + + return $this->insert($staticSite); + } + + /** + * Set the status, unless the static site was changed since it was read. + * + * Check and update happen in a single query, so of concurrent requests only one can + * change the status, e.g. to pending when updating an existing static site. + * + * @return bool False if the static site was changed meanwhile + * + * @throws Exception + */ + public function updateStatusIfUnchanged(StaticSite $staticSite, string $status): bool { + $qb = $this->db->getQueryBuilder(); + $qb->update($this->tableName) + ->set('status', $qb->createNamedParameter($status, IQueryBuilder::PARAM_STR)) + ->set('updated_at', $qb->createNamedParameter($this->timeFactory->getTime(), IQueryBuilder::PARAM_INT)) + ->where($qb->expr()->eq('id', $qb->createNamedParameter($staticSite->getId(), IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('status', $qb->createNamedParameter($staticSite->getStatus(), IQueryBuilder::PARAM_STR))) + ->andWhere($qb->expr()->eq('updated_at', $qb->createNamedParameter($staticSite->getUpdatedAt(), IQueryBuilder::PARAM_INT))); + + return $qb->executeStatement() > 0; + } + + /** + * Store title, page selection and the resulting status of a static site with a provided archive. + * + * @param list $pageIds + * + * @throws NotFoundException + * @throws UnprocessableEntityException + * @throws Exception + */ + public function updateSettingsAndStatus(StaticSite $staticSite, string $title, array $pageIds, string $status): StaticSite { + $qb = $this->db->getQueryBuilder(); + $qb->update($this->tableName) + ->set('title', $qb->createNamedParameter($title, IQueryBuilder::PARAM_STR)) + ->set('selected_pages', $qb->createNamedParameter(StaticSite::encodePageIds($pageIds), IQueryBuilder::PARAM_STR)) + ->set('status', $qb->createNamedParameter($status, IQueryBuilder::PARAM_STR)) + ->set('updated_at', $qb->createNamedParameter($this->timeFactory->getTime(), IQueryBuilder::PARAM_INT)) + ->where($qb->expr()->eq('id', $qb->createNamedParameter($staticSite->getId(), IQueryBuilder::PARAM_INT))); + + if ($qb->executeStatement() === 0) { + throw new NotFoundException('Static site not found'); + } + + try { + return $this->find($staticSite->getId()); + } catch (DoesNotExistException|MultipleObjectsReturnedException $e) { + throw new NotFoundException('Static site not found', 0, $e); + } + } + + /** + * @throws NotFoundException + * @throws Exception + */ + public function updateStatus(int $id, string $status): StaticSite { + try { + $staticSite = $this->find($id); + } catch (DoesNotExistException|MultipleObjectsReturnedException $e) { + throw new NotFoundException('Static site not found', 0, $e); + } + + $staticSite->setStatus($status); + $staticSite->setUpdatedAt($this->timeFactory->getTime()); + return $this->update($staticSite); + } + + /** + * @throws NotFoundException + * @throws Exception + */ + public function updatePublishedUrl(int $id, string $publishedUrl): StaticSite { + try { + $staticSite = $this->find($id); + } catch (DoesNotExistException|MultipleObjectsReturnedException $e) { + throw new NotFoundException('Static site not found', 0, $e); + } + + $staticSite->setPublishedUrl($publishedUrl); + $staticSite->setUpdatedAt($this->timeFactory->getTime()); + return $this->update($staticSite); + } + + /** + * @throws NotFoundException + * @throws Exception + */ + public function findByIdAndCollectiveId(int $id, int $collectiveId): StaticSite { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->tableName) + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('collective_id', $qb->createNamedParameter($collectiveId, IQueryBuilder::PARAM_INT))); + try { + return $this->findEntity($qb); + } catch (DoesNotExistException|MultipleObjectsReturnedException $e) { + throw new NotFoundException('Static site not found', 0, $e); + } + } + + /** + * @throws DoesNotExistException + * @throws MultipleObjectsReturnedException + * @throws Exception + */ + public function find(int $id): StaticSite { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->tableName) + ->where( + $qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)) + ); + return $this->findEntity($qb); + } +} diff --git a/lib/Listeners/BeforeTemplateRenderedListener.php b/lib/Listeners/BeforeTemplateRenderedListener.php index 8a2c04cff1..c49dd114fc 100644 --- a/lib/Listeners/BeforeTemplateRenderedListener.php +++ b/lib/Listeners/BeforeTemplateRenderedListener.php @@ -10,6 +10,7 @@ namespace OCA\Collectives\Listeners; use OCA\Collectives\AppInfo\Application; +use OCA\Collectives\ConfigLexicon; use OCA\Collectives\Fs\UserFolderHelper; use OCA\Collectives\Service\NotFoundException; use OCA\Collectives\Service\NotPermittedException; @@ -21,6 +22,7 @@ use OCP\EventDispatcher\IEventDispatcher; use OCP\EventDispatcher\IEventListener; use OCP\Files\Template\ITemplateManager; +use OCP\IAppConfig; use OCP\IUserSession; use OCP\Server; use OCP\Util; @@ -33,6 +35,7 @@ public function __construct( private readonly IEventDispatcher $eventDispatcher, private readonly IInitialState $initialState, private readonly ITemplateManager $templateManager, + private readonly IAppConfig $appConfig, ) { } @@ -73,5 +76,8 @@ public function handle(Event $event): void { $this->initialState->provideInitialState('user_folder', $userFolder); $this->initialState->provideInitialState('templates', $this->templateManager->listCreators()); $this->initialState->provideInitialState('is_guest', $isGuest); + + // Provide admin settings as initial state + $this->initialState->provideInitialState('publish_enabled', $this->appConfig->getValueString('collectives', ConfigLexicon::PUBLISH_ENABLED, ConfigLexicon::PUBLISH_ENABLED_DEFAULT) === 'true'); } } diff --git a/lib/Migration/Version040602Date20260908000000.php b/lib/Migration/Version040602Date20260908000000.php new file mode 100644 index 0000000000..7ac1572c7a --- /dev/null +++ b/lib/Migration/Version040602Date20260908000000.php @@ -0,0 +1,77 @@ +hasTable('collectives_st_sites')) { + $table = $schema->createTable('collectives_st_sites'); + $table->addColumn('id', Types::BIGINT, [ + 'autoincrement' => true, + 'notnull' => true, + ]); + $table->addColumn('collective_id', Types::BIGINT, [ + 'notnull' => true, + 'length' => 6, + ]); + $table->addColumn('static_site_id', Types::STRING, [ + 'notnull' => true, + 'length' => 64, + ]); + $table->addColumn('title', Types::STRING, [ + 'notnull' => true, + 'length' => 255, + ]); + $table->addColumn('slug', Types::STRING, [ + 'notnull' => true, + 'length' => 64, + ]); + $table->addColumn('selected_pages', Types::TEXT, [ + 'notnull' => true, + ]); + $table->addColumn('published_url', Types::STRING, [ + 'notnull' => false, + 'length' => 255, + 'default' => null, + ]); + $table->addColumn('status', Types::STRING, [ + 'notnull' => true, + 'length' => 32, + 'default' => 'pending', + ]); + $table->addColumn('created_by', Types::STRING, [ + 'notnull' => true, + 'length' => 64, + ]); + $table->addColumn('created_at', Types::BIGINT, [ + 'notnull' => true, + ]); + $table->addColumn('updated_at', Types::BIGINT, [ + 'notnull' => true, + ]); + $table->setPrimaryKey(['id']); + $table->addIndex(['collective_id'], 'collectives_st_sites_c_id_idx'); + $table->addUniqueIndex(['static_site_id'], 'collectives_st_sites_ssid_idx'); + $table->addUniqueIndex(['slug'], 'collectives_st_sites_slug_idx'); + return $schema; + } + + return null; + } +} diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php index 4d4cfb624d..b531ded865 100644 --- a/lib/ResponseDefinitions.php +++ b/lib/ResponseDefinitions.php @@ -44,6 +44,20 @@ * hasPassword: bool, * } * + * @psalm-type CollectivesStaticSite = array{ + * id: int, + * collectiveId: int, + * staticSiteId: string, + * title: string, + * slug: string, + * selectedPageIds: list, + * publishedUrl: ?string, + * status: string, + * createdBy: string, + * createdAt: int, + * updatedAt: int, + * } + * * @psalm-type CollectivesPageInfo = array{ * id: int, * slug?: string, diff --git a/lib/Service/CollectiveService.php b/lib/Service/CollectiveService.php index 172a1e5c11..5d764f7055 100644 --- a/lib/Service/CollectiveService.php +++ b/lib/Service/CollectiveService.php @@ -371,6 +371,8 @@ public function purgeCollective(Collective $collective): Collective { } finally { // Delete leftovers in any case (also if collective folder is already gone) + // TODO(Publish): Delete static-sites and send delete request to publish service + // Delete shares and user settings $this->shareService->deleteShareByCollectiveId($collective->getId()); $this->collectiveUserSettingsMapper->deleteByCollectiveId($collective->getId()); diff --git a/lib/Service/StaticSiteArchiver.php b/lib/Service/StaticSiteArchiver.php new file mode 100644 index 0000000000..f301b20e5e --- /dev/null +++ b/lib/Service/StaticSiteArchiver.php @@ -0,0 +1,276 @@ + $files Files mapped to their relative path in the archive + * + * @throws UnprocessableEntityException No files given or a path is too long for the archive + * @throws ServiceException + */ + public function store(string $staticSiteId, array $files): void { + if ($files === []) { + throw new UnprocessableEntityException($this->l10n->t('There is no content to publish.')); + } + + foreach (array_keys($files) as $path) { + $this->validateArchivePath((string)$path); + $this->validateArchivePathLength((string)$path); + } + + $tempFolder = $this->tempManager->getTemporaryFolder(); + if ($tempFolder === false) { + throw new ServiceException('Failed to create temporary folder for static site archive'); + } + + try { + $archivePath = $this->buildArchive($tempFolder, $files); + $this->writeToAppData($staticSiteId, $archivePath); + } finally { + // ITempManager only removes temporary files in a background job, archives may be too large to wait for the job + $this->removeDirectory($tempFolder); + } + } + + /** + * @throws NotFoundException No archive stored for the static site + */ + public function get(string $staticSiteId): ISimpleFile { + try { + return $this->appData->getFolder(self::APPDATA_FOLDER)->getFile($staticSiteId . self::ARCHIVE_SUFFIX); + } catch (FilesNotFoundException $e) { + throw new NotFoundException('Static site archive not found', 0, $e); + } + } + + /** + * @throws ServiceException + */ + public function delete(string $staticSiteId): void { + try { + $folder = $this->appData->getFolder(self::APPDATA_FOLDER); + $name = $staticSiteId . self::ARCHIVE_SUFFIX; + if ($folder->fileExists($name)) { + $folder->getFile($name)->delete(); + } + } catch (FilesNotFoundException) { + // No archives stored yet + } catch (FilesNotPermittedException $e) { + throw new ServiceException('Failed to delete static site archive: ' . $e->getMessage(), 0, $e); + } + } + + /** + * Reject paths that would point outside the archive root when extracted. + * + * @throws ServiceException + */ + private function validateArchivePath(string $path): void { + $segments = explode('/', $path); + if ($path === '' + || str_starts_with($path, '/') + || str_contains($path, '\\') + || in_array('', $segments, true) + || in_array('.', $segments, true) + || in_array('..', $segments, true)) { + throw new ServiceException('Invalid path for static site archive: ' . $path); + } + } + + /** + * Mirrors the check of PharData, which splits long paths at a slash into prefix and name. + * + * @throws UnprocessableEntityException + */ + private function validateArchivePathLength(string $path): void { + $length = strlen($path); + if ($length <= self::TAR_NAME_MAX_LENGTH) { + return; + } + + $boundary = $length <= self::TAR_PREFIX_MAX_LENGTH + 1 + self::TAR_NAME_MAX_LENGTH + ? strpos($path, '/', $length - self::TAR_NAME_MAX_LENGTH - 1) + : false; + if ($boundary === false || $boundary > self::TAR_PREFIX_MAX_LENGTH) { + throw new UnprocessableEntityException($this->l10n->t( + 'The path "%s" is too long for the website. Please shorten the titles of the pages in this path or nest them less deeply.', + [$path], + )); + } + } + + /** + * Source files are copied to flat local names, so the archive paths, set by the user, never get used as local paths in the filesystem. + * + * @param array $files + * + * @return string Local path of the tar.gz archive + * + * @throws ServiceException + */ + private function buildArchive(string $tempFolder, array $files): string { + $filesFolder = $tempFolder . '/files'; + if (!mkdir($filesFolder)) { + throw new ServiceException('Failed to create temporary folder for static site files'); + } + + $localPaths = []; + $index = 0; + foreach ($files as $path => $file) { + $localPath = $filesFolder . '/' . $index++; + $this->copyToLocal($file, $localPath); + $localPaths[$path] = $localPath; + } + + try { + // PharData detects the archive format from the file extension + $tar = new PharData($tempFolder . '/site.tar'); + // PharData rewrites the whole archive on every addFile(), so all files are added in one go. + // The generator casts keys to strings, as numeric paths become integer array keys and would get rejected by PharData. + $tar->buildFromIterator((static function () use ($localPaths): Generator { + foreach ($localPaths as $path => $localPath) { + yield (string)$path => $localPath; + } + })()); + // Creates site.tar.gz next to uncompressed site.tar + $tar->compress(Phar::GZ); + } catch (\UnexpectedValueException|\BadMethodCallException|\PharException $e) { + throw new ServiceException('Failed to build static site archive: ' . $e->getMessage(), 0, $e); + } + + $archivePath = $tempFolder . '/site' . self::ARCHIVE_SUFFIX; + if (!is_file($archivePath)) { + throw new ServiceException('PharData did not create the compressed static site archive'); + } + return $archivePath; + } + + /** + * @throws ServiceException + */ + private function copyToLocal(File $file, string $localPath): void { + try { + $source = $file->fopen('r'); + } catch (FilesNotPermittedException $e) { + throw new ServiceException('Failed to read file ' . $file->getPath() . ': ' . $e->getMessage(), 0, $e); + } + if ($source === false) { + throw new ServiceException('Failed to read file ' . $file->getPath()); + } + + $target = fopen($localPath, 'wb'); + try { + if ($target === false || stream_copy_to_stream($source, $target) === false) { + throw new ServiceException('Failed to copy file ' . $file->getPath()); + } + } finally { + fclose($source); + if ($target !== false) { + fclose($target); + } + } + } + + /** + * @throws ServiceException + */ + private function writeToAppData(string $staticSiteId, string $archivePath): void { + $handle = fopen($archivePath, 'rb'); + if ($handle === false) { + throw new ServiceException('Failed to open static site archive'); + } + + try { + $folder = $this->getAppDataFolder(); + $name = $staticSiteId . self::ARCHIVE_SUFFIX; + if ($folder->fileExists($name)) { + $folder->getFile($name)->putContent($handle); + } else { + $folder->newFile($name, $handle); + } + } catch (FilesNotFoundException|FilesNotPermittedException|GenericFileException|LockedException $e) { + throw new ServiceException('Failed to store static site archive: ' . $e->getMessage(), 0, $e); + } finally { + /** @psalm-suppress RedundantCondition Storage backends may close the stream after writing */ + if (is_resource($handle)) { + fclose($handle); + } + } + } + + /** + * ISimpleFolder::getOrCreateFolder() is only available from Nextcloud 35 on. + * + * @throws FilesNotPermittedException + */ + private function getAppDataFolder(): ISimpleFolder { + try { + return $this->appData->getFolder(self::APPDATA_FOLDER); + } catch (FilesNotFoundException) { + return $this->appData->newFolder(self::APPDATA_FOLDER); + } + } + + private function removeDirectory(string $path): void { + if (!is_dir($path)) { + return; + } + + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS), + RecursiveIteratorIterator::CHILD_FIRST, + ); + foreach ($iterator as $item) { + $item->isDir() ? rmdir($item->getPathname()) : unlink($item->getPathname()); + } + rmdir($path); + } +} diff --git a/lib/Service/StaticSiteContentCollector.php b/lib/Service/StaticSiteContentCollector.php new file mode 100644 index 0000000000..4c537f9406 --- /dev/null +++ b/lib/Service/StaticSiteContentCollector.php @@ -0,0 +1,65 @@ + Files mapped to their path relative to the collective folder + * + * @throws FilesNotFoundException + * @throws FilesNotPermittedException + * @throws InvalidPathException + */ + public function collect(int $collectiveId): array { + return $this->collectFiles($this->collectiveFolderManager->getFolder($collectiveId)); + } + + /** + * @return array + * + * @throws FilesNotFoundException + * @throws FilesNotPermittedException + */ + private function collectFiles(Folder $folder, string $prefix = ''): array { + $files = []; + foreach ($folder->getDirectoryListing() as $node) { + $path = $prefix . $node->getName(); + if ($path === TemplateService::TEMPLATE_FOLDER) { + continue; + } + + if ($node instanceof Folder) { + $files += $this->collectFiles($node, $path . '/'); + } elseif ($node instanceof File) { + $files[$path] = $node; + } + } + return $files; + } +} diff --git a/lib/Service/StaticSiteGeneratorClient.php b/lib/Service/StaticSiteGeneratorClient.php new file mode 100644 index 0000000000..5da49aef56 --- /dev/null +++ b/lib/Service/StaticSiteGeneratorClient.php @@ -0,0 +1,144 @@ +getBaseUrl() === '' || $this->getToken() === '') { + throw new StaticSiteGeneratorNotConfiguredException($this->l10n->t('The static site generator is not configured')); + } + } + + /** + * @throws StaticSiteGeneratorException Not configured, not reachable or not healthy + */ + public function checkHealth(): void { + $response = $this->request('GET', '/health'); + if ($response->getStatusCode() !== 200 || ($this->decodeBody($response)['status'] ?? null) !== 'ok') { + throw new StaticSiteGeneratorException($this->l10n->t('The static site generator is not healthy (HTTP status %d)', [$response->getStatusCode()])); + } + } + + /** + * Ask the static site generator to build the static site from its archive. + * + * @throws StaticSiteGeneratorNotConfiguredException + * @throws StaticSiteGeneratorUnavailableException Not reachable or temporarily unavailable + * @throws StaticSiteGeneratorException Request rejected + */ + public function build(StaticSite $staticSite, string $title): void { + $staticSiteId = $staticSite->getStaticSiteId(); + // TODO(Publish): Add callback_status_url to request + $response = $this->request('POST', '/build', [ + 'static_site_id' => $staticSiteId, + 'content_download_url' => $this->urlGenerator->linkToRouteAbsolute('collectives.static_site_archive.download', [ + 'apiVersion' => '1.0', + 'staticSiteId' => $staticSiteId, + ]), + 'slug' => $staticSite->getSlug(), + 'title' => $title, + ]); + + $statusCode = $response->getStatusCode(); + $body = $this->decodeBody($response); + if ($statusCode >= 200 && $statusCode < 300 && ($body['status'] ?? null) === 'enqueued') { + return; + } + + // The error of the generator may contain technical details, so it's only logged + $this->logger->warning('Static site generator did not accept the build', [ + 'staticSiteId' => $staticSiteId, + 'statusCode' => $statusCode, + 'error' => $body['error'] ?? null, + ]); + $message = $this->l10n->t('The static site generator did not accept the build (HTTP status %d)', [$statusCode]); + throw $statusCode >= 500 + ? new StaticSiteGeneratorUnavailableException($message) + : new StaticSiteGeneratorException($message); + } + + /** + * Check the bearer token sent by the static site generator against the configured one. + */ + public function isValidToken(string $authorizationHeader): bool { + $token = $this->getToken(); + return $token !== '' && hash_equals('Bearer ' . $token, $authorizationHeader); + } + + /** + * @throws StaticSiteGeneratorNotConfiguredException + * @throws StaticSiteGeneratorUnavailableException Not reachable + */ + private function request(string $method, string $path, ?array $json = null): IResponse { + $this->assertConfigured(); + + $options = [ + 'headers' => ['Authorization' => 'Bearer ' . $this->getToken()], + 'timeout' => self::TIMEOUT, + // Error status codes are handled by the caller + 'http_errors' => false, + ]; + if ($json !== null) { + $options['json'] = $json; + } + + try { + return $this->clientService->newClient()->request($method, $this->getBaseUrl() . $path, $options); + } catch (Exception $e) { + $this->logger->warning('Failed to reach static site generator', ['exception' => $e]); + throw new StaticSiteGeneratorUnavailableException($this->l10n->t('The static site generator is not reachable'), 0, $e); + } + } + + private function getBaseUrl(): string { + return rtrim($this->appConfig->getValueString('collectives', ConfigLexicon::STATIC_SITE_GENERATOR_URL), '/'); + } + + private function getToken(): string { + return $this->appConfig->getValueString('collectives', ConfigLexicon::STATIC_SITE_GENERATOR_TOKEN); + } + + private function decodeBody(IResponse $response): array { + try { + $body = json_decode((string)$response->getBody(), true, 512, JSON_THROW_ON_ERROR); + } catch (JsonException) { + return []; + } + return is_array($body) ? $body : []; + } +} diff --git a/lib/Service/StaticSiteGeneratorException.php b/lib/Service/StaticSiteGeneratorException.php new file mode 100644 index 0000000000..75913c6469 --- /dev/null +++ b/lib/Service/StaticSiteGeneratorException.php @@ -0,0 +1,18 @@ + $pageIds IDs of the pages to publish, incl. the collective's root page + * + * @throws NotFoundException Collective or one of the pages not found + * @throws NotPermittedException User is not allowed to edit the collective + * @throws UnprocessableEntityException No pages selected, invalid title or slug, slug already in use, path too long + * @throws StaticSiteGeneratorException Generator not configured or didn't accept the build + * @throws ServiceException Archive couldn't be provided + * @throws DBException + */ + public function create(int $collectiveId, array $pageIds, string $userId, ?string $title = null, ?string $slug = null): StaticSite { + $pageIds = $this->normalizePageIds($pageIds); + $collective = $this->getEditableCollective($collectiveId, $userId); + + $title = trim($title ?? $collective->getName()); + $slug ??= $this->generateSlug($collective->getName()); + $this->validateTitle($title); + $this->validateSlug($slug); + $this->verifyPagesBelongToCollective($collectiveId, $pageIds, $userId); + $this->generatorClient->assertConfigured(); + + try { + $staticSite = $this->staticSiteMapper->create($collectiveId, $pageIds, $title, $slug, $userId); + } catch (DBException $e) { + if ($e->getReason() !== DBException::REASON_UNIQUE_CONSTRAINT_VIOLATION) { + throw $e; + } + throw new UnprocessableEntityException('Slug is already in use: ' . $slug, 0, $e); + } + + try { + $this->buildArchive($staticSite); + $this->generatorClient->build($staticSite, $title); + } catch (ServiceException $e) { + $this->staticSiteMapper->delete($staticSite); + $this->deleteArchive($staticSite); + throw $e; + } + + return $this->updateSettingsAndStatus($staticSite, $title, $pageIds, StaticSite::STATUS_PROVIDED); + } + + /** + * Update title and page selection of a static site, provide its archive again and + * ask the static site generator to build it. + * + * The slug can't be changed as it is part of the public URL. If the archive can't be + * provided, the static site and its previous archive remain unchanged. If the generator + * doesn't accept the build, title and page selection are stored with status failed. + * + * @param list $pageIds IDs of the pages to publish, incl. the collective's root page + * + * @throws NotFoundException Collective, static site or one of the pages not found + * @throws NotPermittedException User is not allowed to edit the collective + * @throws UnprocessableEntityException No pages selected, invalid title, publication in progress, path too long + * @throws StaticSiteGeneratorException Generator not configured or didn't accept the build + * @throws ServiceException Archive couldn't be provided + * @throws DBException + */ + public function update(int $collectiveId, int $id, array $pageIds, string $title, string $userId): StaticSite { + $pageIds = $this->normalizePageIds($pageIds); + $this->getEditableCollective($collectiveId, $userId); + + $staticSite = $this->staticSiteMapper->findByIdAndCollectiveId($id, $collectiveId); + if ($staticSite->isInProgress($this->timeFactory->getTime())) { + throw new UnprocessableEntityException('Publication of static site is already in progress'); + } + + $title = trim($title); + $this->validateTitle($title); + $this->verifyPagesBelongToCollective($collectiveId, $pageIds, $userId); + $this->generatorClient->assertConfigured(); + + $previousStatus = $staticSite->getStatus(); + if (!$this->staticSiteMapper->updateStatusIfUnchanged($staticSite, StaticSite::STATUS_PENDING)) { + throw new UnprocessableEntityException('Publication of static site is already in progress'); + } + + try { + $this->buildArchive($staticSite); + } catch (ServiceException $e) { + $this->restoreStatus($staticSite, $previousStatus); + throw $e; + } + + try { + $this->generatorClient->build($staticSite, $title); + } catch (StaticSiteGeneratorException $e) { + $this->updateSettingsAndStatus($staticSite, $title, $pageIds, StaticSite::STATUS_FAILED); + throw $e; + } + + return $this->updateSettingsAndStatus($staticSite, $title, $pageIds, StaticSite::STATUS_PROVIDED); + } + + /** + * @throws NotFoundException Collective or static site not found + * @throws NotPermittedException User is not allowed to edit the collective + * @throws DBException + */ + public function delete(int $collectiveId, int $id, string $userId): void { + $this->getEditableCollective($collectiveId, $userId); + + $staticSite = $this->staticSiteMapper->findByIdAndCollectiveId($id, $collectiveId); + $this->staticSiteMapper->delete($staticSite); + $this->deleteArchive($staticSite); + } + + /** + * @return StaticSite[] + * + * @throws NotFoundException Collective not found + * @throws NotPermittedException User is not allowed to access the collective + */ + public function getStaticSites(int $collectiveId, string $userId): array { + $this->collectiveService->getCollective($collectiveId, $userId); + + return $this->staticSiteMapper->findByCollectiveId($collectiveId); + } + + /** + * Archives of static sites that were deleted meanwhile do not get returned. + * + * @throws NotFoundException Static site or its archive not found + * @throws DBException + */ + public function getArchive(string $staticSiteId): ISimpleFile { + try { + $this->staticSiteMapper->findOneByStaticSiteId($staticSiteId); + } catch (DoesNotExistException|MultipleObjectsReturnedException $e) { + throw new NotFoundException('Static site not found', 0, $e); + } + + return $this->archiver->get($staticSiteId); + } + + /** + * @throws UnprocessableEntityException Content can't be stored in the archive, e.g. too large or a path is too long + * @throws ServiceException + */ + private function buildArchive(StaticSite $staticSite): void { + try { + $files = $this->contentCollector->collect($staticSite->getCollectiveId()); + $this->validateSize($files); + $this->archiver->store($staticSite->getStaticSiteId(), $files); + } catch (UnprocessableEntityException $e) { + throw $e; + } catch (Throwable $e) { + // Catch everything, otherwise the static site would remain in progress + throw new ServiceException('Failed to provide static site archive', 0, $e); + } + + $this->logger->info('Provided static site archive', $this->getLogContext($staticSite) + ['fileCount' => count($files)]); + } + + /** + * The size of the files is checked instead of the archive, so too large sites fail before building. + * As most attachments are compressed already, the archive won't be much smaller anyway. + * + * @param array $files + * + * @throws UnprocessableEntityException + */ + private function validateSize(array $files): void { + $maxSize = $this->appConfig->getValueInt('collectives', ConfigLexicon::STATIC_SITE_MAX_SIZE, ConfigLexicon::STATIC_SITE_MAX_SIZE_DEFAULT); + $sizes = array_map(static fn (File $file): int|float => $file->getSize(), $files); + $totalSize = array_sum($sizes); + if ($totalSize <= $maxSize) { + return; + } + + arsort($sizes); + $largest = array_slice($sizes, 0, self::LARGEST_FILES_IN_MESSAGE, true); + $largestFiles = array_map( + // Numeric paths become integer array keys + static fn (int|string $path, int|float $size): string => $path . ' (' . Util::humanFileSize($size) . ')', + array_keys($largest), + $largest, + ); + throw new UnprocessableEntityException($this->l10n->t( + 'The website would be %1$s, but at most %2$s are allowed. Please remove or shrink large files, e.g. %3$s', + [Util::humanFileSize($totalSize), Util::humanFileSize($maxSize), implode(', ', $largestFiles)], + )); + } + + /** + * @param list $pageIds + * + * @throws NotFoundException Static site was deleted meanwhile + * @throws UnprocessableEntityException + * @throws DBException + */ + private function updateSettingsAndStatus(StaticSite $staticSite, string $title, array $pageIds, string $status): StaticSite { + try { + return $this->staticSiteMapper->updateSettingsAndStatus($staticSite, $title, $pageIds, $status); + } catch (NotFoundException $e) { + $this->deleteArchive($staticSite); + throw $e; + } + } + + /** + * @throws DBException + */ + private function restoreStatus(StaticSite $staticSite, string $status): void { + try { + $this->staticSiteMapper->updateStatus($staticSite->getId(), $status); + } catch (NotFoundException) { + // Static site was deleted meanwhile, nothing to restore + } + } + + /** + * Failing to delete the archive only leaves an orphaned file, so it doesn't throw. + */ + private function deleteArchive(StaticSite $staticSite): void { + try { + $this->archiver->delete($staticSite->getStaticSiteId()); + } catch (ServiceException $e) { + $this->logger->warning('Failed to delete static site archive', $this->getLogContext($staticSite) + ['exception' => $e]); + } + } + + private function getLogContext(StaticSite $staticSite): array { + return [ + 'staticSiteId' => $staticSite->getStaticSiteId(), + 'collectiveId' => $staticSite->getCollectiveId(), + ]; + } + + /** + * @return list + * + * @throws UnprocessableEntityException + */ + private function normalizePageIds(array $pageIds): array { + $pageIds = array_map(intval(...), array_values($pageIds)); + if (empty($pageIds)) { + throw new UnprocessableEntityException('No pages selected for publishing'); + } + return $pageIds; + } + + /** + * @throws NotFoundException + * @throws NotPermittedException + */ + private function getEditableCollective(int $collectiveId, string $userId): Collective { + $collective = $this->collectiveService->getCollective($collectiveId, $userId); + if (!$collective->canEdit()) { + throw new NotPermittedException('Not allowed to edit collective'); + } + return $collective; + } + + private function generateSlug(string $name): string { + $slug = strtolower($this->slugService->generateSlug($name)); + return trim(substr($slug, 0, self::SLUG_MAX_LENGTH), '-'); + } + + /** + * @throws UnprocessableEntityException + */ + private function validateTitle(string $title): void { + if ($title === '' || mb_strlen($title) > self::TITLE_MAX_LENGTH) { + throw new UnprocessableEntityException('Title must be between 1 and ' . self::TITLE_MAX_LENGTH . ' characters'); + } + } + + /** + * @throws UnprocessableEntityException + */ + private function validateSlug(string $slug): void { + if (strlen($slug) > self::SLUG_MAX_LENGTH || !preg_match(self::SLUG_PATTERN, $slug)) { + throw new UnprocessableEntityException('Slug must consist of 1 to ' . self::SLUG_MAX_LENGTH . ' lowercase ASCII letters and numbers, separated by single hyphens'); + } + } + + /** + * @param list $pageIds + * + * @throws NotFoundException If a page doesn't belong to the collective + * @throws NotPermittedException + */ + private function verifyPagesBelongToCollective(int $collectiveId, array $pageIds, string $userId): void { + $validPageIds = array_map( + static fn ($pageInfo) => $pageInfo->getId(), + $this->pageService->findAll($collectiveId, $userId) + ); + + $unknownPageIds = array_diff($pageIds, $validPageIds); + if (!empty($unknownPageIds)) { + throw new NotFoundException('Page(s) not found in collective: ' . implode(', ', $unknownPageIds)); + } + } +} diff --git a/lib/Settings/Admin.php b/lib/Settings/Admin.php index c50a53d483..9c5acc8f0d 100644 --- a/lib/Settings/Admin.php +++ b/lib/Settings/Admin.php @@ -18,6 +18,8 @@ use OCP\Util; class Admin implements ISettings { + public const TOKEN_PLACEHOLDER = '********'; + public function __construct( private readonly IAppConfig $appConfig, private readonly IInitialState $initialState, @@ -27,6 +29,10 @@ public function __construct( public function getForm(): TemplateResponse { $parameters = [ 'default_user_folder' => $this->appConfig->getValueString('collectives', ConfigLexicon::DEFAULT_USER_FOLDER, ''), + 'publish_enabled' => $this->appConfig->getValueString('collectives', ConfigLexicon::PUBLISH_ENABLED, ConfigLexicon::PUBLISH_ENABLED_DEFAULT) === 'true', + 'static_site_generator_url' => $this->appConfig->getValueString('collectives', ConfigLexicon::STATIC_SITE_GENERATOR_URL), + // The token itself must not be rendered into the page, so a set token is replaced by a placeholder + 'static_site_generator_token' => $this->appConfig->getValueString('collectives', ConfigLexicon::STATIC_SITE_GENERATOR_TOKEN) === '' ? '' : self::TOKEN_PLACEHOLDER, ]; $this->initialState->provideInitialState('adminSettings', $parameters); diff --git a/openapi-administration.json b/openapi-administration.json new file mode 100644 index 0000000000..0002ccf536 --- /dev/null +++ b/openapi-administration.json @@ -0,0 +1,304 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "collectives-administration", + "version": "0.0.1", + "description": "Your space to collaboratively write and organize", + "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/collectives/api/v{apiVersion}/static-site-generator/health": { + "get": { + "operationId": "static_site_generator-health", + "summary": "Check whether the configured static site generator is reachable and accepts the token", + "description": "This endpoint requires admin access\nThis endpoint requires admin access", + "tags": [ + "static_site_generator" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Static site generator is healthy", + "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": [ + "status" + ], + "properties": { + "status": { + "type": "string", + "enum": [ + "ok" + ] + } + } + } + } + } + } + } + } + } + }, + "502": { + "description": "Static site generator is not configured, not reachable or not healthy", + "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" + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Logged in account must be an admin", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + } + }, + "tags": [ + { + "name": "collective", + "description": "Provides access to collectives." + }, + { + "name": "trash", + "description": "Provides access to the collective trash." + }, + { + "name": "share", + "description": "Provides access to collective and page shares." + }, + { + "name": "static_site", + "description": "Provides access to static site exports of a collective." + }, + { + "name": "static_site_generator", + "description": "Provides admins access to the external static site generator." + }, + { + "name": "search", + "description": "Allows to search collectives." + }, + { + "name": "page", + "description": "Provides access to pages of a collective." + }, + { + "name": "page_trash", + "description": "Provides access to page trash of a collective." + }, + { + "name": "template", + "description": "Provides access to template pages of a collective." + }, + { + "name": "tag", + "description": "Provides access to tags for pages." + }, + { + "name": "collective_user_settings", + "description": "Provides access to user settings for a specific collective." + }, + { + "name": "session", + "description": "Provides access to collectives user sessions. Sessions are used to track active users/clients in a collective, e.g. to notify about updates." + }, + { + "name": "settings", + "description": "Provides access to the following user settings used by the collectives app. - user_folder: Path where collectives are mounted in user home directory." + }, + { + "name": "public_collective", + "description": "Provides access to public collective and page shares." + }, + { + "name": "public_page", + "description": "Provides access to pages of a public collective/page share." + }, + { + "name": "public_page_trash", + "description": "Provides access to page trash of a public collective share." + }, + { + "name": "public_template", + "description": "Provides access to template pages of a public collective share." + }, + { + "name": "public_tag", + "description": "Provides access to tags for pages." + } + ] +} diff --git a/openapi-full.json b/openapi-full.json new file mode 100644 index 0000000000..65eb13eaf2 --- /dev/null +++ b/openapi-full.json @@ -0,0 +1,16705 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "collectives-full", + "version": "0.0.1", + "description": "Your space to collaboratively write and organize", + "license": { + "name": "agpl" + } + }, + "components": { + "securitySchemes": { + "basic_auth": { + "type": "http", + "scheme": "basic" + }, + "bearer_auth": { + "type": "http", + "scheme": "bearer" + } + }, + "schemas": { + "Collective": { + "type": "object", + "required": [ + "id", + "circleId", + "pageMode", + "name", + "level", + "editPermissionLevel", + "sharePermissionLevel", + "canEdit", + "canShare", + "isPageShare", + "sharePageId", + "shareEditable", + "userPageOrder", + "userShowMembers", + "userShowRecentPages", + "userFavoritePages", + "canLeave" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "slug": { + "type": "string" + }, + "circleId": { + "type": "string" + }, + "emoji": { + "type": "string" + }, + "trashTimestamp": { + "type": "integer", + "format": "int64" + }, + "pageMode": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "level": { + "type": "integer", + "format": "int64" + }, + "editPermissionLevel": { + "type": "integer", + "format": "int64" + }, + "sharePermissionLevel": { + "type": "integer", + "format": "int64" + }, + "canEdit": { + "type": "boolean" + }, + "canShare": { + "type": "boolean" + }, + "shareToken": { + "type": "string" + }, + "isPageShare": { + "type": "boolean" + }, + "sharePageId": { + "type": "integer", + "format": "int64" + }, + "shareEditable": { + "type": "boolean" + }, + "userPageOrder": { + "type": "integer", + "format": "int64" + }, + "userShowMembers": { + "type": "boolean" + }, + "userShowRecentPages": { + "type": "boolean" + }, + "userFavoritePages": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + }, + "canLeave": { + "type": "boolean" + } + } + }, + "CollectiveShare": { + "type": "object", + "required": [ + "id", + "collectiveId", + "pageId", + "token", + "owner", + "editable", + "hasPassword" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "collectiveId": { + "type": "integer", + "format": "int64" + }, + "pageId": { + "type": "integer", + "format": "int64" + }, + "token": { + "type": "string" + }, + "owner": { + "type": "string" + }, + "editable": { + "type": "boolean" + }, + "hasPassword": { + "type": "boolean" + } + } + }, + "OCSMeta": { + "type": "object", + "required": [ + "status", + "statuscode" + ], + "properties": { + "status": { + "type": "string" + }, + "statuscode": { + "type": "integer" + }, + "message": { + "type": "string" + }, + "totalitems": { + "type": "string" + }, + "itemsperpage": { + "type": "string" + } + } + }, + "PageAttachment": { + "type": "object", + "required": [ + "id", + "name", + "filesize", + "mimetype", + "timestamp", + "path", + "src", + "internalPath", + "hasPreview", + "type" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "filesize": { + "type": "integer", + "format": "int64" + }, + "mimetype": { + "type": "string" + }, + "timestamp": { + "type": "integer", + "format": "int64" + }, + "path": { + "type": "string" + }, + "src": { + "type": "string" + }, + "internalPath": { + "type": "string" + }, + "hasPreview": { + "type": "boolean" + }, + "type": { + "type": "string", + "enum": [ + "text", + "folder" + ] + } + } + }, + "PageInfo": { + "type": "object", + "required": [ + "id", + "subpageOrder", + "isFullWidth", + "tags", + "title", + "timestamp", + "size", + "fileName", + "filePath", + "filePathString", + "parentId", + "linkedPageIds" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "slug": { + "type": "string" + }, + "lastUserId": { + "type": "string" + }, + "lastUserDisplayName": { + "type": "string" + }, + "emoji": { + "type": "string" + }, + "subpageOrder": { + "type": "array", + "description": "Custom subpage order. Not guaranteed to contain all subpages of this page.", + "items": { + "type": "integer", + "format": "int64" + } + }, + "isFullWidth": { + "type": "boolean" + }, + "tags": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + }, + "trashTimestamp": { + "type": "integer", + "format": "int64" + }, + "title": { + "type": "string" + }, + "timestamp": { + "type": "integer", + "format": "int64" + }, + "size": { + "type": "integer", + "format": "int64" + }, + "fileName": { + "type": "string" + }, + "filePath": { + "type": "string" + }, + "filePathString": { + "type": "string" + }, + "collectivePath": { + "type": "string" + }, + "collectiveNameWithEmoji": { + "type": "string" + }, + "parentId": { + "type": "integer", + "format": "int64" + }, + "shareToken": { + "type": "string" + }, + "linkedPageIds": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + } + } + }, + "StaticSite": { + "type": "object", + "required": [ + "id", + "collectiveId", + "staticSiteId", + "title", + "slug", + "selectedPageIds", + "publishedUrl", + "status", + "createdBy", + "createdAt", + "updatedAt" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "collectiveId": { + "type": "integer", + "format": "int64" + }, + "staticSiteId": { + "type": "string" + }, + "title": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "selectedPageIds": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + }, + "publishedUrl": { + "type": "string", + "nullable": true + }, + "status": { + "type": "string" + }, + "createdBy": { + "type": "string" + }, + "createdAt": { + "type": "integer", + "format": "int64" + }, + "updatedAt": { + "type": "integer", + "format": "int64" + } + } + }, + "Tag": { + "type": "object", + "required": [ + "id", + "collectiveId", + "name", + "color" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "collectiveId": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "color": { + "type": "string" + } + } + } + } + }, + "paths": { + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives": { + "get": { + "operationId": "collective-index", + "summary": "Get collectives", + "tags": [ + "collective" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Collectives 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": [ + "collectives" + ], + "properties": { + "collectives": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Collective" + } + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Something not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + }, + "post": { + "operationId": "collective-create", + "summary": "Create a collective", + "tags": [ + "collective" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "description": "Name of the collective" + }, + "emoji": { + "type": "string", + "nullable": true, + "default": null, + "description": "Optional emoji" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Collective created", + "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": [ + "collective", + "info" + ], + "properties": { + "collective": { + "$ref": "#/components/schemas/Collective" + }, + "info": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "400": { + "description": "Collective or team already exists", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Something not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{id}": { + "put": { + "operationId": "collective-update", + "summary": "Update an existing collective", + "tags": [ + "collective" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "emoji": { + "type": "string", + "nullable": true, + "default": null, + "description": "Optional emoji" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Collective updated", + "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": [ + "collective" + ], + "properties": { + "collective": { + "$ref": "#/components/schemas/Collective" + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Collective not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + }, + "delete": { + "operationId": "collective-trash", + "summary": "Trash an existing collective", + "tags": [ + "collective" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Collective trashed", + "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": [ + "collective" + ], + "properties": { + "collective": { + "$ref": "#/components/schemas/Collective" + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Collective not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{id}/editLevel": { + "put": { + "operationId": "collective-edit-level", + "summary": "Set edit level for an existing collective", + "tags": [ + "collective" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "level" + ], + "properties": { + "level": { + "type": "integer", + "format": "int64", + "description": "Edit level" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Collective updated", + "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": [ + "collective" + ], + "properties": { + "collective": { + "$ref": "#/components/schemas/Collective" + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Collective not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{id}/shareLevel": { + "put": { + "operationId": "collective-share-level", + "summary": "Set share level for an existing collective", + "tags": [ + "collective" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "level" + ], + "properties": { + "level": { + "type": "integer", + "format": "int64", + "description": "Share level" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Collective updated", + "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": [ + "collective" + ], + "properties": { + "collective": { + "$ref": "#/components/schemas/Collective" + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Collective not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{id}/pageMode": { + "put": { + "operationId": "collective-page-mode", + "summary": "Set page mode for an existing collective", + "tags": [ + "collective" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "mode" + ], + "properties": { + "mode": { + "type": "integer", + "format": "int64", + "description": "Page edit mode" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Collective updated", + "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": [ + "collective" + ], + "properties": { + "collective": { + "$ref": "#/components/schemas/Collective" + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Collective not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/trash": { + "get": { + "operationId": "trash-index", + "summary": "Get trashed collectives", + "tags": [ + "trash" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Trashed collectives 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": [ + "collectives" + ], + "properties": { + "collectives": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Collective" + } + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Something not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/trash/{id}": { + "delete": { + "operationId": "trash-delete", + "summary": "Delete a collective from trash", + "tags": [ + "trash" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "circle", + "in": "query", + "description": "Whether to delete the team/circle as well (optional, default: false)", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Trashed collective deleted", + "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": [ + "collective" + ], + "properties": { + "collective": { + "$ref": "#/components/schemas/Collective" + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Collective not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + }, + "patch": { + "operationId": "trash-restore", + "summary": "Restore a collective from trash", + "tags": [ + "trash" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Trashed collective restored", + "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": [ + "collective" + ], + "properties": { + "collective": { + "$ref": "#/components/schemas/Collective" + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Collective not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{collectiveId}/shares": { + "get": { + "operationId": "share-get-collective-shares", + "summary": "Get collective and page shares of a collective", + "tags": [ + "share" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Shares returned", + "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/CollectiveShare" + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or page not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + }, + "post": { + "operationId": "share-create-collective-share", + "summary": "Create a collective share", + "tags": [ + "share" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "password": { + "type": "string", + "nullable": true, + "default": null, + "description": "Optional password for the share" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Share created", + "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/CollectiveShare" + } + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or page not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{collectiveId}/pages/{pageId}/shares": { + "post": { + "operationId": "share-create-page-share", + "summary": "Create a page share", + "tags": [ + "share" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "password": { + "type": "string", + "nullable": true, + "default": null, + "description": "Optional password for the share" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "pageId", + "in": "path", + "description": "ID of the page", + "required": true, + "schema": { + "type": "integer", + "format": "int64", + "default": 0 + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Share created", + "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/CollectiveShare" + } + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or page not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{collectiveId}/pages/{pageId}/shares/{token}": { + "put": { + "operationId": "share-update-page-share", + "summary": "Update a page share", + "tags": [ + "share" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "editable" + ], + "properties": { + "editable": { + "type": "boolean", + "description": "Whether share has edit permissions" + }, + "password": { + "type": "string", + "nullable": true, + "default": null, + "description": "Optional password for the share" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "pageId", + "in": "path", + "description": "ID of the page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "token", + "in": "path", + "description": "Token of the share", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Share updated", + "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/CollectiveShare" + } + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective, page or share not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + }, + "delete": { + "operationId": "share-delete-page-share", + "summary": "Delete a page share", + "tags": [ + "share" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "pageId", + "in": "path", + "description": "ID of the page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "token", + "in": "path", + "description": "Token of the share", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Share deleted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective, page or share not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{collectiveId}/shares/{token}": { + "put": { + "operationId": "share-update-collective-share", + "summary": "Update a collective share", + "tags": [ + "share" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "editable" + ], + "properties": { + "editable": { + "type": "boolean", + "description": "Whether share has edit permissions" + }, + "password": { + "type": "string", + "nullable": true, + "default": null, + "description": "Optional password for the share" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "token", + "in": "path", + "description": "Token of the share", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Share updated", + "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/CollectiveShare" + } + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective, page or share not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + }, + "delete": { + "operationId": "share-delete-collective-share", + "summary": "Delete a collective share", + "tags": [ + "share" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "token", + "in": "path", + "description": "Token of the share", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Share deleted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or share not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{collectiveId}/static-sites": { + "get": { + "operationId": "static_site-index", + "summary": "Get static sites of a collective", + "tags": [ + "static_site" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Static sites returned", + "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/StaticSite" + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + }, + "post": { + "operationId": "static_site-create", + "summary": "Start a static site export for a selection of pages of a collective", + "tags": [ + "static_site" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "pageIds" + ], + "properties": { + "pageIds": { + "type": "array", + "description": "IDs of the pages to publish", + "items": { + "type": "integer", + "format": "int64" + } + }, + "title": { + "type": "string", + "nullable": true, + "default": null, + "description": "Title of the static site, defaults to the collective name" + }, + "slug": { + "type": "string", + "nullable": true, + "default": null, + "description": "Slug of the static site (lowercase ASCII letters and numbers, separated by single hyphens), defaults to the slugified collective name" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Static site created", + "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/StaticSite" + } + } + } + } + } + } + } + }, + "400": { + "description": "No pages selected, invalid title or slug, slug already in use", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or page not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{collectiveId}/static-sites/{id}": { + "put": { + "operationId": "static_site-update", + "summary": "Update title and page selection of a static site and publish it again", + "description": "Every call starts a new publication, even if nothing changed. The slug can't be changed.", + "tags": [ + "static_site" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "pageIds", + "title" + ], + "properties": { + "pageIds": { + "type": "array", + "description": "IDs of the pages to publish", + "items": { + "type": "integer", + "format": "int64" + } + }, + "title": { + "type": "string", + "description": "Title of the static site" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the static site", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Static site updated", + "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/StaticSite" + } + } + } + } + } + } + } + }, + "400": { + "description": "No pages selected, invalid title or publication in progress", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective, static site or page not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + }, + "delete": { + "operationId": "static_site-delete", + "summary": "Delete a static site", + "tags": [ + "static_site" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the static site", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Static site deleted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or static site not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/search/recent": { + "get": { + "operationId": "search-search-recent-pages", + "summary": "Get recent pages across collectives", + "tags": [ + "search" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "query", + "in": "query", + "description": "Search term", + "schema": { + "type": "string", + "nullable": true, + "default": null + } + }, + { + "name": "limit", + "in": "query", + "description": "Limit of recent pages (default: 10, maximum 100)", + "schema": { + "type": "integer", + "format": "int64", + "default": 10 + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Recent pages 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": [ + "pages" + ], + "properties": { + "pages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PageInfo" + } + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{collectiveId}/search": { + "get": { + "operationId": "page-content-search", + "summary": "Search the content of pages", + "tags": [ + "page" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "searchString", + "in": "query", + "description": "String to search for", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Found pages 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": [ + "pages" + ], + "properties": { + "pages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PageInfo" + } + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{collectiveId}/pages": { + "get": { + "operationId": "page-index", + "summary": "Get pages of a collective", + "tags": [ + "page" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Pages 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": [ + "pages" + ], + "properties": { + "pages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PageInfo" + } + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{collectiveId}/pages/{id}": { + "get": { + "operationId": "page-get", + "summary": "Get one page", + "tags": [ + "page" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Page 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": [ + "page" + ], + "properties": { + "page": { + "$ref": "#/components/schemas/PageInfo" + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or page not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + }, + "put": { + "operationId": "page-move-or-copy", + "summary": "Move or copy a page inside the collective", + "tags": [ + "page" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "parentId": { + "type": "integer", + "format": "int64", + "nullable": true, + "default": null, + "description": "ID of target parent page (optional)" + }, + "title": { + "type": "string", + "nullable": true, + "default": null, + "description": "Target title (optional)" + }, + "index": { + "type": "integer", + "format": "int64", + "nullable": true, + "default": 0, + "description": "Index in subpage order (optional, default 0)" + }, + "copy": { + "type": "boolean", + "default": false, + "description": "Copy the page instead of move (optional, default false)" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Page moved/copied", + "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": [ + "page" + ], + "properties": { + "page": { + "$ref": "#/components/schemas/PageInfo" + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or (parent) page not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + }, + "delete": { + "operationId": "page-trash", + "summary": "Trash a page", + "tags": [ + "page" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Page trashed", + "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": [ + "page" + ], + "properties": { + "page": { + "$ref": "#/components/schemas/PageInfo" + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or page not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{collectiveId}/pages/{parentId}": { + "post": { + "operationId": "page-create", + "summary": "Create a new page", + "tags": [ + "page" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "title" + ], + "properties": { + "title": { + "type": "string", + "description": "Title of the page" + }, + "templateId": { + "type": "integer", + "format": "int64", + "nullable": true, + "default": null, + "description": "ID of the template page to use" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "parentId", + "in": "path", + "description": "ID of the parent page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "New page created", + "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": [ + "page" + ], + "properties": { + "page": { + "$ref": "#/components/schemas/PageInfo" + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or parent/template page not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{collectiveId}/pages/{id}/touch": { + "get": { + "operationId": "page-touch", + "summary": "Touch a page (updates last edited user)", + "tags": [ + "page" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Page touched", + "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": [ + "page" + ], + "properties": { + "page": { + "$ref": "#/components/schemas/PageInfo" + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or parent/template page not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{collectiveId}/pages/{id}/fullWidth": { + "put": { + "operationId": "page-set-full-width", + "summary": "Set/unset full width view for a page", + "tags": [ + "page" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "fullWidth" + ], + "properties": { + "fullWidth": { + "type": "boolean", + "description": "Whether to enable full width view for the page" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Full width view set/unset", + "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": [ + "page" + ], + "properties": { + "page": { + "$ref": "#/components/schemas/PageInfo" + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or page not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{collectiveId}/pages/{id}/to/{newCollectiveId}": { + "put": { + "operationId": "page-move-or-copy-to-collective", + "summary": "Move or copy a page to another collective", + "tags": [ + "page" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "parentId": { + "type": "integer", + "format": "int64", + "nullable": true, + "default": null, + "description": "ID of target parent page (optional)" + }, + "index": { + "type": "integer", + "format": "int64", + "nullable": true, + "default": 0, + "description": "Index in subpage order (optional, default 0)" + }, + "copy": { + "type": "boolean", + "default": false, + "description": "Copy the page instead of move (optional, default false)" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "newCollectiveId", + "in": "path", + "description": "ID of the target collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Page moved/copied", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or (parent) page not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{collectiveId}/pages/{id}/emoji": { + "put": { + "operationId": "page-set-emoji", + "summary": "Set/unset emoji for a page", + "tags": [ + "page" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "emoji": { + "type": "string", + "nullable": true, + "default": null, + "description": "Emoji to set or null to unset (optional, default null)" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Emoji set/unset", + "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": [ + "page" + ], + "properties": { + "page": { + "$ref": "#/components/schemas/PageInfo" + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or page not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{collectiveId}/pages/{id}/subpageOrder": { + "put": { + "operationId": "page-set-subpage-order", + "summary": "Set subpage order for a page", + "tags": [ + "page" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "subpageOrder": { + "type": "string", + "nullable": true, + "default": null, + "description": "JSON-stringified array of subpage IDs" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Subpage order set", + "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": [ + "page" + ], + "properties": { + "page": { + "$ref": "#/components/schemas/PageInfo" + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or page not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{collectiveId}/pages/{id}/tags/{tagId}": { + "put": { + "operationId": "page-add-tag", + "summary": "Add tag to a page", + "tags": [ + "page" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "tagId", + "in": "path", + "description": "ID of the tag to add", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Tag added", + "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": [ + "page" + ], + "properties": { + "page": { + "$ref": "#/components/schemas/PageInfo" + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or page not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + }, + "delete": { + "operationId": "page-remove-tag", + "summary": "Remove tag from a page", + "tags": [ + "page" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "tagId", + "in": "path", + "description": "ID of the tag to remove", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Tag removed", + "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": [ + "page" + ], + "properties": { + "page": { + "$ref": "#/components/schemas/PageInfo" + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or page not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{collectiveId}/pages/{id}/attachments": { + "get": { + "operationId": "page-get-attachments", + "summary": "Get attachments of a page", + "tags": [ + "page" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Attachments 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": [ + "attachments" + ], + "properties": { + "attachments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PageAttachment" + } + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or page not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + }, + "post": { + "operationId": "page-upload-attachment", + "summary": "Upload an attachment", + "tags": [ + "page" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Attachment uploaded", + "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": [ + "attachment" + ], + "properties": { + "attachment": { + "$ref": "#/components/schemas/PageAttachment" + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or page not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "400": { + "description": "Invalid file upload", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{collectiveId}/pages/{id}/attachments/{attachmentId}": { + "put": { + "operationId": "page-rename-attachment", + "summary": "Rename an attachment", + "tags": [ + "page" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "description": "Target name" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "attachmentId", + "in": "path", + "description": "ID of the attachment", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Attachment renamed", + "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": [ + "attachment" + ], + "properties": { + "attachment": { + "$ref": "#/components/schemas/PageAttachment" + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective, page or attachment not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + }, + "delete": { + "operationId": "page-delete-attachment", + "summary": "Delete an attachment", + "tags": [ + "page" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "attachmentId", + "in": "path", + "description": "ID of the attachment", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Attachment deleted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective, page or attachment not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{collectiveId}/pages/{id}/attachments/trash/{attachmentId}": { + "patch": { + "operationId": "page-restore-attachment", + "summary": "Restore an attachment", + "tags": [ + "page" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "attachmentId", + "in": "path", + "description": "ID of the attachment", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Attachment restored", + "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": [ + "attachment" + ], + "properties": { + "attachment": { + "$ref": "#/components/schemas/PageAttachment" + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective, page or attachment not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{collectiveId}/pages/trash": { + "get": { + "operationId": "page_trash-index", + "summary": "Get trashed pages of a collective", + "tags": [ + "page_trash" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Trashed pages 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": [ + "pages" + ], + "properties": { + "pages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PageInfo" + } + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{collectiveId}/pages/trash/{id}": { + "delete": { + "operationId": "page_trash-delete", + "summary": "Permanently delete a page from trash", + "tags": [ + "page_trash" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the trashed page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Trashed page deleted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or page not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + }, + "patch": { + "operationId": "page_trash-restore", + "summary": "Restore a page from trash", + "tags": [ + "page_trash" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the trashed page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Trashed page restored", + "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": [ + "page" + ], + "properties": { + "page": { + "$ref": "#/components/schemas/PageInfo" + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or page not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{collectiveId}/pages/templates": { + "get": { + "operationId": "template-index", + "summary": "Get template pages of a collective", + "tags": [ + "template" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Template pages 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": "array", + "items": { + "$ref": "#/components/schemas/PageInfo" + } + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{collectiveId}/pages/templates/{id}": { + "post": { + "operationId": "template-create", + "summary": "Create a new template page", + "tags": [ + "template" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "title", + "parentId" + ], + "properties": { + "title": { + "type": "string", + "description": "Title of the page" + }, + "parentId": { + "type": "integer", + "format": "int64", + "description": "ID of the parent template page" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^\\d+$" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "New template page created", + "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" + ], + "properties": { + "template": { + "$ref": "#/components/schemas/PageInfo" + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or parent template page not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + }, + "delete": { + "operationId": "template-delete", + "summary": "Permanently delete a template page", + "tags": [ + "template" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the template page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Template page deleted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or page not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + }, + "put": { + "operationId": "template-rename", + "summary": "Rename a template page", + "tags": [ + "template" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "title" + ], + "properties": { + "title": { + "type": "string", + "description": "New title of the template page" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the template page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Template page renamed", + "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" + ], + "properties": { + "template": { + "$ref": "#/components/schemas/PageInfo" + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or page not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{collectiveId}/pages/templates/{id}/emoji": { + "put": { + "operationId": "template-set-emoji", + "summary": "Set/unset emoji for a template page", + "tags": [ + "template" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "emoji": { + "type": "string", + "nullable": true, + "default": null, + "description": "Emoji to set or null to unset (optional, default null)" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the template page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Emoji set/unset", + "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" + ], + "properties": { + "template": { + "$ref": "#/components/schemas/PageInfo" + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or page not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{collectiveId}/tags": { + "get": { + "operationId": "tag-index", + "summary": "Get tags", + "tags": [ + "tag" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Tags 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": [ + "tags" + ], + "properties": { + "tags": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Tag" + } + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Something not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + }, + "post": { + "operationId": "tag-create", + "summary": "Create a tag", + "tags": [ + "tag" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "name", + "color" + ], + "properties": { + "name": { + "type": "string", + "description": "Name of the tag" + }, + "color": { + "type": "string", + "description": "Color of the tag" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Tag created", + "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": [ + "tag", + "info" + ], + "properties": { + "tag": { + "$ref": "#/components/schemas/Tag" + }, + "info": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "400": { + "description": "Tag already exists for the collective or color invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{collectiveId}/tags/{id}": { + "put": { + "operationId": "tag-update", + "summary": "Update an existing tag", + "tags": [ + "tag" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "name", + "color" + ], + "properties": { + "name": { + "type": "string", + "description": "Name of the tag" + }, + "color": { + "type": "string", + "description": "Color of the tag" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the tag", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Tag updated", + "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": [ + "tag" + ], + "properties": { + "tag": { + "$ref": "#/components/schemas/Tag" + } + } + } + } + } + } + } + } + } + }, + "400": { + "description": "Color invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or tag not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + }, + "delete": { + "operationId": "tag-delete", + "summary": "Delete a tag", + "tags": [ + "tag" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the tag", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Tag deleted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or tag not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{collectiveId}/userSettings/pageOrder": { + "put": { + "operationId": "collective_user_settings-set-page-order", + "summary": "Set order type for pages (in page list)", + "tags": [ + "collective_user_settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "pageOrder" + ], + "properties": { + "pageOrder": { + "type": "integer", + "format": "int64", + "description": "Selected page order" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "showMembers settings was set", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{collectiveId}/userSettings/showMembers": { + "put": { + "operationId": "collective_user_settings-set-show-members", + "summary": "Set whether members widget on landing page is expanded or not", + "tags": [ + "collective_user_settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "showMembers" + ], + "properties": { + "showMembers": { + "type": "boolean", + "description": "Whether members widget on landing page is expanded or not" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "showMembers setting was set", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{collectiveId}/userSettings/showRecentPages": { + "put": { + "operationId": "collective_user_settings-set-show-recent-pages", + "summary": "Set whether recent pages widget on landing page is expanded or not", + "tags": [ + "collective_user_settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "showRecentPages" + ], + "properties": { + "showRecentPages": { + "type": "boolean", + "description": "Whether recent pages widget on landing page is expanded or not" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "showRecentPages setting was set", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{collectiveId}/userSettings/favoritePages": { + "put": { + "operationId": "collective_user_settings-set-favorite-pages", + "summary": "Set list of page favorites", + "tags": [ + "collective_user_settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "favoritePages" + ], + "properties": { + "favoritePages": { + "type": "string", + "description": "JSON stringified array with favorite page IDs" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "page favorites were set", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{collectiveId}/userSettings/notify": { + "put": { + "operationId": "collective_user_settings-set-notify", + "summary": "Set whether the user wants notifications about changes in this collective", + "tags": [ + "collective_user_settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "notify" + ], + "properties": { + "notify": { + "type": "integer", + "format": "int64", + "description": "Notification level (0=off, 1=mentions only, 2=all changes)" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "notify level was set", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{collectiveId}/sessions": { + "post": { + "operationId": "session-create", + "summary": "Start a session for a collective", + "tags": [ + "session" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Session created, token 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" + ], + "properties": { + "token": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Collective not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + }, + "put": { + "operationId": "session-sync", + "summary": "Update a session for a collective", + "tags": [ + "session" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "string", + "description": "Token of the session" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Session updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Session not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + }, + "delete": { + "operationId": "session-close", + "summary": "Close a session for a collective", + "tags": [ + "session" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "token", + "in": "query", + "description": "Token of the session", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Session closed or not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/settings/user/{key}": { + "get": { + "operationId": "settings-get-user-setting", + "summary": "Get a collectives user setting by key (defaults to empty string if key is unset)", + "tags": [ + "settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "key", + "in": "path", + "description": "The key to get", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Valid key, value 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": "string" + } + } + } + } + } + } + } + } + }, + "400": { + "description": "Invalid key", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/settings/user": { + "post": { + "operationId": "settings-set-user-setting", + "summary": "Set a collectives user setting by key", + "tags": [ + "settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "key", + "value" + ], + "properties": { + "key": { + "type": "string", + "description": "The key to set" + }, + "value": { + "type": "string", + "description": "The value" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Valid key set to value", + "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": "string" + } + } + } + } + } + } + } + } + }, + "400": { + "description": "Invalid key", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/p/collectives/{token}": { + "get": { + "operationId": "public_collective-get", + "summary": "Get public collective/page share by token", + "tags": [ + "public_collective" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Public collective/page 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": [ + "collectives" + ], + "properties": { + "collectives": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Collective" + } + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Public collective/page share not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/p/collectives/{token}/search": { + "get": { + "operationId": "public_page-content-search", + "summary": "Search the content of pages", + "tags": [ + "public_page" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "searchString", + "in": "query", + "description": "String to search for", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Found pages 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": [ + "pages" + ], + "properties": { + "pages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PageInfo" + } + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/p/collectives/{token}/pages": { + "get": { + "operationId": "public_page-index", + "summary": "Get pages of a collective", + "tags": [ + "public_page" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Pages 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": [ + "pages" + ], + "properties": { + "pages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PageInfo" + } + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Public collective/page share not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/p/collectives/{token}/pages/{id}": { + "get": { + "operationId": "public_page-get", + "summary": "Get one page", + "tags": [ + "public_page" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Page 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": [ + "page" + ], + "properties": { + "page": { + "$ref": "#/components/schemas/PageInfo" + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or page not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + }, + "put": { + "operationId": "public_page-move-or-copy", + "summary": "Move or copy a page inside the collective", + "tags": [ + "public_page" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "parentId": { + "type": "integer", + "format": "int64", + "nullable": true, + "description": "ID of target parent page (optional)" + }, + "title": { + "type": "string", + "nullable": true, + "default": null, + "description": "Target title (optional)" + }, + "index": { + "type": "integer", + "format": "int64", + "nullable": true, + "default": 0, + "description": "Index in subpage order (optional, default 0)" + }, + "copy": { + "type": "boolean", + "default": false, + "description": "Copy the page instead of move (optional, default false)" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Page moved/copied", + "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": [ + "page" + ], + "properties": { + "page": { + "$ref": "#/components/schemas/PageInfo" + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or (parent) page not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + }, + "delete": { + "operationId": "public_page-trash", + "summary": "Trash a page", + "tags": [ + "public_page" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Page trashed", + "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": [ + "page" + ], + "properties": { + "page": { + "$ref": "#/components/schemas/PageInfo" + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or page not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/p/collectives/{token}/pages/{parentId}": { + "post": { + "operationId": "public_page-create", + "summary": "Create a new page", + "tags": [ + "public_page" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "title" + ], + "properties": { + "title": { + "type": "string", + "description": "Title of the page" + }, + "templateId": { + "type": "integer", + "format": "int64", + "nullable": true, + "default": null, + "description": "ID of the template page to use" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "parentId", + "in": "path", + "description": "ID of the parent page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "New page created", + "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": [ + "page" + ], + "properties": { + "page": { + "$ref": "#/components/schemas/PageInfo" + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or parent/template page not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/p/collectives/{token}/pages/{id}/touch": { + "get": { + "operationId": "public_page-touch", + "summary": "Touch a page (updates last edited user)", + "tags": [ + "public_page" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Page touched", + "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": [ + "page" + ], + "properties": { + "page": { + "$ref": "#/components/schemas/PageInfo" + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or parent/template page not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/p/collectives/{token}/pages/{id}/fullWidth": { + "put": { + "operationId": "public_page-set-full-width", + "summary": "Set/unset full width view for a page", + "tags": [ + "public_page" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "fullWidth" + ], + "properties": { + "fullWidth": { + "type": "boolean", + "description": "Whether to enable full width view for the page" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Full width view set/unset", + "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": [ + "page" + ], + "properties": { + "page": { + "$ref": "#/components/schemas/PageInfo" + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or page not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/p/collectives/{token}/pages/{id}/emoji": { + "put": { + "operationId": "public_page-set-emoji", + "summary": "Set/unset emoji for a page", + "tags": [ + "public_page" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "emoji": { + "type": "string", + "nullable": true, + "default": null, + "description": "Emoji to set or null to unset (optional, default null)" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Emoji set/unset", + "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": [ + "page" + ], + "properties": { + "page": { + "$ref": "#/components/schemas/PageInfo" + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or page not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/p/collectives/{token}/pages/{id}/subpageOrder": { + "put": { + "operationId": "public_page-set-subpage-order", + "summary": "Set subpage order for a page", + "tags": [ + "public_page" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "subpageOrder": { + "type": "string", + "nullable": true, + "default": null, + "description": "JSON-stringified array of subpage IDs" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Subpage order set", + "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": [ + "page" + ], + "properties": { + "page": { + "$ref": "#/components/schemas/PageInfo" + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or page not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/p/collectives/{token}/pages/{id}/tags/{tagId}": { + "put": { + "operationId": "public_page-add-tag", + "summary": "Add tag to a page", + "tags": [ + "public_page" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "tagId", + "in": "path", + "description": "ID of the tag to add", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Tag added", + "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": [ + "page" + ], + "properties": { + "page": { + "$ref": "#/components/schemas/PageInfo" + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or page not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + }, + "delete": { + "operationId": "public_page-remove-tag", + "summary": "Remove tag from a page", + "tags": [ + "public_page" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "tagId", + "in": "path", + "description": "ID of the tag to remove", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Tag removed", + "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": [ + "page" + ], + "properties": { + "page": { + "$ref": "#/components/schemas/PageInfo" + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or page not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/p/collectives/{token}/pages/{id}/attachments": { + "get": { + "operationId": "public_page-get-attachments", + "summary": "Get attachments of a page", + "tags": [ + "public_page" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Attachments 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": [ + "attachments" + ], + "properties": { + "attachments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PageAttachment" + } + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or page not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + }, + "post": { + "operationId": "public_page-upload-attachment", + "summary": "Upload an attachment", + "tags": [ + "public_page" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Attachment uploaded", + "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": [ + "attachment" + ], + "properties": { + "attachment": { + "$ref": "#/components/schemas/PageAttachment" + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or page not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "400": { + "description": "Invalid file upload", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/p/collectives/{token}/pages/{id}/attachments/{attachmentId}": { + "put": { + "operationId": "public_page-rename-attachment", + "summary": "Rename an attachment", + "tags": [ + "public_page" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "description": "Target name" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "attachmentId", + "in": "path", + "description": "ID of the attachment", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Attachment renamed", + "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": [ + "attachment" + ], + "properties": { + "attachment": { + "$ref": "#/components/schemas/PageAttachment" + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or attachment not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + }, + "delete": { + "operationId": "public_page-delete-attachment", + "summary": "Delete an attachment", + "tags": [ + "public_page" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "attachmentId", + "in": "path", + "description": "ID of the attachment", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Attachment deleted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or attachment not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/p/collectives/{token}/pages/{id}/attachments/trash/{attachmentId}": { + "patch": { + "operationId": "public_page-restore-attachment", + "summary": "Restore an attachment", + "tags": [ + "public_page" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "attachmentId", + "in": "path", + "description": "ID of the attachment", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Attachment restored", + "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": [ + "attachment" + ], + "properties": { + "attachment": { + "$ref": "#/components/schemas/PageAttachment" + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or attachment not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/p/collectives/{token}/pages/trash": { + "get": { + "operationId": "public_page_trash-index", + "summary": "Get trashed pages of a collective", + "tags": [ + "public_page_trash" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Trashed pages 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": [ + "pages" + ], + "properties": { + "pages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PageInfo" + } + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Public collective/page share not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/p/collectives/{token}/pages/trash/{id}": { + "delete": { + "operationId": "public_page_trash-delete", + "summary": "Permanently delete a page from trash", + "tags": [ + "public_page_trash" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the trashed page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Trashed page deleted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or page not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + }, + "patch": { + "operationId": "public_page_trash-restore", + "summary": "Restore a page from trash", + "tags": [ + "public_page_trash" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the trashed page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Trashed page restored", + "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": [ + "page" + ], + "properties": { + "page": { + "$ref": "#/components/schemas/PageInfo" + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not Permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or page not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/p/collectives/{token}/pages/templates": { + "get": { + "operationId": "public_template-index", + "summary": "Get template pages of a collective", + "tags": [ + "public_template" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Template pages 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": "array", + "items": { + "$ref": "#/components/schemas/PageInfo" + } + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Public collective/page share not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/p/collectives/{token}/tags": { + "get": { + "operationId": "public_tag-index", + "summary": "Get tags", + "tags": [ + "public_tag" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Tags 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": [ + "tags" + ], + "properties": { + "tags": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Tag" + } + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Something not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + }, + "post": { + "operationId": "public_tag-create", + "summary": "Create a tag", + "tags": [ + "public_tag" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "name", + "color" + ], + "properties": { + "name": { + "type": "string", + "description": "Name of the tag" + }, + "color": { + "type": "string", + "description": "Color of the tag" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Tag created", + "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": [ + "tag", + "info" + ], + "properties": { + "tag": { + "$ref": "#/components/schemas/Tag" + }, + "info": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "400": { + "description": "Tag already exists for the collective or color invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/p/collectives/{token}/tags/{id}": { + "put": { + "operationId": "public_tag-update", + "summary": "Update an existing tag", + "tags": [ + "public_tag" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "name", + "color" + ], + "properties": { + "name": { + "type": "string", + "description": "Name of the tag" + }, + "color": { + "type": "string", + "description": "Color of the tag" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the tag", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Tag updated", + "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": [ + "tag" + ], + "properties": { + "tag": { + "$ref": "#/components/schemas/Tag" + } + } + } + } + } + } + } + } + } + }, + "400": { + "description": "Color invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or tag not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + }, + "delete": { + "operationId": "public_tag-delete", + "summary": "Delete a tag", + "tags": [ + "public_tag" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "description": "ID of the tag", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Tag deleted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or tag not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/static-site-generator/health": { + "get": { + "operationId": "static_site_generator-health", + "summary": "Check whether the configured static site generator is reachable and accepts the token", + "description": "This endpoint requires admin access\nThis endpoint requires admin access", + "tags": [ + "static_site_generator" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Static site generator is healthy", + "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": [ + "status" + ], + "properties": { + "status": { + "type": "string", + "enum": [ + "ok" + ] + } + } + } + } + } + } + } + } + } + }, + "502": { + "description": "Static site generator is not configured, not reachable or not healthy", + "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" + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Logged in account must be an admin", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + } + }, + "tags": [ + { + "name": "collective", + "description": "Provides access to collectives." + }, + { + "name": "trash", + "description": "Provides access to the collective trash." + }, + { + "name": "share", + "description": "Provides access to collective and page shares." + }, + { + "name": "static_site", + "description": "Provides access to static site exports of a collective." + }, + { + "name": "static_site_generator", + "description": "Provides admins access to the external static site generator." + }, + { + "name": "search", + "description": "Allows to search collectives." + }, + { + "name": "page", + "description": "Provides access to pages of a collective." + }, + { + "name": "page_trash", + "description": "Provides access to page trash of a collective." + }, + { + "name": "template", + "description": "Provides access to template pages of a collective." + }, + { + "name": "tag", + "description": "Provides access to tags for pages." + }, + { + "name": "collective_user_settings", + "description": "Provides access to user settings for a specific collective." + }, + { + "name": "session", + "description": "Provides access to collectives user sessions. Sessions are used to track active users/clients in a collective, e.g. to notify about updates." + }, + { + "name": "settings", + "description": "Provides access to the following user settings used by the collectives app. - user_folder: Path where collectives are mounted in user home directory." + }, + { + "name": "public_collective", + "description": "Provides access to public collective and page shares." + }, + { + "name": "public_page", + "description": "Provides access to pages of a public collective/page share." + }, + { + "name": "public_page_trash", + "description": "Provides access to page trash of a public collective share." + }, + { + "name": "public_template", + "description": "Provides access to template pages of a public collective share." + }, + { + "name": "public_tag", + "description": "Provides access to tags for pages." + } + ] +} diff --git a/openapi.json b/openapi.json index 83518d4f69..6e4ca343a8 100644 --- a/openapi.json +++ b/openapi.json @@ -332,6 +332,66 @@ } } }, + "StaticSite": { + "type": "object", + "required": [ + "id", + "collectiveId", + "staticSiteId", + "title", + "slug", + "selectedPageIds", + "publishedUrl", + "status", + "createdBy", + "createdAt", + "updatedAt" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "collectiveId": { + "type": "integer", + "format": "int64" + }, + "staticSiteId": { + "type": "string" + }, + "title": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "selectedPageIds": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + }, + "publishedUrl": { + "type": "string", + "nullable": true + }, + "status": { + "type": "string" + }, + "createdBy": { + "type": "string" + }, + "createdAt": { + "type": "integer", + "format": "int64" + }, + "updatedAt": { + "type": "integer", + "format": "int64" + } + } + }, "Tag": { "type": "object", "required": [ @@ -2441,11 +2501,801 @@ { "name": "collectiveId", "in": "path", - "description": "ID of the collective", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Share created", + "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/CollectiveShare" + } + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or page not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{collectiveId}/pages/{pageId}/shares": { + "post": { + "operationId": "share-create-page-share", + "summary": "Create a page share", + "tags": [ + "share" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "password": { + "type": "string", + "nullable": true, + "default": null, + "description": "Optional password for the share" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "pageId", + "in": "path", + "description": "ID of the page", + "required": true, + "schema": { + "type": "integer", + "format": "int64", + "default": 0 + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Share created", + "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/CollectiveShare" + } + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective or page not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{collectiveId}/pages/{pageId}/shares/{token}": { + "put": { + "operationId": "share-update-page-share", + "summary": "Update a page share", + "tags": [ + "share" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "editable" + ], + "properties": { + "editable": { + "type": "boolean", + "description": "Whether share has edit permissions" + }, + "password": { + "type": "string", + "nullable": true, + "default": null, + "description": "Optional password for the share" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "pageId", + "in": "path", + "description": "ID of the page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "token", + "in": "path", + "description": "Token of the share", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Share updated", + "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/CollectiveShare" + } + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective, page or share not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + }, + "delete": { + "operationId": "share-delete-page-share", + "summary": "Delete a page share", + "tags": [ + "share" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "pageId", + "in": "path", + "description": "ID of the page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "token", + "in": "path", + "description": "Token of the share", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Share deleted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Not permitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Collective, page or share not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{collectiveId}/shares/{token}": { + "put": { + "operationId": "share-update-collective-share", + "summary": "Update a collective share", + "tags": [ + "share" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "editable" + ], + "properties": { + "editable": { + "type": "boolean", + "description": "Whether share has edit permissions" + }, + "password": { + "type": "string", + "nullable": true, + "default": null, + "description": "Optional password for the share" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "1.0" + ], + "default": "1.0" + } + }, + { + "name": "collectiveId", + "in": "path", + "description": "ID of the collective", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "token", + "in": "path", + "description": "Token of the share", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } }, { @@ -2461,7 +3311,7 @@ ], "responses": { "200": { - "description": "Share created", + "description": "Share updated", "content": { "application/json": { "schema": { @@ -2519,7 +3369,7 @@ } }, "404": { - "description": "Collective or page not found", + "description": "Collective, page or share not found", "content": { "application/json": { "schema": { @@ -2575,12 +3425,10 @@ } } } - } - }, - "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{collectiveId}/pages/{pageId}/shares": { - "post": { - "operationId": "share-create-page-share", - "summary": "Create a page share", + }, + "delete": { + "operationId": "share-delete-collective-share", + "summary": "Delete a collective share", "tags": [ "share" ], @@ -2592,24 +3440,6 @@ "basic_auth": [] } ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "password": { - "type": "string", - "nullable": true, - "default": null, - "description": "Optional password for the share" - } - } - } - } - } - }, "parameters": [ { "name": "apiVersion", @@ -2634,14 +3464,12 @@ } }, { - "name": "pageId", + "name": "token", "in": "path", - "description": "ID of the page", + "description": "Token of the share", "required": true, "schema": { - "type": "integer", - "format": "int64", - "default": 0 + "type": "string" } }, { @@ -2657,7 +3485,7 @@ ], "responses": { "200": { - "description": "Share created", + "description": "Share deleted", "content": { "application/json": { "schema": { @@ -2676,9 +3504,7 @@ "meta": { "$ref": "#/components/schemas/OCSMeta" }, - "data": { - "$ref": "#/components/schemas/CollectiveShare" - } + "data": {} } } } @@ -2715,7 +3541,7 @@ } }, "404": { - "description": "Collective or page not found", + "description": "Collective or share not found", "content": { "application/json": { "schema": { @@ -2773,12 +3599,12 @@ } } }, - "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{collectiveId}/pages/{pageId}/shares/{token}": { - "put": { - "operationId": "share-update-page-share", - "summary": "Update a page share", + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{collectiveId}/static-sites": { + "get": { + "operationId": "static_site-index", + "summary": "Get static sites of a collective", "tags": [ - "share" + "static_site" ], "security": [ { @@ -2788,31 +3614,6 @@ "basic_auth": [] } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "editable" - ], - "properties": { - "editable": { - "type": "boolean", - "description": "Whether share has edit permissions" - }, - "password": { - "type": "string", - "nullable": true, - "default": null, - "description": "Optional password for the share" - } - } - } - } - } - }, "parameters": [ { "name": "apiVersion", @@ -2836,25 +3637,6 @@ "format": "int64" } }, - { - "name": "pageId", - "in": "path", - "description": "ID of the page", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "token", - "in": "path", - "description": "Token of the share", - "required": true, - "schema": { - "type": "string" - } - }, { "name": "OCS-APIRequest", "in": "header", @@ -2868,7 +3650,7 @@ ], "responses": { "200": { - "description": "Share updated", + "description": "Static sites returned", "content": { "application/json": { "schema": { @@ -2888,7 +3670,10 @@ "$ref": "#/components/schemas/OCSMeta" }, "data": { - "$ref": "#/components/schemas/CollectiveShare" + "type": "array", + "items": { + "$ref": "#/components/schemas/StaticSite" + } } } } @@ -2926,7 +3711,7 @@ } }, "404": { - "description": "Collective, page or share not found", + "description": "Collective not found", "content": { "application/json": { "schema": { @@ -2983,11 +3768,11 @@ } } }, - "delete": { - "operationId": "share-delete-page-share", - "summary": "Delete a page share", + "post": { + "operationId": "static_site-create", + "summary": "Start a static site export for a selection of pages of a collective", "tags": [ - "share" + "static_site" ], "security": [ { @@ -2997,6 +3782,41 @@ "basic_auth": [] } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "pageIds" + ], + "properties": { + "pageIds": { + "type": "array", + "description": "IDs of the pages to publish", + "items": { + "type": "integer", + "format": "int64" + } + }, + "title": { + "type": "string", + "nullable": true, + "default": null, + "description": "Title of the static site, defaults to the collective name" + }, + "slug": { + "type": "string", + "nullable": true, + "default": null, + "description": "Slug of the static site (lowercase ASCII letters and numbers, separated by single hyphens), defaults to the slugified collective name" + } + } + } + } + } + }, "parameters": [ { "name": "apiVersion", @@ -3020,25 +3840,6 @@ "format": "int64" } }, - { - "name": "pageId", - "in": "path", - "description": "ID of the page", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "token", - "in": "path", - "description": "Token of the share", - "required": true, - "schema": { - "type": "string" - } - }, { "name": "OCS-APIRequest", "in": "header", @@ -3052,7 +3853,37 @@ ], "responses": { "200": { - "description": "Share deleted", + "description": "Static site created", + "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/StaticSite" + } + } + } + } + } + } + } + }, + "400": { + "description": "No pages selected, invalid title or slug, slug already in use", "content": { "application/json": { "schema": { @@ -3108,7 +3939,7 @@ } }, "404": { - "description": "Collective, page or share not found", + "description": "Collective or page not found", "content": { "application/json": { "schema": { @@ -3166,12 +3997,13 @@ } } }, - "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{collectiveId}/shares/{token}": { + "/ocs/v2.php/apps/collectives/api/v{apiVersion}/collectives/{collectiveId}/static-sites/{id}": { "put": { - "operationId": "share-update-collective-share", - "summary": "Update a collective share", + "operationId": "static_site-update", + "summary": "Update title and page selection of a static site and publish it again", + "description": "Every call starts a new publication, even if nothing changed. The slug can't be changed.", "tags": [ - "share" + "static_site" ], "security": [ { @@ -3188,18 +4020,21 @@ "schema": { "type": "object", "required": [ - "editable" + "pageIds", + "title" ], "properties": { - "editable": { - "type": "boolean", - "description": "Whether share has edit permissions" + "pageIds": { + "type": "array", + "description": "IDs of the pages to publish", + "items": { + "type": "integer", + "format": "int64" + } }, - "password": { + "title": { "type": "string", - "nullable": true, - "default": null, - "description": "Optional password for the share" + "description": "Title of the static site" } } } @@ -3230,12 +4065,13 @@ } }, { - "name": "token", + "name": "id", "in": "path", - "description": "Token of the share", + "description": "ID of the static site", "required": true, "schema": { - "type": "string" + "type": "integer", + "format": "int64" } }, { @@ -3251,7 +4087,7 @@ ], "responses": { "200": { - "description": "Share updated", + "description": "Static site updated", "content": { "application/json": { "schema": { @@ -3271,7 +4107,7 @@ "$ref": "#/components/schemas/OCSMeta" }, "data": { - "$ref": "#/components/schemas/CollectiveShare" + "$ref": "#/components/schemas/StaticSite" } } } @@ -3280,6 +4116,34 @@ } } }, + "400": { + "description": "No pages selected, invalid title or publication in progress", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, "403": { "description": "Not permitted", "content": { @@ -3309,7 +4173,7 @@ } }, "404": { - "description": "Collective, page or share not found", + "description": "Collective, static site or page not found", "content": { "application/json": { "schema": { @@ -3367,10 +4231,10 @@ } }, "delete": { - "operationId": "share-delete-collective-share", - "summary": "Delete a collective share", + "operationId": "static_site-delete", + "summary": "Delete a static site", "tags": [ - "share" + "static_site" ], "security": [ { @@ -3404,12 +4268,13 @@ } }, { - "name": "token", + "name": "id", "in": "path", - "description": "Token of the share", + "description": "ID of the static site", "required": true, "schema": { - "type": "string" + "type": "integer", + "format": "int64" } }, { @@ -3425,7 +4290,7 @@ ], "responses": { "200": { - "description": "Share deleted", + "description": "Static site deleted", "content": { "application/json": { "schema": { @@ -3481,7 +4346,7 @@ } }, "404": { - "description": "Collective or share not found", + "description": "Collective or static site not found", "content": { "application/json": { "schema": { @@ -15597,6 +16462,14 @@ "name": "share", "description": "Provides access to collective and page shares." }, + { + "name": "static_site", + "description": "Provides access to static site exports of a collective." + }, + { + "name": "static_site_generator", + "description": "Provides admins access to the external static site generator." + }, { "name": "search", "description": "Allows to search collectives." diff --git a/playwright/e2e/admin-settings.spec.ts b/playwright/e2e/admin-settings.spec.ts index 6e08d782f6..8b96a770ff 100644 --- a/playwright/e2e/admin-settings.spec.ts +++ b/playwright/e2e/admin-settings.spec.ts @@ -12,6 +12,7 @@ import { expect, test } from '@playwright/test' test.describe('Admin settings', () => { let defaultUserFolderInput: Locator let defaultUserFolderHelperText: Locator + let publishFeatureSwitch: Locator test.beforeEach(async ({ page }) => { const admin: User = { @@ -23,6 +24,7 @@ test.describe('Admin settings', () => { await page.goto('/index.php/settings/admin/additional') defaultUserFolderInput = page.getByRole('textbox', { name: 'Default user folder' }) defaultUserFolderHelperText = page.locator('#defaultUserFolder-helper-text') + publishFeatureSwitch = page.getByLabel('Enable publish feature') await expect(defaultUserFolderInput).toBeVisible() await expect(defaultUserFolderHelperText).not.toBeVisible() }) @@ -54,4 +56,72 @@ test.describe('Admin settings', () => { await defaultUserFolderInput.blur() await requestPromise }) + + test('Publish feature can be enabled and disabled', async ({ page }) => { + await publishFeatureSwitch.setChecked(true, { force: true }) + await page.waitForTimeout(100) + await page.reload() + publishFeatureSwitch = page.getByLabel('Enable publish feature') + await expect(publishFeatureSwitch).toBeChecked() + + await publishFeatureSwitch.setChecked(false, { force: true }) + await page.waitForTimeout(100) + await page.reload() + publishFeatureSwitch = page.getByLabel('Enable publish feature') + await expect(publishFeatureSwitch).not.toBeChecked() + }) + + test('Static site generator settings', async ({ page }) => { + const urlInput = page.getByRole('textbox', { name: 'Static site generator URL' }) + const urlHelperText = page.locator('#staticSiteGeneratorUrl-helper-text') + const tokenInput = page.getByLabel('Static site generator token', { exact: true }) + + await test.step('are only shown with enabled publish feature', async () => { + await publishFeatureSwitch.setChecked(false, { force: true }) + await expect(urlInput).not.toBeVisible() + await expect(tokenInput).not.toBeVisible() + + await publishFeatureSwitch.setChecked(true, { force: true }) + await expect(urlInput).toBeVisible() + await expect(tokenInput).toHaveValue('') + }) + + await test.step('warn about an invalid URL and save a valid one', async () => { + await urlInput.fill('ssg.example.org') + await expect(urlHelperText).toBeVisible() + + // Fails fast in the connection test below, as nothing listens on this port + await urlInput.fill('http://localhost:1') + await expect(urlHelperText).not.toBeVisible() + const responsePromise = page.waitForResponse(/static_site_generator_url/) + await urlInput.blur() + expect((await responsePromise).ok()).toBe(true) + }) + + await test.step('save the token and only show a placeholder afterwards', async () => { + await tokenInput.fill('secret-token') + const responsePromise = page.waitForResponse(/static_site_generator_token/) + await tokenInput.blur() + expect((await responsePromise).ok()).toBe(true) + + await page.reload() + await expect(tokenInput).toHaveValue('********') + expect(await page.content()).not.toContain('secret-token') + + let tokenRequestSent = false + page.on('request', (request) => { + tokenRequestSent ||= request.url().includes('static_site_generator_token') + }) + await tokenInput.focus() + await tokenInput.blur() + await page.waitForTimeout(200) + expect(tokenRequestSent).toBe(false) + }) + + await test.step('show the connection test result below the button', async () => { + await page.getByRole('button', { name: 'Test connection' }).click() + await expect(page.getByRole('status').filter({ hasText: 'The static site generator is not reachable' })) + .toBeVisible() + }) + }) }) diff --git a/playwright/e2e/collective-publish.spec.ts b/playwright/e2e/collective-publish.spec.ts new file mode 100644 index 0000000000..c5ae4a2ab8 --- /dev/null +++ b/playwright/e2e/collective-publish.spec.ts @@ -0,0 +1,142 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { Page } from '@playwright/test' + +import { runOcc } from '@nextcloud/e2e-test-server/docker' +import { expect, mergeTests } from '@playwright/test' +import { test as createCollectivesTest } from '../support/fixtures/create-collectives.ts' +import { test as navigationTest } from '../support/fixtures/navigation.ts' +import { loginAsUser } from '../support/fixtures/random-user.ts' +import { User } from '../support/fixtures/User.ts' + +type MemberFixture = { page: Page, user: User } + +const baseTest = mergeTests(createCollectivesTest, navigationTest) + +// Extend fixture to add a logged-in member user for permission tests +const test = baseTest.extend<{ member: MemberFixture }>({ + member: async ({ collective, browser, baseURL }, use) => { + const account = await collective.addMember() + const memberPage = await loginAsUser(browser, baseURL, account) + await use({ page: memberPage, user: new User(account) }) + await memberPage.close() + }, +}) + +test.beforeAll(async () => { + await runOcc(['config:app:set', 'collectives', 'publish_enabled', '--value', 'true']) +}) + +test.describe('Collective publish', () => { + test('admin can open the publish modal and publish the collective', async ({ collective, navigation, page }) => { + await collective.openCollective() + const modal = page.getByRole('dialog') + + await test.step('open and close the publish modal', async () => { + await page.getByRole('button', { name: 'Collective actions' }).click() + + // Publish button is visible for admin + const publishButton = page.locator('.action-item__popper:visible') + .getByRole('button', { name: 'Publish website', exact: true }) + await expect(publishButton).toBeVisible() + + // Publish modal opens on publish button click + await publishButton.click() + await expect(modal + .filter({ has: page.getByRole('heading', { name: `Publish website for Collective ${collective.data.name}` }) })) + .toBeVisible() + + // Modal can be closed + await modal.getByRole('button', { name: 'Close' }).click() + await expect(modal).toHaveCount(0) + }) + + await test.step('close the publish modal with Escape', async () => { + await navigation.clickCollectiveMenu(collective.data.name, 'Publish website') + await expect(modal).toBeVisible() + + // Escape must also work while a checkbox has the focus + const checkbox = modal.getByRole('checkbox').first() + await checkbox.focus() + await expect(checkbox).toBeFocused() + await page.keyboard.press('Escape') + await expect(modal).toHaveCount(0) + }) + + // TODO(Publish): Remove SKIP once the CI provides a stub of the publish service, publishing fails without it + await test.step.skip('reopen the publish modal and publish', async () => { + // Intercept the API call to verify it is sent and succeeds + const requestPromise = page.waitForRequest((req) => req.url().includes('/static-sites') && req.method() === 'POST') + + // Modal can be opened again + await navigation.clickCollectiveMenu(collective.data.name, 'Publish website') + await expect(modal).toBeVisible() + await expect(modal).toContainText(collective.data.name) + + // Title and web address are prefilled from the collective name + const titleField = modal.getByRole('textbox', { name: 'Website title' }) + const slugField = modal.getByRole('textbox', { name: 'Web address' }) + const submitButton = modal.getByRole('button', { name: 'Publish website' }) + await expect(titleField).toHaveValue(collective.data.name) + const suggestedSlug = await slugField.inputValue() + expect(suggestedSlug).toMatch(/^[a-z0-9]+(-[a-z0-9]+)*$/) + + // Invalid web address prevents publishing + await slugField.fill('Not a valid slug') + await expect(submitButton).toBeDisabled() + await slugField.fill(suggestedSlug) + + // All pages are pre-selected; click publish + await submitButton.click() + + // The request must reach the backend and provide the website files + const response = await (await requestPromise).response() + expect(response).not.toBeNull() + expect(response!.status()).toBe(200) + expect((await response!.json()).ocs.data.status).toBe('provided') + + // Modal closes after successful submission + await expect(modal).toHaveCount(0) + await expect(page.locator('.toast-success')).toContainText('Website publishing started') + }) + }) + + test('keeps the modal open if the website files could not be provided', async ({ collective, navigation, page }) => { + // A failed archive build can't be provoked reliably, so the response is replaced + await page.route('**/static-sites', async (route) => { + if (route.request().method() !== 'POST') { + return route.fallback() + } + await route.fulfill({ status: 500, json: { ocs: { meta: { status: 'failure', statuscode: 500, message: '' }, data: [] } } }) + }) + + await collective.openCollective() + await navigation.clickCollectiveMenu(collective.data.name, 'Publish website') + const modal = page.getByRole('dialog') + const titleField = modal.getByRole('textbox', { name: 'Website title' }) + await titleField.fill('My website') + await modal.getByRole('button', { name: 'Publish website' }).click() + + await expect(page.locator('.toast-error')).toContainText('Could not publish collective as website') + // Entered values remain for another attempt + await expect(modal).toBeVisible() + await expect(titleField).toHaveValue('My website') + }) + + test('regular members cannot see publish button', async ({ collective, member }) => { + await member.page.goto(`/index.php/apps/collectives/${collective.getCollectiveUrlPart()}`) + + // Open the current collective's actions menu + await member.page.getByRole('button', { name: 'Collective actions' }).click() + + // Actions menu is visible for regular member, but without the publish button + const actionsMenu = member.page.locator('.action-item__popper:visible') + await expect(actionsMenu).toBeVisible() + + const publishButton = actionsMenu.getByRole('button', { name: 'Publish website', exact: true }) + await expect(publishButton).toHaveCount(0) + }) +}) diff --git a/src/CollectivesApp.vue b/src/CollectivesApp.vue index f759e91e1d..00f445dc28 100644 --- a/src/CollectivesApp.vue +++ b/src/CollectivesApp.vue @@ -41,6 +41,7 @@ + + diff --git a/src/components/Nav/CollectiveSelector.vue b/src/components/Nav/CollectiveSelector.vue index e14f4a2944..cb2714e780 100644 --- a/src/components/Nav/CollectiveSelector.vue +++ b/src/components/Nav/CollectiveSelector.vue @@ -95,6 +95,10 @@ v-if="showCollectiveMembersModal" :collective="membersCollective" @close="onCloseCollectiveMembersModal" /> + @@ -117,6 +121,7 @@ import CollectivesIcon from '../Icon/CollectivesIcon.vue' import SkeletonLoading from '../SkeletonLoading.vue' import CollectiveListItem from './CollectiveListItem.vue' import CollectiveMembersModal from './CollectiveMembersModal.vue' +import CollectivePublishModal from './CollectivePublishModal.vue' import CollectivesGlobalSettings from './CollectivesGlobalSettings.vue' import CollectivesTrash from './CollectivesTrash.vue' import TemplatesDialog from './TemplatesDialog.vue' @@ -132,6 +137,7 @@ export default { ChevronDownIcon, CollectiveListItem, CollectiveMembersModal, + CollectivePublishModal, CollectivesGlobalSettings, CollectivesIcon, CollectivesTrash, @@ -176,6 +182,7 @@ export default { 'collectivePath', 'currentCollective', 'membersCollective', + 'publishCollective', 'sortedCollectives', 'templatesCollectiveId', ]), @@ -183,6 +190,10 @@ export default { showCollectiveMembersModal() { return !!this.membersCollective }, + + showCollectivePublishModal() { + return !!this.publishCollective + }, }, methods: { @@ -192,6 +203,7 @@ export default { 'deleteCollective', 'restoreCollective', 'setMembersCollectiveId', + 'setPublishCollectiveId', ]), onTriggerClick() { @@ -220,6 +232,10 @@ export default { onCloseCollectiveMembersModal() { this.setMembersCollectiveId(null) }, + + onCloseCollectivePublishModal() { + this.setPublishCollectiveId(null) + }, }, } diff --git a/src/components/Nav/PublishPageTreeItem.vue b/src/components/Nav/PublishPageTreeItem.vue new file mode 100644 index 0000000000..ac2f655ec4 --- /dev/null +++ b/src/components/Nav/PublishPageTreeItem.vue @@ -0,0 +1,113 @@ + + + + + + + diff --git a/src/components/Nav/PublishTreeRow.vue b/src/components/Nav/PublishTreeRow.vue new file mode 100644 index 0000000000..7bb90d6829 --- /dev/null +++ b/src/components/Nav/PublishTreeRow.vue @@ -0,0 +1,135 @@ + + + + + + + diff --git a/src/stores/collectives.js b/src/stores/collectives.js index f57b8929a0..dc793887dc 100644 --- a/src/stores/collectives.js +++ b/src/stores/collectives.js @@ -27,6 +27,7 @@ export const useCollectivesStore = defineStore('collectives', { templatesCollectiveId: undefined, membersCollectiveId: undefined, settingsCollectiveId: undefined, + publishCollectiveId: undefined, }), getters: { @@ -176,6 +177,12 @@ export const useCollectivesStore = defineStore('collectives', { isFavoritePage: (state) => (id, pageId) => { return state.collectives.find((c) => c.id === id).userFavoritePages.includes(pageId) }, + + publishCollective(state) { + return state.publishCollectiveId + ? state.collectives.find((c) => c.id === state.publishCollectiveId) + : null + }, }, actions: { @@ -195,6 +202,10 @@ export const useCollectivesStore = defineStore('collectives', { this.lastVisitedCollectiveId = id }, + setPublishCollectiveId(id) { + this.publishCollectiveId = id + }, + /** * Get list of all collectives */ diff --git a/src/stores/root.js b/src/stores/root.js index eff91e8852..397300d4ca 100644 --- a/src/stores/root.js +++ b/src/stores/root.js @@ -24,6 +24,8 @@ export const useRootStore = defineStore('root', { fileIdQuery: '', listenPush: false, isGuest: loadState('collectives', 'is_guest', false), + // Safety default - actual value gets loaded from backend by setPublishFeatureEnabled() + isPublishFeatureEnabled: false, }), getters: { @@ -61,5 +63,7 @@ export const useRootStore = defineStore('root', { setPrintView() { this.printView = true }, setActiveSidebarTab(id) { this.activeSidebarTab = id }, + + setPublishFeatureEnabled(enabled) { this.isPublishFeatureEnabled = enabled }, }, }) diff --git a/src/tests/util/staticSiteGeneratorUrl.spec.js b/src/tests/util/staticSiteGeneratorUrl.spec.js new file mode 100644 index 0000000000..477b133054 --- /dev/null +++ b/src/tests/util/staticSiteGeneratorUrl.spec.js @@ -0,0 +1,28 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { describe, expect, test } from 'vitest' +import { isValidStaticSiteGeneratorUrl } from '../../util/staticSiteGeneratorUrl.js' + +describe('isValidStaticSiteGeneratorUrl', () => { + test.each([ + '', + 'https://ssg.example.org', + 'https://ssg.example.org/', + 'http://localhost:8080/ssg', + ])('accepts %j', (url) => { + expect(isValidStaticSiteGeneratorUrl(url)).toBe(true) + }) + + test.each([ + 'ssg.example.org', + 'ftp://ssg.example.org', + 'https://', + 'https://ssg example.org', + ' https://ssg.example.org', + ])('rejects %j', (url) => { + expect(isValidStaticSiteGeneratorUrl(url)).toBe(false) + }) +}) diff --git a/src/tests/util/staticSiteSlug.spec.js b/src/tests/util/staticSiteSlug.spec.js new file mode 100644 index 0000000000..c000f0179c --- /dev/null +++ b/src/tests/util/staticSiteSlug.spec.js @@ -0,0 +1,38 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { describe, expect, test } from 'vitest' +import { generateSlug, isValidSlug } from '../../util/staticSiteSlug.js' + +describe('generateSlug', () => { + test.each([ + ['My Collective', 'my-collective'], + ['Über uns & Café', 'uber-uns-cafe'], + [' --Team 2026-- ', 'team-2026'], + ['🎉 Party 🎉', 'party'], + ['🎉', ''], + ])('%s -> %s', (name, slug) => { + expect(generateSlug(name)).toBe(slug) + }) + + test('truncates to 64 characters without trailing hyphen', () => { + const slug = generateSlug('a'.repeat(63) + ' b') + expect(slug).toBe('a'.repeat(63)) + }) + + test('generated slugs are valid', () => { + expect(isValidSlug(generateSlug('Some Name 42'))).toBe(true) + }) +}) + +describe('isValidSlug', () => { + test.each(['abc', 'a-b-c', 'team-2026'])('accepts %s', (slug) => { + expect(isValidSlug(slug)).toBe(true) + }) + + test.each(['', 'My-Site', 'my site', 'my_site', 'über', '-abc', 'abc-', 'a--b', 'a'.repeat(65)])('rejects %s', (slug) => { + expect(isValidSlug(slug)).toBe(false) + }) +}) diff --git a/src/util/staticSiteGeneratorUrl.js b/src/util/staticSiteGeneratorUrl.js new file mode 100644 index 0000000000..04131ca254 --- /dev/null +++ b/src/util/staticSiteGeneratorUrl.js @@ -0,0 +1,14 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +const URL_PATTERN = /^https?:\/\/\S+$/ + +/** + * @param {string} url the static site generator URL to check + * @return {boolean} whether the URL is empty (not configured) or an HTTP(S) URL + */ +export function isValidStaticSiteGeneratorUrl(url) { + return url === '' || URL_PATTERN.test(url) +} diff --git a/src/util/staticSiteSlug.js b/src/util/staticSiteSlug.js new file mode 100644 index 0000000000..07813d97a7 --- /dev/null +++ b/src/util/staticSiteSlug.js @@ -0,0 +1,33 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +export const SLUG_MAX_LENGTH = 64 + +const SLUG_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/ + +/** + * Suggest a static site slug based on a name + * + * @param {string} name the name to derive the slug from + * @return {string} lowercase ASCII letters and numbers, separated by single hyphens + */ +export function generateSlug(name) { + return name + .normalize('NFKD') + .replace(/\p{Diacritic}/gu, '') + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') + .slice(0, SLUG_MAX_LENGTH) + .replace(/-+$/, '') +} + +/** + * @param {string} slug the slug to check + * @return {boolean} whether the slug is accepted by the backend + */ +export function isValidSlug(slug) { + return slug.length <= SLUG_MAX_LENGTH && SLUG_PATTERN.test(slug) +} diff --git a/src/views/AdminSettings.vue b/src/views/AdminSettings.vue index 27422ec8dc..516cc7eccc 100644 --- a/src/views/AdminSettings.vue +++ b/src/views/AdminSettings.vue @@ -17,19 +17,83 @@ @keydown.enter="saveDefaultUserFolder" @blur="saveDefaultUserFolder" /> + +
+ + {{ t('collectives', 'Enable publish feature') }} + + +
+
+ + diff --git a/tests/Integration/features/bootstrap/FeatureContext.php b/tests/Integration/features/bootstrap/FeatureContext.php index 242c113454..adc8cc8a9f 100644 --- a/tests/Integration/features/bootstrap/FeatureContext.php +++ b/tests/Integration/features/bootstrap/FeatureContext.php @@ -881,6 +881,93 @@ public function userSetsSetting(string $user, string $key, string $value, ?strin } } + /** + * @When user :user publishes collective :collective as website with slug :slug + * + * @throws GuzzleException + */ + public function userCreatesStaticSite(string $user, string $collective, string $slug): void { + $this->setCurrentUser($user); + $collectiveId = $this->collectiveIdByName($collective); + $pageIds = $this->allPageIds($collectiveId); + + $this->sendOcsCollectivesRequest('POST', 'collectives/' . $collectiveId . '/static-sites', null, ['pageIds' => $pageIds, 'slug' => $slug]); + $this->assertStatusCode(200); + $data = $this->getJson()['ocs']['data']; + Assert::assertSame('provided', $data['status']); + Assert::assertSame($slug, $data['slug']); + Assert::assertSame($pageIds, $data['selectedPageIds']); + } + + /** + * @When user :user fails to publish collective :collective as website with slug :slug with status :statusCode + * + * @throws GuzzleException + */ + public function userFailsToCreateStaticSite(string $user, string $collective, string $slug, int $statusCode): void { + $this->setCurrentUser($user); + $collectiveId = $this->collectiveIdByName($collective); + $pageIds = $this->allPageIds($collectiveId); + + $this->sendOcsCollectivesRequest('POST', 'collectives/' . $collectiveId . '/static-sites', null, ['pageIds' => $pageIds, 'slug' => $slug]); + $this->assertStatusCode($statusCode); + } + + /** + * @When user :user republishes website :slug of collective :collective with title :title + * + * @throws GuzzleException + */ + public function userUpdatesStaticSite(string $user, string $slug, string $collective, string $title): void { + $this->setCurrentUser($user); + $collectiveId = $this->collectiveIdByName($collective); + $staticSiteId = $this->staticSiteIdBySlug($collectiveId, $slug); + $pageIds = $this->allPageIds($collectiveId); + + $this->sendOcsCollectivesRequest('PUT', 'collectives/' . $collectiveId . '/static-sites/' . $staticSiteId, null, ['pageIds' => $pageIds, 'title' => $title]); + $this->assertStatusCode(200); + $data = $this->getJson()['ocs']['data']; + Assert::assertSame('provided', $data['status']); + Assert::assertSame($title, $data['title']); + Assert::assertSame($slug, $data['slug']); + } + + /** + * @When user :user deletes website :slug of collective :collective + * + * @throws GuzzleException + */ + public function userDeletesStaticSite(string $user, string $slug, string $collective): void { + $this->setCurrentUser($user); + $collectiveId = $this->collectiveIdByName($collective); + $staticSiteId = $this->staticSiteIdBySlug($collectiveId, $slug); + + $this->sendOcsCollectivesRequest('DELETE', 'collectives/' . $collectiveId . '/static-sites/' . $staticSiteId); + $this->assertStatusCode(200); + } + + /** + * @Then user :user sees website :slug in collective :collective + * + * @throws GuzzleException + */ + public function userSeesStaticSite(string $user, string $slug, string $collective): void { + $this->setCurrentUser($user); + $collectiveId = $this->collectiveIdByName($collective); + Assert::assertNotNull($this->staticSiteIdBySlug($collectiveId, $slug, false)); + } + + /** + * @Then user :user doesn't see website :slug in collective :collective + * + * @throws GuzzleException + */ + public function userDoesntSeeStaticSite(string $user, string $slug, string $collective): void { + $this->setCurrentUser($user); + $collectiveId = $this->collectiveIdByName($collective); + Assert::assertNull($this->staticSiteIdBySlug($collectiveId, $slug, false)); + } + /** * @When we wait for :seconds seconds */ @@ -2281,6 +2368,34 @@ private function collectiveIdByName(string $name, bool $trash = false): ?int { return null; } + /** + * @return list + * + * @throws GuzzleException + */ + private function allPageIds(int $collectiveId): array { + $this->sendOcsCollectivesRequest('GET', 'collectives/' . $collectiveId . '/pages'); + $this->assertStatusCode(200); + return array_map(static fn (array $page): int => $page['id'], $this->getJson()['ocs']['data']['pages']); + } + + /** + * @throws GuzzleException + */ + private function staticSiteIdBySlug(int $collectiveId, string $slug, bool $required = true): ?int { + $this->sendOcsCollectivesRequest('GET', 'collectives/' . $collectiveId . '/static-sites'); + $this->assertStatusCode(200); + foreach ($this->getJson()['ocs']['data'] as $staticSite) { + if ($slug === $staticSite['slug']) { + return $staticSite['id']; + } + } + if ($required) { + throw new RuntimeException('Unable to find website ' . $slug . ' in collective ' . $collectiveId); + } + return null; + } + /** * @throws GuzzleException */ diff --git a/tests/Integration/features/staticSites.feature b/tests/Integration/features/staticSites.feature new file mode 100644 index 0000000000..6951bdb5ee --- /dev/null +++ b/tests/Integration/features/staticSites.feature @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +Feature: staticSites + + # TODO(Publish): Enable the commented steps and scenarios once the CI provides a stub of the publish service, publishing fails without it + Scenario: Publish a collective as website + When user "jane" creates collective "BehatStaticSiteCollective" + # And user "jane" publishes collective "BehatStaticSiteCollective" as website with slug "behat-static-site" + # Then user "jane" sees website "behat-static-site" in collective "BehatStaticSiteCollective" + # TODO(Publish): Download the archive and check its content once it's provided via public API + + # Scenario: Fail to publish a website with a slug in use + # Then user "jane" fails to publish collective "BehatStaticSiteCollective" as website with slug "behat-static-site" with status 400 + + Scenario: Fail to publish a website with an invalid slug + Then user "jane" fails to publish collective "BehatStaticSiteCollective" as website with slug "Invalid Slug" with status 400 + + # Scenario: Republish a website with a new title + # When user "jane" republishes website "behat-static-site" of collective "BehatStaticSiteCollective" with title "New website title" + + # Scenario: Delete a website + # When user "jane" deletes website "behat-static-site" of collective "BehatStaticSiteCollective" + # Then user "jane" doesn't see website "behat-static-site" in collective "BehatStaticSiteCollective" + + Scenario: Trash and delete collective + Then user "jane" trashes and deletes collective "BehatStaticSiteCollective" diff --git a/tests/Unit/ConfigLexiconTest.php b/tests/Unit/ConfigLexiconTest.php index f8663a721e..88491af98c 100644 --- a/tests/Unit/ConfigLexiconTest.php +++ b/tests/Unit/ConfigLexiconTest.php @@ -12,6 +12,7 @@ use OCA\Collectives\ConfigLexicon; use OCP\Config\Lexicon\Entry; use OCP\Config\Lexicon\Preset; +use OCP\IAppConfig; use Test\TestCase; class ConfigLexiconTest extends TestCase { @@ -35,4 +36,13 @@ public function testUserFolderHasNoLexiconDefault(): void { self::assertCount(1, $entries); self::assertNull(array_values($entries)[0]->getDefault(Preset::NONE)); } + + public function testStaticSiteGeneratorTokenIsSensitive(): void { + $entries = array_filter( + $this->lexicon->getAppConfigs(), + static fn (Entry $e) => $e->getKey() === ConfigLexicon::STATIC_SITE_GENERATOR_TOKEN, + ); + self::assertCount(1, $entries); + self::assertTrue(array_values($entries)[0]->isFlagged(IAppConfig::FLAG_SENSITIVE)); + } } diff --git a/tests/Unit/Controller/StaticSiteArchiveControllerTest.php b/tests/Unit/Controller/StaticSiteArchiveControllerTest.php new file mode 100644 index 0000000000..1fa6f37781 --- /dev/null +++ b/tests/Unit/Controller/StaticSiteArchiveControllerTest.php @@ -0,0 +1,77 @@ +createMock(IRequest::class); + $request->method('getHeader')->with('Authorization')->willReturn('Bearer secret'); + $this->staticSiteService = $this->createMock(StaticSiteService::class); + $this->staticSiteGeneratorClient = $this->createMock(StaticSiteGeneratorClient::class); + + $this->controller = new StaticSiteArchiveController('collectives', $request, $this->staticSiteService, $this->staticSiteGeneratorClient); + } + + public function testDownloadRejectsInvalidTokenAndThrottles(): void { + $this->staticSiteGeneratorClient->method('isValidToken')->with('Bearer secret')->willReturn(false); + $this->staticSiteService->expects($this->never())->method('getArchive'); + + $response = $this->controller->download($this->staticSiteId); + + $this->assertSame(Http::STATUS_UNAUTHORIZED, $response->getStatus()); + $this->assertTrue($response->isThrottled()); + } + + public function testDownloadReturnsNotFoundForUnknownArchive(): void { + $this->staticSiteGeneratorClient->method('isValidToken')->willReturn(true); + $this->staticSiteService->method('getArchive') + ->with($this->staticSiteId) + ->willThrowException(new NotFoundException('Static site archive not found')); + + $response = $this->controller->download($this->staticSiteId); + + $this->assertSame(Http::STATUS_NOT_FOUND, $response->getStatus()); + $this->assertFalse($response->isThrottled()); + } + + public function testDownloadReturnsArchiveAsAttachment(): void { + $archive = $this->createMock(ISimpleFile::class); + $archive->method('getName')->willReturn($this->staticSiteId . '.tar.gz'); + $this->staticSiteGeneratorClient->method('isValidToken')->willReturn(true); + $this->staticSiteService->method('getArchive')->with($this->staticSiteId)->willReturn($archive); + + $response = $this->controller->download($this->staticSiteId); + + $this->assertInstanceOf(FileDisplayResponse::class, $response); + $this->assertSame(Http::STATUS_OK, $response->getStatus()); + $headers = $response->getHeaders(); + $this->assertSame('application/gzip', $headers['Content-Type']); + $this->assertSame('attachment; filename="' . $this->staticSiteId . '.tar.gz"', $headers['Content-Disposition']); + } +} diff --git a/tests/Unit/Controller/StaticSiteControllerTest.php b/tests/Unit/Controller/StaticSiteControllerTest.php new file mode 100644 index 0000000000..4803686906 --- /dev/null +++ b/tests/Unit/Controller/StaticSiteControllerTest.php @@ -0,0 +1,133 @@ +staticSiteService = $this->createMock(StaticSiteService::class); + $this->logger = $this->createMock(LoggerInterface::class); + $l10n = $this->createMock(IL10N::class); + $l10n->method('t')->willReturnArgument(0); + + $this->controller = new StaticSiteController( + 'collectives', + $this->createMock(IRequest::class), + $this->staticSiteService, + $this->logger, + $l10n, + 'jane', + ); + } + + public function testCreateReturnsStaticSite(): void { + $staticSite = new StaticSite(); + $this->staticSiteService->method('create') + ->with(42, [1], 'jane', 'Title', 'slug') + ->willReturn($staticSite); + + $this->assertSame($staticSite, $this->controller->create(42, [1], 'Title', 'slug')->getData()); + } + + public function testCreateLogsArchiveFailureWithContext(): void { + $exception = new ServiceException('Failed to provide static site archive'); + $this->staticSiteService->method('create')->willThrowException($exception); + $this->logger->expects($this->once()) + ->method('error') + ->with('Failed to provide static site archive', ['collectiveId' => 42, 'exception' => $exception]); + + try { + $this->controller->create(42, [1]); + $this->fail('Expected OCSException'); + } catch (OCSException $e) { + $this->assertSame(Http::STATUS_INTERNAL_SERVER_ERROR, $e->getCode()); + $this->assertSame('The website could not be prepared. Please contact your administrator.', $e->getMessage()); + } + } + + public function testUpdateLogsArchiveFailureWithContext(): void { + $exception = new ServiceException('Failed to provide static site archive'); + $this->staticSiteService->method('update')->willThrowException($exception); + $this->logger->expects($this->once()) + ->method('error') + ->with('Failed to provide static site archive', ['collectiveId' => 42, 'id' => 7, 'exception' => $exception]); + + $this->expectException(OCSException::class); + $this->controller->update(42, 7, [1], 'Title'); + } + + public static function generatorExceptionProvider(): array { + return [ + 'not configured' => [ + new StaticSiteGeneratorNotConfiguredException('Not configured'), + Http::STATUS_SERVICE_UNAVAILABLE, + 'Publishing websites is not set up. Please contact your administrator.', + ], + 'unavailable' => [ + new StaticSiteGeneratorUnavailableException('Not reachable'), + Http::STATUS_SERVICE_UNAVAILABLE, + 'The publish service is currently not available. Please try again later.', + ], + 'rejected' => [ + new StaticSiteGeneratorException('Rejected'), + Http::STATUS_BAD_GATEWAY, + 'The publish service rejected the website. Please contact your administrator.', + ], + ]; + } + + /** + * @dataProvider generatorExceptionProvider + */ + public function testCreateTellsWhetherTryingAgainMayHelpIfGeneratorFails(StaticSiteGeneratorException $exception, int $statusCode, string $message): void { + $this->staticSiteService->method('create')->willThrowException($exception); + $this->logger->expects($this->never())->method('error'); + + try { + $this->controller->create(42, [1]); + $this->fail('Expected OCSException'); + } catch (OCSException $e) { + $this->assertSame($statusCode, $e->getCode()); + $this->assertSame($message, $e->getMessage()); + } + } + + public function testCreateKeepsBadRequestForUnprocessableContent(): void { + $this->staticSiteService->method('create') + ->willThrowException(new UnprocessableEntityException('There is no content to publish.')); + $this->logger->expects($this->never())->method('error'); + + $this->expectException(OCSBadRequestException::class); + $this->expectExceptionMessage('There is no content to publish.'); + $this->controller->create(42, [1]); + } +} diff --git a/tests/Unit/Controller/StaticSiteGeneratorControllerTest.php b/tests/Unit/Controller/StaticSiteGeneratorControllerTest.php new file mode 100644 index 0000000000..3db878f368 --- /dev/null +++ b/tests/Unit/Controller/StaticSiteGeneratorControllerTest.php @@ -0,0 +1,54 @@ +client = $this->createMock(StaticSiteGeneratorClient::class); + $this->controller = new StaticSiteGeneratorController( + 'collectives', + $this->createMock(IRequest::class), + $this->client, + ); + } + + public function testHealth(): void { + $this->client->expects(self::once())->method('checkHealth'); + + $response = $this->controller->health(); + + self::assertSame(Http::STATUS_OK, $response->getStatus()); + self::assertSame(['status' => 'ok'], $response->getData()); + } + + public function testHealthFailed(): void { + $this->client->method('checkHealth') + ->willThrowException(new StaticSiteGeneratorException('The static site generator is not reachable')); + + $response = $this->controller->health(); + + self::assertSame(Http::STATUS_BAD_GATEWAY, $response->getStatus()); + self::assertSame(['message' => 'The static site generator is not reachable'], $response->getData()); + } +} diff --git a/tests/Unit/Db/StaticSiteMapperTest.php b/tests/Unit/Db/StaticSiteMapperTest.php new file mode 100644 index 0000000000..7651f1b45b --- /dev/null +++ b/tests/Unit/Db/StaticSiteMapperTest.php @@ -0,0 +1,129 @@ + */ + private array $parameters = []; + /** @var list */ + private array $conditions = []; + /** @var array */ + private array $values = []; + + protected function setUp(): void { + parent::setUp(); + + $this->qb = $this->createMock(IQueryBuilder::class); + $this->qb->method('createNamedParameter')->willReturnCallback(function (mixed $value): string { + $name = ':param' . count($this->parameters); + $this->parameters[$name] = $value; + return $name; + }); + $expr = $this->createMock(IExpressionBuilder::class); + $expr->method('eq')->willReturnCallback( + fn (string $column, string $parameter): string => $column . ' = ' . var_export($this->parameters[$parameter], true) + ); + $this->qb->method('expr')->willReturn($expr); + $this->qb->method('update')->willReturnSelf(); + $this->qb->method('set')->willReturnCallback(function (string $column, string $parameter): IQueryBuilder { + $this->values[$column] = $this->parameters[$parameter]; + return $this->qb; + }); + $this->qb->method('where')->willReturnCallback(function (string $condition): IQueryBuilder { + $this->conditions[] = $condition; + return $this->qb; + }); + $this->qb->method('andWhere')->willReturnCallback(function (string $condition): IQueryBuilder { + $this->conditions[] = $condition; + return $this->qb; + }); + + $connection = $this->createMock(IDBConnection::class); + $connection->method('getQueryBuilder')->willReturn($this->qb); + $timeFactory = $this->createMock(ITimeFactory::class); + $timeFactory->method('getTime')->willReturn($this->now); + + $this->mapper = $this->getMockBuilder(StaticSiteMapper::class) + ->setConstructorArgs([$connection, $timeFactory]) + ->onlyMethods(['find']) + ->getMock(); + } + + private function makeStaticSite(): StaticSite { + $staticSite = new StaticSite(); + $staticSite->setId(7); + $staticSite->setStatus(StaticSite::STATUS_PUBLISHED); + $staticSite->setUpdatedAt(1700000000); + return $staticSite; + } + + public function testUpdateStatusIfUnchangedOnlyUpdatesUnchangedStaticSite(): void { + $this->qb->method('executeStatement')->willReturn(1); + + $this->assertTrue($this->mapper->updateStatusIfUnchanged($this->makeStaticSite(), StaticSite::STATUS_PENDING)); + + $this->assertSame([ + 'id = 7', + "status = 'published'", + 'updated_at = 1700000000', + ], $this->conditions); + $this->assertSame([ + 'status' => StaticSite::STATUS_PENDING, + 'updated_at' => $this->now, + ], $this->values); + } + + public function testUpdateStatusIfUnchangedReturnsFalseIfStaticSiteChangedMeanwhile(): void { + $this->qb->method('executeStatement')->willReturn(0); + + $this->assertFalse($this->mapper->updateStatusIfUnchanged($this->makeStaticSite(), StaticSite::STATUS_PENDING)); + } + + public function testUpdateSettingsAndStatusStoresTitlePagesAndStatus(): void { + $this->qb->method('executeStatement')->willReturn(1); + $provided = new StaticSite(); + $this->mapper->expects($this->once())->method('find')->with(7)->willReturn($provided); + + $this->assertSame($provided, $this->mapper->updateSettingsAndStatus($this->makeStaticSite(), 'Title', [3, 1], StaticSite::STATUS_FAILED)); + + $this->assertSame(['id = 7'], $this->conditions); + $this->assertSame([ + 'title' => 'Title', + 'selected_pages' => '[3,1]', + 'status' => StaticSite::STATUS_FAILED, + 'updated_at' => $this->now, + ], $this->values); + } + + public function testUpdateSettingsAndStatusThrowsIfStaticSiteWasDeleted(): void { + $this->qb->method('executeStatement')->willReturn(0); + $this->mapper->expects($this->never())->method('find'); + + $this->expectException(NotFoundException::class); + $this->mapper->updateSettingsAndStatus($this->makeStaticSite(), 'Title', [1], StaticSite::STATUS_PROVIDED); + } +} diff --git a/tests/Unit/Db/StaticSiteTest.php b/tests/Unit/Db/StaticSiteTest.php new file mode 100644 index 0000000000..92369b4543 --- /dev/null +++ b/tests/Unit/Db/StaticSiteTest.php @@ -0,0 +1,76 @@ +assertSame('[1,2,3]', StaticSite::encodePageIds([5 => 1, 7 => 2, 9 => 3])); + } + + public function testEncodePageIdsThrowsForUnencodableValues(): void { + $this->expectException(UnprocessableEntityException::class); + StaticSite::encodePageIds([NAN]); + } + + public static function inProgressProvider(): array { + $now = 1800000000; + return [ + 'recent pending' => [StaticSite::STATUS_PENDING, $now - 60, $now, true], + 'pending at timeout' => [StaticSite::STATUS_PENDING, $now - StaticSite::PENDING_TIMEOUT, $now, true], + 'aborted pending' => [StaticSite::STATUS_PENDING, $now - StaticSite::PENDING_TIMEOUT - 1, $now, false], + 'provided' => [StaticSite::STATUS_PROVIDED, $now - 60, $now, false], + 'published' => [StaticSite::STATUS_PUBLISHED, $now - 60, $now, false], + 'failed' => [StaticSite::STATUS_FAILED, $now - 60, $now, false], + ]; + } + + /** + * @dataProvider inProgressProvider + */ + public function testIsInProgress(string $status, int $updatedAt, int $now, bool $expected): void { + $staticSite = new StaticSite(); + $staticSite->setStatus($status); + $staticSite->setUpdatedAt($updatedAt); + + $this->assertSame($expected, $staticSite->isInProgress($now)); + } + + public static function corruptedSelectedPagesProvider(): array { + return [ + 'invalid JSON' => ['[1,'], + 'scalar' => ['5'], + 'string' => ['"page"'], + 'null' => ['null'], + ]; + } + + /** + * @dataProvider corruptedSelectedPagesProvider + */ + public function testGetSelectedPageIdsThrowsForCorruptedValue(string $selectedPages): void { + $staticSite = new StaticSite(); + $staticSite->setSelectedPages($selectedPages); + + $this->expectException(UnexpectedValueException::class); + $staticSite->getSelectedPageIds(); + } + + public function testSelectedPageIdsRoundTrip(): void { + $staticSite = new StaticSite(); + $staticSite->setSelectedPageIds([3, 1, 2]); + + $this->assertSame([3, 1, 2], $staticSite->getSelectedPageIds()); + } +} diff --git a/tests/Unit/Service/StaticSiteArchiverTest.php b/tests/Unit/Service/StaticSiteArchiverTest.php new file mode 100644 index 0000000000..0bc54f4f4d --- /dev/null +++ b/tests/Unit/Service/StaticSiteArchiverTest.php @@ -0,0 +1,309 @@ +tempFolder = sys_get_temp_dir() . '/collectives-archiver-test-' . bin2hex(random_bytes(4)); + mkdir($this->tempFolder); + + $this->appData = $this->createMock(IAppData::class); + $this->folder = $this->createMock(ISimpleFolder::class); + $this->tempManager = $this->createMock(ITempManager::class); + $this->tempManager->method('getTemporaryFolder')->willReturn($this->tempFolder); + + $this->l10n = $this->createMock(IL10N::class); + $this->l10n->method('t')->willReturnCallback(static fn (string $text, array $parameters = []): string => vsprintf($text, $parameters)); + + $this->archiver = new StaticSiteArchiver($this->appData, $this->tempManager, $this->l10n); + } + + protected function tearDown(): void { + if (is_dir($this->tempFolder)) { + exec('rm -rf ' . escapeshellarg($this->tempFolder)); + } + parent::tearDown(); + } + + private function makeFile(string $content, string $path = '/collective/file'): File&MockObject { + $file = $this->createMock(File::class); + $file->method('getPath')->willReturn($path); + $file->method('fopen')->willReturnCallback(static function () use ($content) { + $stream = fopen('php://memory', 'r+b'); + fwrite($stream, $content); + rewind($stream); + return $stream; + }); + return $file; + } + + private function captureContent(mixed $data): void { + $this->storedArchive = is_resource($data) ? stream_get_contents($data) : $data; + } + + /** + * @return array Archive paths mapped to their contents + */ + private function readStoredArchive(): array { + $this->assertNotNull($this->storedArchive); + $path = sys_get_temp_dir() . '/collectives-archiver-read-' . bin2hex(random_bytes(4)) . '.tar.gz'; + file_put_contents($path, $this->storedArchive); + try { + $prefix = 'phar://' . $path . '/'; + $entries = []; + foreach (new RecursiveIteratorIterator(new PharData($path)) as $entry) { + $entries[substr($entry->getPathname(), strlen($prefix))] = file_get_contents($entry->getPathname()); + } + ksort($entries); + return $entries; + } finally { + unlink($path); + } + } + + public function testStoreCreatesArchiveWithFilesAtTheirPaths(): void { + $this->appData->method('getFolder')->with('static_sites')->willReturn($this->folder); + $this->folder->method('fileExists')->with($this->staticSiteId . '.tar.gz')->willReturn(false); + $this->folder->expects($this->once()) + ->method('newFile') + ->with($this->staticSiteId . '.tar.gz', $this->anything()) + ->willReturnCallback(function (string $name, mixed $content) { + $this->captureContent($content); + return $this->createMock(ISimpleFile::class); + }); + + $this->archiver->store($this->staticSiteId, [ + 'Readme.md' => $this->makeFile('# Home'), + '.attachments.12/image.png' => $this->makeFile('png-data'), + 'Subpage/Readme.md' => $this->makeFile('# Sub'), + '2024' => $this->makeFile('numeric'), + ]); + + $this->assertSame([ + '.attachments.12/image.png' => 'png-data', + '2024' => 'numeric', + 'Readme.md' => '# Home', + 'Subpage/Readme.md' => '# Sub', + ], $this->readStoredArchive()); + $this->assertDirectoryDoesNotExist($this->tempFolder); + } + + public function testStoreReplacesExistingArchive(): void { + $existingFile = $this->createMock(ISimpleFile::class); + $existingFile->expects($this->once()) + ->method('putContent') + ->willReturnCallback(fn (mixed $content) => $this->captureContent($content)); + + $this->appData->method('getFolder')->willReturn($this->folder); + $this->folder->method('fileExists')->willReturn(true); + $this->folder->method('getFile')->with($this->staticSiteId . '.tar.gz')->willReturn($existingFile); + $this->folder->expects($this->never())->method('newFile'); + + $this->archiver->store($this->staticSiteId, ['Readme.md' => $this->makeFile('# New')]); + + $this->assertSame(['Readme.md' => '# New'], $this->readStoredArchive()); + } + + public function testStoreWrapsStorageErrors(): void { + $existingFile = $this->createMock(ISimpleFile::class); + $existingFile->method('putContent')->willThrowException(new GenericFileException('Write failed')); + + $this->appData->method('getFolder')->willReturn($this->folder); + $this->folder->method('fileExists')->willReturn(true); + $this->folder->method('getFile')->willReturn($existingFile); + + $this->expectException(ServiceException::class); + $this->archiver->store($this->staticSiteId, ['Readme.md' => $this->makeFile('# Home')]); + } + + public function testStoreCreatesMissingStaticSitesFolder(): void { + $this->appData->method('getFolder')->willThrowException(new FilesNotFoundException()); + $this->appData->expects($this->once()) + ->method('newFolder') + ->with('static_sites') + ->willReturn($this->folder); + $this->folder->method('fileExists')->willReturn(false); + $this->folder->expects($this->once())->method('newFile'); + + $this->archiver->store($this->staticSiteId, ['Readme.md' => $this->makeFile('# Home')]); + } + + public static function invalidPathProvider(): array { + return [ + 'empty' => [''], + 'absolute' => ['/etc/passwd'], + 'parent segment' => ['../Readme.md'], + 'nested parent segment' => ['Subpage/../../Readme.md'], + 'current segment' => ['./Readme.md'], + 'empty segment' => ['Subpage//Readme.md'], + 'backslash' => ['Subpage\\Readme.md'], + ]; + } + + /** + * @dataProvider invalidPathProvider + */ + public function testStoreRejectsInvalidPaths(string $path): void { + $this->appData->expects($this->never())->method('getFolder'); + $this->tempManager->expects($this->never())->method('getTemporaryFolder'); + + $this->expectException(ServiceException::class); + $this->archiver->store($this->staticSiteId, [$path => $this->makeFile('content')]); + } + + public function testStoreSplitsLongPathIntoPrefixAndName(): void { + $path = str_repeat('a', 150) . '/' . str_repeat('b', 90) . '.md'; + $this->appData->method('getFolder')->willReturn($this->folder); + $this->folder->method('fileExists')->willReturn(false); + $this->folder->method('newFile')->willReturnCallback(function (string $name, mixed $content) { + $this->captureContent($content); + return $this->createMock(ISimpleFile::class); + }); + + $this->archiver->store($this->staticSiteId, [$path => $this->makeFile('# Deep')]); + + $this->assertSame([$path => '# Deep'], $this->readStoredArchive()); + } + + public static function tooLongPathProvider(): array { + return [ + 'name without slash' => [str_repeat('a', 101)], + 'name after last slash' => ['Page/' . str_repeat('a', 101)], + 'prefix before slash' => [str_repeat('a', 160) . '/Readme.md'], + 'total length' => [str_repeat('a/', 128) . 'Readme.md'], + 'multibyte name' => [str_repeat('ä', 51)], + ]; + } + + /** + * @dataProvider tooLongPathProvider + */ + public function testStoreRejectsTooLongPaths(string $path): void { + $this->tempManager->expects($this->never())->method('getTemporaryFolder'); + + try { + $this->archiver->store($this->staticSiteId, [$path => $this->makeFile('content')]); + $this->fail('Expected UnprocessableEntityException'); + } catch (UnprocessableEntityException $e) { + $this->assertStringContainsString($path, $e->getMessage()); + } + } + + public function testStoreRejectsEmptyFileList(): void { + $this->tempManager->expects($this->never())->method('getTemporaryFolder'); + $this->appData->expects($this->never())->method('getFolder'); + + $this->expectException(UnprocessableEntityException::class); + $this->expectExceptionMessage('There is no content to publish.'); + $this->archiver->store($this->staticSiteId, []); + } + + public function testStoreFailsAndCleansUpIfFileCannotBeRead(): void { + $file = $this->createMock(File::class); + $file->method('fopen')->willReturn(false); + $this->appData->expects($this->never())->method('getFolder'); + + try { + $this->archiver->store($this->staticSiteId, ['Readme.md' => $file]); + $this->fail('Expected ServiceException'); + } catch (ServiceException) { + } + + $this->assertDirectoryDoesNotExist($this->tempFolder); + } + + public function testStoreFailsIfTemporaryFolderCannotBeCreated(): void { + $tempManager = $this->createMock(ITempManager::class); + $tempManager->method('getTemporaryFolder')->willReturn(false); + $archiver = new StaticSiteArchiver($this->appData, $tempManager, $this->l10n); + + $this->expectException(ServiceException::class); + $archiver->store($this->staticSiteId, ['Readme.md' => $this->makeFile('# Home')]); + } + + public function testGetReturnsArchive(): void { + $file = $this->createMock(ISimpleFile::class); + $this->appData->method('getFolder')->with('static_sites')->willReturn($this->folder); + $this->folder->method('getFile')->with($this->staticSiteId . '.tar.gz')->willReturn($file); + + $this->assertSame($file, $this->archiver->get($this->staticSiteId)); + } + + public function testGetThrowsIfArchiveDoesNotExist(): void { + $this->appData->method('getFolder')->willReturn($this->folder); + $this->folder->method('getFile')->willThrowException(new FilesNotFoundException()); + + $this->expectException(NotFoundException::class); + $this->archiver->get($this->staticSiteId); + } + + public function testGetThrowsIfStaticSitesFolderDoesNotExist(): void { + $this->appData->method('getFolder')->willThrowException(new FilesNotFoundException()); + $this->appData->expects($this->never())->method('newFolder'); + + $this->expectException(NotFoundException::class); + $this->archiver->get($this->staticSiteId); + } + + public function testDeleteRemovesArchive(): void { + $file = $this->createMock(ISimpleFile::class); + $file->expects($this->once())->method('delete'); + + $this->appData->method('getFolder')->willReturn($this->folder); + $this->folder->method('fileExists')->with($this->staticSiteId . '.tar.gz')->willReturn(true); + $this->folder->method('getFile')->willReturn($file); + + $this->archiver->delete($this->staticSiteId); + } + + public function testDeleteIgnoresMissingArchive(): void { + $this->appData->method('getFolder')->willReturn($this->folder); + $this->folder->method('fileExists')->willReturn(false); + $this->folder->expects($this->never())->method('getFile'); + + $this->archiver->delete($this->staticSiteId); + } + + public function testDeleteIgnoresMissingStaticSitesFolder(): void { + $this->appData->method('getFolder')->willThrowException(new FilesNotFoundException()); + $this->appData->expects($this->never())->method('newFolder'); + + $this->archiver->delete($this->staticSiteId); + } +} diff --git a/tests/Unit/Service/StaticSiteContentCollectorTest.php b/tests/Unit/Service/StaticSiteContentCollectorTest.php new file mode 100644 index 0000000000..c142c1d9bc --- /dev/null +++ b/tests/Unit/Service/StaticSiteContentCollectorTest.php @@ -0,0 +1,103 @@ +collectiveFolderManager = $this->createMock(CollectiveFolderManager::class); + $this->collector = new StaticSiteContentCollector($this->collectiveFolderManager); + } + + private function makeFile(string $name): File&MockObject { + $file = $this->createMock(File::class); + $file->method('getName')->willReturn($name); + return $file; + } + + /** + * @param list $children + */ + private function makeFolder(string $name, array $children): Folder&MockObject { + $folder = $this->createMock(Folder::class); + $folder->method('getName')->willReturn($name); + $folder->method('getDirectoryListing')->willReturn($children); + return $folder; + } + + public function testCollectReturnsAllFilesWithRelativePaths(): void { + $readme = $this->makeFile('Readme.md'); + $image = $this->makeFile('image.png'); + $subReadme = $this->makeFile('Readme.md'); + $pdf = $this->makeFile('file.pdf'); + $subSubPage = $this->makeFile('Page.md'); + + $this->collectiveFolderManager->method('getFolder') + ->with($this->collectiveId) + ->willReturn($this->makeFolder('42', [ + $readme, + $this->makeFolder('.attachments.12', [$image]), + $this->makeFolder('Subpage', [ + $subReadme, + $pdf, + $this->makeFolder('Empty', []), + $this->makeFolder('Subsubpage', [$subSubPage]), + ]), + ])); + + $this->assertSame([ + 'Readme.md' => $readme, + '.attachments.12/image.png' => $image, + 'Subpage/Readme.md' => $subReadme, + 'Subpage/file.pdf' => $pdf, + 'Subpage/Subsubpage/Page.md' => $subSubPage, + ], $this->collector->collect($this->collectiveId)); + } + + public function testCollectSkipsTemplateFolderOnlyAtTopLevel(): void { + $readme = $this->makeFile('Readme.md'); + $nestedTemplate = $this->makeFile('Readme.md'); + + $this->collectiveFolderManager->method('getFolder') + ->willReturn($this->makeFolder('42', [ + $readme, + $this->makeFolder('.templates', [$this->makeFile('Readme.md')]), + $this->makeFolder('Subpage', [ + $this->makeFolder('.templates', [$nestedTemplate]), + ]), + ])); + + $this->assertSame([ + 'Readme.md' => $readme, + 'Subpage/.templates/Readme.md' => $nestedTemplate, + ], $this->collector->collect($this->collectiveId)); + } + + public function testCollectThrowsIfCollectiveFolderIsMissing(): void { + $this->collectiveFolderManager->method('getFolder')->willThrowException(new FilesNotFoundException()); + + $this->expectException(FilesNotFoundException::class); + $this->collector->collect($this->collectiveId); + } +} diff --git a/tests/Unit/Service/StaticSiteGeneratorClientTest.php b/tests/Unit/Service/StaticSiteGeneratorClientTest.php new file mode 100644 index 0000000000..97d38f8d6f --- /dev/null +++ b/tests/Unit/Service/StaticSiteGeneratorClientTest.php @@ -0,0 +1,249 @@ +httpClient = $this->createMock(IClient::class); + $clientService = $this->createMock(IClientService::class); + $clientService->method('newClient')->willReturn($this->httpClient); + $appConfig = $this->createMock(IAppConfig::class); + $appConfig->method('getValueString') + ->willReturnCallback(fn (string $app, string $key): string => match ($key) { + ConfigLexicon::STATIC_SITE_GENERATOR_URL => $this->url, + ConfigLexicon::STATIC_SITE_GENERATOR_TOKEN => $this->token, + }); + $urlGenerator = $this->createMock(IURLGenerator::class); + $urlGenerator->method('linkToRouteAbsolute') + ->with('collectives.static_site_archive.download', ['apiVersion' => '1.0', 'staticSiteId' => $this->staticSiteId]) + ->willReturn('https://cloud.example.org/archive'); + $l10n = $this->createMock(IL10N::class); + $l10n->method('t')->willReturnCallback(static fn (string $text, array $parameters = []): string => vsprintf($text, $parameters)); + $this->logger = $this->createMock(LoggerInterface::class); + + $this->client = new StaticSiteGeneratorClient( + $clientService, + $appConfig, + $urlGenerator, + $l10n, + $this->logger, + ); + } + + private function makeStaticSite(): StaticSite { + $staticSite = new StaticSite(); + $staticSite->setStaticSiteId($this->staticSiteId); + $staticSite->setSlug('my-site'); + return $staticSite; + } + + private function makeResponse(int $statusCode, string $body): IResponse&MockObject { + $response = $this->createMock(IResponse::class); + $response->method('getStatusCode')->willReturn($statusCode); + $response->method('getBody')->willReturn($body); + return $response; + } + + // TODO(Publish): Change tests when ext service provides a test endpoint to validate url and shared secret + public function testCheckHealthSendsBearerToken(): void { + $this->httpClient->expects(self::once()) + ->method('request') + ->with('GET', 'https://ssg.example.org/health', self::callback(static fn (array $options): bool + => $options['headers']['Authorization'] === 'Bearer secret-token' + && $options['http_errors'] === false)) + ->willReturn($this->makeResponse(200, '{"status": "ok"}')); + + $this->client->checkHealth(); + } + + public static function notConfiguredProvider(): array { + return [ + 'no URL' => ['', 'secret-token'], + 'no token' => ['https://ssg.example.org', ''], + ]; + } + + /** + * @dataProvider notConfiguredProvider + */ + public function testCheckHealthNotConfigured(string $url, string $token): void { + $this->url = $url; + $this->token = $token; + $this->httpClient->expects(self::never())->method('request'); + + $this->expectException(StaticSiteGeneratorException::class); + $this->expectExceptionMessage('The static site generator is not configured'); + $this->client->checkHealth(); + } + + public function testCheckHealthNotReachable(): void { + $this->httpClient->method('request')->willThrowException(new Exception('Connection refused')); + + $this->expectException(StaticSiteGeneratorException::class); + $this->expectExceptionMessage('The static site generator is not reachable'); + $this->client->checkHealth(); + } + + public static function unhealthyResponseProvider(): array { + return [ + 'unauthorized' => [401, '{"status": "ok"}'], + 'server error' => [503, ''], + 'other status' => [200, '{"status": "degraded"}'], + 'no JSON' => [200, 'ok'], + ]; + } + + /** + * @dataProvider unhealthyResponseProvider + */ + public function testCheckHealthNotHealthy(int $statusCode, string $body): void { + $this->httpClient->method('request')->willReturn($this->makeResponse($statusCode, $body)); + + $this->expectException(StaticSiteGeneratorException::class); + $this->expectExceptionMessage('The static site generator is not healthy (HTTP status ' . $statusCode . ')'); + $this->client->checkHealth(); + } + + public function testBuildSendsStaticSiteWithBearerToken(): void { + $this->httpClient->expects(self::once()) + ->method('request') + // TODO(Publish): Add callback_status_url to request + ->with('POST', 'https://ssg.example.org/build', self::callback(fn (array $options): bool + => $options['headers']['Authorization'] === 'Bearer secret-token' + && $options['json'] === [ + 'static_site_id' => $this->staticSiteId, + 'content_download_url' => 'https://cloud.example.org/archive', + 'slug' => 'my-site', + 'title' => 'My Site', + ])) + ->willReturn($this->makeResponse(202, '{"status": "enqueued"}')); + + $this->client->build($this->makeStaticSite(), 'My Site'); + } + + /** + * @dataProvider notConfiguredProvider + */ + public function testBuildWithSsgNotConfigured(string $url, string $token): void { + $this->url = $url; + $this->token = $token; + $this->httpClient->expects(self::never())->method('request'); + + $this->expectException(StaticSiteGeneratorNotConfiguredException::class); + $this->client->build($this->makeStaticSite(), 'My Site'); + } + + public function testBuildWithSsgNotReachable(): void { + $this->httpClient->method('request')->willThrowException(new Exception('Connection timed out')); + + $this->expectException(StaticSiteGeneratorUnavailableException::class); + $this->client->build($this->makeStaticSite(), 'My Site'); + } + + public static function unavailableResponseProvider(): array { + return [ + 'server error' => [503, '{"status": "error", "error": "queue full"}'], + 'gateway error' => [502, ''], + ]; + } + + /** + * @dataProvider unavailableResponseProvider + */ + public function testBuildWithSsgUnavailable(int $statusCode, string $body): void { + $this->httpClient->method('request')->willReturn($this->makeResponse($statusCode, $body)); + + $this->expectException(StaticSiteGeneratorUnavailableException::class); + $this->client->build($this->makeStaticSite(), 'My Site'); + } + + public static function rejectedResponseProvider(): array { + return [ + 'invalid' => [400, '{"status": "invalid", "error": "slug is taken"}'], + 'unauthorized' => [401, ''], + 'not enqueued' => [200, '{"status": "ok"}'], + 'no JSON' => [202, 'enqueued'], + ]; + } + + /** + * @dataProvider rejectedResponseProvider + */ + public function testBuildRejected(int $statusCode, string $body): void { + $this->httpClient->method('request')->willReturn($this->makeResponse($statusCode, $body)); + + try { + $this->client->build($this->makeStaticSite(), 'My Site'); + $this->fail('Expected StaticSiteGeneratorException'); + } catch (StaticSiteGeneratorException $e) { + $this->assertNotInstanceOf(StaticSiteGeneratorUnavailableException::class, $e); + } + } + + public function testBuildLogsErrorOfGenerator(): void { + $this->httpClient->method('request')->willReturn($this->makeResponse(400, '{"status": "invalid", "error": "slug is taken"}')); + $this->logger->expects(self::once()) + ->method('warning') + ->with('Static site generator did not accept the build', [ + 'staticSiteId' => $this->staticSiteId, + 'statusCode' => 400, + 'error' => 'slug is taken', + ]); + + $this->expectException(StaticSiteGeneratorException::class); + $this->client->build($this->makeStaticSite(), 'My Site'); + } + + public static function tokenProvider(): array { + return [ + 'matching token' => ['secret-token', 'Bearer secret-token', true], + 'wrong token' => ['secret-token', 'Bearer other', false], + 'missing scheme' => ['secret-token', 'secret-token', false], + 'missing header' => ['secret-token', '', false], + 'no token configured' => ['', 'Bearer ', false], + ]; + } + + /** + * @dataProvider tokenProvider + */ + public function testIsValidToken(string $configuredToken, string $authorizationHeader, bool $expected): void { + $this->token = $configuredToken; + + $this->assertSame($expected, $this->client->isValidToken($authorizationHeader)); + } +} diff --git a/tests/Unit/Service/StaticSiteServiceTest.php b/tests/Unit/Service/StaticSiteServiceTest.php new file mode 100644 index 0000000000..865e962054 --- /dev/null +++ b/tests/Unit/Service/StaticSiteServiceTest.php @@ -0,0 +1,783 @@ +staticSiteMapper = $this->createMock(StaticSiteMapper::class); + $this->collectiveService = $this->createMock(CollectiveService::class); + $this->pageService = $this->createMock(PageService::class); + $this->contentCollector = $this->createMock(StaticSiteContentCollector::class); + $this->archiver = $this->createMock(StaticSiteArchiver::class); + $this->generatorClient = $this->createMock(StaticSiteGeneratorClient::class); + $this->logger = $this->createMock(LoggerInterface::class); + $timeFactory = $this->createMock(ITimeFactory::class); + $timeFactory->method('getTime')->willReturn($this->now); + $appConfig = $this->createMock(IAppConfig::class); + $appConfig->method('getValueInt') + ->with('collectives', ConfigLexicon::STATIC_SITE_MAX_SIZE, ConfigLexicon::STATIC_SITE_MAX_SIZE_DEFAULT) + ->willReturnCallback(fn (): int => $this->maxSize); + $l10n = $this->createMock(IL10N::class); + $l10n->method('t')->willReturnCallback(static fn (string $text, array $parameters = []): string => vsprintf($text, $parameters)); + + $this->service = new StaticSiteService( + $this->staticSiteMapper, + $this->collectiveService, + $this->pageService, + new SlugService(new AsciiSlugger()), + $this->contentCollector, + $this->archiver, + $this->generatorClient, + $this->logger, + $timeFactory, + $appConfig, + $l10n, + ); + } + + private function makeCollective(bool $canEdit, string $name = 'My Collective'): Collective { + $collective = $this->createMock(Collective::class); + $collective->method('canEdit')->willReturn($canEdit); + $collective->method('getName')->willReturn($name); + return $collective; + } + + private function makeStaticSite(string $status, int $updatedAgo = 60): StaticSite { + $staticSite = new StaticSite(); + $staticSite->setUpdatedAt($this->now - $updatedAgo); + $staticSite->setId(7); + $staticSite->setCollectiveId($this->collectiveId); + $staticSite->setStaticSiteId($this->staticSiteId); + $staticSite->setSlug('my-collective'); + $staticSite->setStatus($status); + return $staticSite; + } + + private function makePageInfo(int $id): PageInfo { + $page = $this->createMock(PageInfo::class); + $page->method('getId')->willReturn($id); + return $page; + } + + // --- publish() --- + + public function testPublishCreatesStaticSiteForValidPages(): void { + $pageIds = [1, 2, 3]; + $collective = $this->makeCollective(canEdit: true); + + $this->collectiveService->method('getCollective') + ->with($this->collectiveId, $this->userId) + ->willReturn($collective); + + $this->pageService->method('findAll') + ->with($this->collectiveId, $this->userId) + ->willReturn(array_map($this->makePageInfo(...), $pageIds)); + + $staticSite = $this->makeStaticSite(StaticSite::STATUS_PENDING); + $this->staticSiteMapper->expects($this->once()) + ->method('create') + ->with($this->collectiveId, $pageIds, 'My Collective', 'my-collective', $this->userId) + ->willReturn($staticSite); + + $files = ['Readme.md' => $this->makeFileWithSize(1)]; + $this->contentCollector->expects($this->once()) + ->method('collect') + ->with($this->collectiveId) + ->willReturn($files); + $this->archiver->expects($this->once()) + ->method('store') + ->with($this->staticSiteId, $files); + $this->generatorClient->expects($this->once()) + ->method('build') + ->with($staticSite, 'My Collective'); + + $provided = $this->makeStaticSite(StaticSite::STATUS_PROVIDED); + $this->staticSiteMapper->expects($this->once()) + ->method('updateSettingsAndStatus') + ->with($staticSite, 'My Collective', $pageIds, StaticSite::STATUS_PROVIDED) + ->willReturn($provided); + + $result = $this->service->create($this->collectiveId, $pageIds, $this->userId); + + $this->assertSame($provided, $result); + } + + public function testPublishCastsPageIdsToInt(): void { + // Simulates JSON body parsed by OCS framework where IDs may arrive as strings + $pageIds = ['1', '2']; + $collective = $this->makeCollective(canEdit: true); + + $this->collectiveService->method('getCollective')->willReturn($collective); + $this->pageService->method('findAll')->willReturn([ + $this->makePageInfo(1), + $this->makePageInfo(2), + ]); + + $this->staticSiteMapper->expects($this->once()) + ->method('create') + ->with($this->collectiveId, [1, 2], $this->anything(), $this->anything(), $this->userId) + ->willReturn($this->makeStaticSite(StaticSite::STATUS_PENDING)); + + $this->service->create($this->collectiveId, $pageIds, $this->userId); + } + + public function testPublishThrowsWhenNoPageIdsGiven(): void { + $this->expectException(UnprocessableEntityException::class); + + $this->staticSiteMapper->expects($this->never())->method('create'); + + $this->service->create($this->collectiveId, [], $this->userId); + } + + public function testPublishThrowsWhenUserCannotEdit(): void { + $collective = $this->makeCollective(canEdit: false); + $this->collectiveService->method('getCollective')->willReturn($collective); + + $this->expectException(NotPermittedException::class); + $this->staticSiteMapper->expects($this->never())->method('create'); + + $this->service->create($this->collectiveId, [1], $this->userId); + } + + public function testPublishThrowsWhenCollectiveNotAccessible(): void { + $this->collectiveService->method('getCollective') + ->willThrowException(new NotFoundException('Collective not found')); + + $this->expectException(NotFoundException::class); + $this->staticSiteMapper->expects($this->never())->method('create'); + + $this->service->create($this->collectiveId, [1], $this->userId); + } + + public function testPublishRejectsPageIdsFromOtherCollective(): void { + // IDOR guard: pageId 99 belongs to a different collective + $collective = $this->makeCollective(canEdit: true); + $this->collectiveService->method('getCollective')->willReturn($collective); + $this->pageService->method('findAll')->willReturn([ + $this->makePageInfo(1), + $this->makePageInfo(2), + ]); + + $this->expectException(NotFoundException::class); + $this->staticSiteMapper->expects($this->never())->method('create'); + + $this->service->create($this->collectiveId, [1, 99], $this->userId); + } + + private function expectValidCollectiveWithPages(string $name = 'My Collective'): void { + $this->collectiveService->method('getCollective')->willReturn($this->makeCollective(canEdit: true, name: $name)); + $this->pageService->method('findAll')->willReturn([$this->makePageInfo(1)]); + } + + public function testPublishUsesGivenTitleAndSlug(): void { + $this->expectValidCollectiveWithPages(); + + $this->staticSiteMapper->expects($this->once()) + ->method('create') + ->with($this->collectiveId, [1], 'Our Site', 'our-site-2026', $this->userId) + ->willReturn($this->makeStaticSite(StaticSite::STATUS_PENDING)); + + $this->service->create($this->collectiveId, [1], $this->userId, ' Our Site ', 'our-site-2026'); + } + + public function testPublishDefaultSlugIsTruncated(): void { + $name = str_repeat('a', 70); + $this->expectValidCollectiveWithPages($name); + + $this->staticSiteMapper->expects($this->once()) + ->method('create') + ->with($this->collectiveId, [1], $name, str_repeat('a', 64), $this->userId) + ->willReturn($this->makeStaticSite(StaticSite::STATUS_PENDING)); + + $this->service->create($this->collectiveId, [1], $this->userId); + } + + public static function invalidTitleProvider(): array { + return [ + 'empty' => [''], + 'whitespace only' => [' '], + 'too long' => [str_repeat('a', 256)], + ]; + } + + /** + * @dataProvider invalidTitleProvider + */ + public function testPublishRejectsInvalidTitle(string $title): void { + $this->expectValidCollectiveWithPages(); + + $this->expectException(UnprocessableEntityException::class); + $this->staticSiteMapper->expects($this->never())->method('create'); + + $this->service->create($this->collectiveId, [1], $this->userId, $title); + } + + public static function invalidSlugProvider(): array { + return [ + 'empty' => [''], + 'uppercase' => ['My-Site'], + 'space' => ['my site'], + 'underscore' => ['my_site'], + 'non-ascii' => ['über'], + 'leading hyphen' => ['-abc'], + 'trailing hyphen' => ['abc-'], + 'double hyphen' => ['a--b'], + 'hyphen only' => ['-'], + 'too long' => [str_repeat('a', 65)], + ]; + } + + /** + * @dataProvider invalidSlugProvider + */ + public function testPublishRejectsInvalidSlug(string $slug): void { + $this->expectValidCollectiveWithPages(); + + $this->expectException(UnprocessableEntityException::class); + $this->staticSiteMapper->expects($this->never())->method('create'); + + $this->service->create($this->collectiveId, [1], $this->userId, null, $slug); + } + + public function testPublishThrowsWhenSlugAlreadyInUse(): void { + $this->expectValidCollectiveWithPages(); + + $dbException = $this->createMock(DBException::class); + $dbException->method('getReason')->willReturn(DBException::REASON_UNIQUE_CONSTRAINT_VIOLATION); + $this->staticSiteMapper->method('create')->willThrowException($dbException); + + $this->expectException(UnprocessableEntityException::class); + + $this->service->create($this->collectiveId, [1], $this->userId, null, 'taken'); + } + + public function testPublishRethrowsOtherDatabaseErrors(): void { + $this->expectValidCollectiveWithPages(); + + $dbException = $this->createMock(DBException::class); + $dbException->method('getReason')->willReturn(DBException::REASON_DRIVER); + $this->staticSiteMapper->method('create')->willThrowException($dbException); + + $this->expectException(DBException::class); + + $this->service->create($this->collectiveId, [1], $this->userId); + } + + // --- update() --- + + public static function finishedStatusProvider(): array { + return [ + 'provided' => [StaticSite::STATUS_PROVIDED], + 'published' => [StaticSite::STATUS_PUBLISHED], + 'failed' => [StaticSite::STATUS_FAILED], + ]; + } + + /** + * @dataProvider finishedStatusProvider + */ + public function testUpdateRepublishesFinishedStaticSite(string $status): void { + $this->expectValidCollectiveWithPages(); + $staticSite = $this->makeStaticSite($status); + + $this->staticSiteMapper->method('findByIdAndCollectiveId') + ->with(7, $this->collectiveId) + ->willReturn($staticSite); + $this->staticSiteMapper->expects($this->once()) + ->method('updateStatusIfUnchanged') + ->with($staticSite, StaticSite::STATUS_PENDING) + ->willReturn(true); + $this->archiver->expects($this->once())->method('store'); + $this->generatorClient->expects($this->once()) + ->method('build') + ->with($staticSite, 'New Title'); + + $provided = $this->makeStaticSite(StaticSite::STATUS_PROVIDED); + $this->staticSiteMapper->expects($this->once()) + ->method('updateSettingsAndStatus') + ->with($staticSite, 'New Title', [1], StaticSite::STATUS_PROVIDED) + ->willReturn($provided); + + $result = $this->service->update($this->collectiveId, 7, ['1'], ' New Title ', $this->userId); + + $this->assertSame($provided, $result); + } + + public function testUpdateRepublishesAbortedPublication(): void { + $this->expectValidCollectiveWithPages(); + $staticSite = $this->makeStaticSite(StaticSite::STATUS_PENDING, StaticSite::PENDING_TIMEOUT + 1); + $this->staticSiteMapper->method('findByIdAndCollectiveId')->willReturn($staticSite); + $this->staticSiteMapper->expects($this->once())->method('updateStatusIfUnchanged')->willReturn(true); + $this->staticSiteMapper->expects($this->once())->method('updateSettingsAndStatus'); + + $this->service->update($this->collectiveId, 7, [1], 'Title', $this->userId); + } + + public function testUpdateRejectsStaticSiteInProgress(): void { + $this->expectValidCollectiveWithPages(); + $this->staticSiteMapper->method('findByIdAndCollectiveId')->willReturn($this->makeStaticSite(StaticSite::STATUS_PENDING)); + + $this->expectException(UnprocessableEntityException::class); + $this->staticSiteMapper->expects($this->never())->method('updateStatusIfUnchanged'); + + $this->service->update($this->collectiveId, 7, [1], 'Title', $this->userId); + } + + public function testUpdateRejectsPublicationStartedConcurrently(): void { + $this->expectValidCollectiveWithPages(); + $this->staticSiteMapper->method('findByIdAndCollectiveId')->willReturn($this->makeStaticSite(StaticSite::STATUS_PUBLISHED)); + $this->staticSiteMapper->method('updateStatusIfUnchanged')->willReturn(false); + + $this->expectException(UnprocessableEntityException::class); + $this->archiver->expects($this->never())->method('store'); + + $this->service->update($this->collectiveId, 7, [1], 'Title', $this->userId); + } + + public function testUpdateRestoresStatusIfArchiveCannotBeProvided(): void { + $this->expectValidCollectiveWithPages(); + $this->staticSiteMapper->method('findByIdAndCollectiveId')->willReturn($this->makeStaticSite(StaticSite::STATUS_PUBLISHED)); + $this->staticSiteMapper->method('updateStatusIfUnchanged')->willReturn(true); + $this->archiver->method('store')->willThrowException(new ServiceException('Failed')); + + $this->staticSiteMapper->expects($this->once()) + ->method('updateStatus') + ->with(7, StaticSite::STATUS_PUBLISHED); + $this->staticSiteMapper->expects($this->never())->method('updateSettingsAndStatus'); + $this->archiver->expects($this->never())->method('delete'); + + $this->expectException(ServiceException::class); + $this->service->update($this->collectiveId, 7, [1], 'Title', $this->userId); + } + + public function testUpdateIgnoresDeletedStaticSiteWhenRestoringStatus(): void { + $this->expectValidCollectiveWithPages(); + $this->staticSiteMapper->method('findByIdAndCollectiveId')->willReturn($this->makeStaticSite(StaticSite::STATUS_PUBLISHED)); + $this->staticSiteMapper->method('updateStatusIfUnchanged')->willReturn(true); + $this->archiver->method('store')->willThrowException(new ServiceException('Failed')); + $this->staticSiteMapper->method('updateStatus') + ->willThrowException(new NotFoundException('Static site not found')); + + $this->expectException(ServiceException::class); + $this->service->update($this->collectiveId, 7, [1], 'Title', $this->userId); + } + + public function testUpdateRestoresStatusAndKeepsMessageIfPathIsTooLong(): void { + $this->expectValidCollectiveWithPages(); + $this->staticSiteMapper->method('findByIdAndCollectiveId')->willReturn($this->makeStaticSite(StaticSite::STATUS_PUBLISHED)); + $this->staticSiteMapper->method('updateStatusIfUnchanged')->willReturn(true); + $this->archiver->method('store')->willThrowException(new UnprocessableEntityException('The path is too long')); + + $this->staticSiteMapper->expects($this->once()) + ->method('updateStatus') + ->with(7, StaticSite::STATUS_PUBLISHED); + + $this->expectException(UnprocessableEntityException::class); + $this->expectExceptionMessage('The path is too long'); + $this->service->update($this->collectiveId, 7, [1], 'Title', $this->userId); + } + + public function testUpdateThrowsWhenNoPageIdsGiven(): void { + $this->expectException(UnprocessableEntityException::class); + $this->staticSiteMapper->expects($this->never())->method('updateStatusIfUnchanged'); + + $this->service->update($this->collectiveId, 7, [], 'Title', $this->userId); + } + + public function testUpdateThrowsWhenUserCannotEdit(): void { + $this->collectiveService->method('getCollective')->willReturn($this->makeCollective(canEdit: false)); + + $this->expectException(NotPermittedException::class); + $this->staticSiteMapper->expects($this->never())->method('findByIdAndCollectiveId'); + $this->staticSiteMapper->expects($this->never())->method('updateStatusIfUnchanged'); + + $this->service->update($this->collectiveId, 7, [1], 'Title', $this->userId); + } + + public function testUpdateThrowsWhenStaticSiteNotInCollective(): void { + $this->expectValidCollectiveWithPages(); + $this->staticSiteMapper->method('findByIdAndCollectiveId') + ->willThrowException(new NotFoundException('Static site not found')); + + $this->expectException(NotFoundException::class); + $this->staticSiteMapper->expects($this->never())->method('updateStatusIfUnchanged'); + + $this->service->update($this->collectiveId, 7, [1], 'Title', $this->userId); + } + + /** + * @dataProvider invalidTitleProvider + */ + public function testUpdateRejectsInvalidTitle(string $title): void { + $this->expectValidCollectiveWithPages(); + $this->staticSiteMapper->method('findByIdAndCollectiveId')->willReturn($this->makeStaticSite(StaticSite::STATUS_PUBLISHED)); + + $this->expectException(UnprocessableEntityException::class); + $this->staticSiteMapper->expects($this->never())->method('updateStatusIfUnchanged'); + + $this->service->update($this->collectiveId, 7, [1], $title, $this->userId); + } + + public function testUpdateRejectsPageIdsFromOtherCollective(): void { + $this->expectValidCollectiveWithPages(); + $this->staticSiteMapper->method('findByIdAndCollectiveId')->willReturn($this->makeStaticSite(StaticSite::STATUS_PUBLISHED)); + + $this->expectException(NotFoundException::class); + $this->staticSiteMapper->expects($this->never())->method('updateStatusIfUnchanged'); + + $this->service->update($this->collectiveId, 7, [1, 99], 'Title', $this->userId); + } + + // --- delete() --- + + /** + * @dataProvider finishedStatusProvider + */ + public function testDeleteRemovesStaticSite(string $status): void { + $this->collectiveService->method('getCollective')->willReturn($this->makeCollective(canEdit: true)); + $staticSite = $this->makeStaticSite($status); + + $this->staticSiteMapper->method('findByIdAndCollectiveId') + ->with(7, $this->collectiveId) + ->willReturn($staticSite); + $this->staticSiteMapper->expects($this->once()) + ->method('delete') + ->with($staticSite); + $this->archiver->expects($this->once()) + ->method('delete') + ->with($this->staticSiteId); + + $this->service->delete($this->collectiveId, 7, $this->userId); + } + + public function testDeleteSucceedsIfArchiveDeletionFails(): void { + $this->collectiveService->method('getCollective')->willReturn($this->makeCollective(canEdit: true)); + $this->staticSiteMapper->method('findByIdAndCollectiveId')->willReturn($this->makeStaticSite(StaticSite::STATUS_PUBLISHED)); + $this->staticSiteMapper->expects($this->once())->method('delete'); + $this->archiver->method('delete')->willThrowException(new ServiceException('Failed')); + $this->logger->expects($this->once())->method('warning'); + + $this->service->delete($this->collectiveId, 7, $this->userId); + } + + public function testDeleteThrowsWhenUserCannotEdit(): void { + $this->collectiveService->method('getCollective')->willReturn($this->makeCollective(canEdit: false)); + + $this->expectException(NotPermittedException::class); + $this->staticSiteMapper->expects($this->never())->method('findByIdAndCollectiveId'); + $this->staticSiteMapper->expects($this->never())->method('delete'); + + $this->service->delete($this->collectiveId, 7, $this->userId); + } + + public function testDeleteThrowsWhenStaticSiteNotInCollective(): void { + $this->collectiveService->method('getCollective')->willReturn($this->makeCollective(canEdit: true)); + $this->staticSiteMapper->method('findByIdAndCollectiveId') + ->willThrowException(new NotFoundException('Static site not found')); + + $this->expectException(NotFoundException::class); + $this->staticSiteMapper->expects($this->never())->method('delete'); + + $this->service->delete($this->collectiveId, 7, $this->userId); + } + + // --- archive --- + + private function expectCreatedStaticSite(): StaticSite { + $this->expectValidCollectiveWithPages(); + $staticSite = $this->makeStaticSite(StaticSite::STATUS_PENDING); + $this->staticSiteMapper->method('create')->willReturn($staticSite); + return $staticSite; + } + + public function testPublishRemovesStaticSiteIfArchiveCannotBeStored(): void { + $staticSite = $this->expectCreatedStaticSite(); + $this->contentCollector->method('collect')->willReturn([]); + $this->archiver->method('store')->willThrowException(new ServiceException('Failed')); + + $this->staticSiteMapper->expects($this->once())->method('delete')->with($staticSite); + $this->archiver->expects($this->once())->method('delete')->with($this->staticSiteId); + $this->staticSiteMapper->expects($this->never())->method('updateSettingsAndStatus'); + + $this->expectException(ServiceException::class); + $this->service->create($this->collectiveId, [1], $this->userId); + } + + public function testPublishRemovesStaticSiteAndKeepsMessageIfPathIsTooLong(): void { + $staticSite = $this->expectCreatedStaticSite(); + $this->contentCollector->method('collect')->willReturn([]); + $exception = new UnprocessableEntityException('The path is too long'); + $this->archiver->method('store')->willThrowException($exception); + + $this->staticSiteMapper->expects($this->once())->method('delete')->with($staticSite); + + try { + $this->service->create($this->collectiveId, [1], $this->userId); + $this->fail('Expected UnprocessableEntityException'); + } catch (UnprocessableEntityException $e) { + $this->assertSame($exception, $e); + } + } + + private function makeFileWithSize(int $size): File&MockObject { + $file = $this->createMock(File::class); + $file->method('getSize')->willReturn($size); + return $file; + } + + public function testPublishAcceptsContentUpToMaxSize(): void { + $this->expectCreatedStaticSite(); + $this->contentCollector->method('collect')->willReturn([ + 'Readme.md' => $this->makeFileWithSize(ConfigLexicon::STATIC_SITE_MAX_SIZE_DEFAULT - 1), + 'image.png' => $this->makeFileWithSize(1), + ]); + $this->archiver->expects($this->once())->method('store'); + + $this->service->create($this->collectiveId, [1], $this->userId); + } + + public function testPublishRejectsContentAboveMaxSizeAndNamesLargestFiles(): void { + $staticSite = $this->expectCreatedStaticSite(); + $mb = 1024 * 1024; + $this->contentCollector->method('collect')->willReturn([ + 'Readme.md' => $this->makeFileWithSize(1), + '.attachments.1/video.mp4' => $this->makeFileWithSize(80 * $mb), + '2024' => $this->makeFileWithSize(5 * $mb), + '.attachments.2/photo.jpg' => $this->makeFileWithSize(20 * $mb), + ]); + $this->archiver->expects($this->never())->method('store'); + $this->staticSiteMapper->expects($this->once())->method('delete')->with($staticSite); + + $this->expectException(UnprocessableEntityException::class); + $this->expectExceptionMessage('The website would be 105 MB, but at most 100 MB are allowed. Please remove or shrink large files, e.g. .attachments.1/video.mp4 (80 MB), .attachments.2/photo.jpg (20 MB), 2024 (5 MB)'); + $this->service->create($this->collectiveId, [1], $this->userId); + } + + public function testPublishUsesConfiguredMaxSize(): void { + $this->maxSize = 10; + $this->expectCreatedStaticSite(); + $this->contentCollector->method('collect')->willReturn(['Readme.md' => $this->makeFileWithSize(11)]); + $this->archiver->expects($this->never())->method('store'); + + $this->expectException(UnprocessableEntityException::class); + $this->service->create($this->collectiveId, [1], $this->userId); + } + + public function testPublishRemovesStaticSiteIfContentCannotBeCollected(): void { + $staticSite = $this->expectCreatedStaticSite(); + $this->contentCollector->method('collect')->willThrowException(new FilesNotFoundException()); + $this->archiver->expects($this->never())->method('store'); + + $this->staticSiteMapper->expects($this->once())->method('delete')->with($staticSite); + + $this->expectException(ServiceException::class); + $this->service->create($this->collectiveId, [1], $this->userId); + } + + public function testPublishDeletesArchiveIfStaticSiteWasDeletedMeanwhile(): void { + $this->expectCreatedStaticSite(); + $this->contentCollector->method('collect')->willReturn([]); + $this->staticSiteMapper->method('updateSettingsAndStatus') + ->willThrowException(new NotFoundException('Static site not found')); + $this->archiver->expects($this->once())->method('delete')->with($this->staticSiteId); + + $this->expectException(NotFoundException::class); + $this->service->create($this->collectiveId, [1], $this->userId); + } + + public function testPublishKeepsNotFoundExceptionIfArchiveDeletionFails(): void { + $this->expectCreatedStaticSite(); + $this->contentCollector->method('collect')->willReturn([]); + $this->staticSiteMapper->method('updateSettingsAndStatus') + ->willThrowException(new NotFoundException('Static site not found')); + $this->archiver->method('delete')->willThrowException(new ServiceException('Failed')); + $this->logger->expects($this->once())->method('warning'); + + $this->expectException(NotFoundException::class); + $this->service->create($this->collectiveId, [1], $this->userId); + } + + // --- static site generator --- + + public function testPublishRejectsIfGeneratorIsNotConfigured(): void { + $this->expectValidCollectiveWithPages(); + $this->generatorClient->method('assertConfigured') + ->willThrowException(new StaticSiteGeneratorNotConfiguredException('Not configured')); + $this->staticSiteMapper->expects($this->never())->method('create'); + $this->archiver->expects($this->never())->method('store'); + + $this->expectException(StaticSiteGeneratorNotConfiguredException::class); + $this->service->create($this->collectiveId, [1], $this->userId); + } + + public function testPublishRemovesStaticSiteIfGeneratorDoesNotAcceptBuild(): void { + $staticSite = $this->expectCreatedStaticSite(); + $this->contentCollector->method('collect')->willReturn(['Readme.md' => $this->makeFileWithSize(1)]); + $this->generatorClient->method('build')->willThrowException(new StaticSiteGeneratorException('Rejected')); + + $this->staticSiteMapper->expects($this->once())->method('delete')->with($staticSite); + $this->archiver->expects($this->once())->method('delete')->with($this->staticSiteId); + $this->staticSiteMapper->expects($this->never())->method('updateSettingsAndStatus'); + + $this->expectException(StaticSiteGeneratorException::class); + $this->service->create($this->collectiveId, [1], $this->userId); + } + + public function testPublishValidatesInputBeforeCheckingGeneratorConfiguration(): void { + $this->expectValidCollectiveWithPages(); + $this->generatorClient->expects($this->never())->method('assertConfigured'); + + $this->expectException(UnprocessableEntityException::class); + $this->service->create($this->collectiveId, [1], $this->userId, null, 'Invalid Slug'); + } + + public function testUpdateRejectsIfGeneratorIsNotConfigured(): void { + $this->expectValidCollectiveWithPages(); + $this->staticSiteMapper->method('findByIdAndCollectiveId')->willReturn($this->makeStaticSite(StaticSite::STATUS_PUBLISHED)); + $this->generatorClient->method('assertConfigured') + ->willThrowException(new StaticSiteGeneratorNotConfiguredException('Not configured')); + $this->staticSiteMapper->expects($this->never())->method('updateStatusIfUnchanged'); + + $this->expectException(StaticSiteGeneratorNotConfiguredException::class); + $this->service->update($this->collectiveId, 7, [1], 'Title', $this->userId); + } + + public function testUpdateFindsStaticSiteBeforeCheckingGeneratorConfiguration(): void { + $this->expectValidCollectiveWithPages(); + $this->staticSiteMapper->method('findByIdAndCollectiveId') + ->willThrowException(new NotFoundException('Static site not found')); + $this->generatorClient->expects($this->never())->method('assertConfigured'); + + $this->expectException(NotFoundException::class); + $this->service->update($this->collectiveId, 7, [1], 'Title', $this->userId); + } + + public function testUpdateStoresStatusFailedIfGeneratorDoesNotAcceptBuild(): void { + $this->expectValidCollectiveWithPages(); + $staticSite = $this->makeStaticSite(StaticSite::STATUS_PUBLISHED); + $this->staticSiteMapper->method('findByIdAndCollectiveId')->willReturn($staticSite); + $this->staticSiteMapper->method('updateStatusIfUnchanged')->willReturn(true); + $this->generatorClient->method('build')->willThrowException(new StaticSiteGeneratorException('Rejected')); + + $this->staticSiteMapper->expects($this->once()) + ->method('updateSettingsAndStatus') + ->with($staticSite, 'Title', [1], StaticSite::STATUS_FAILED); + $this->archiver->expects($this->never())->method('delete'); + + $this->expectException(StaticSiteGeneratorException::class); + $this->service->update($this->collectiveId, 7, [1], 'Title', $this->userId); + } + + // --- getStaticSites() --- + + public function testGetStaticSitesReturnsListForAccessibleCollective(): void { + $collective = $this->makeCollective(canEdit: true); + $this->collectiveService->method('getCollective') + ->with($this->collectiveId, $this->userId) + ->willReturn($collective); + + $sites = [new StaticSite(), new StaticSite()]; + $this->staticSiteMapper->method('findByCollectiveId') + ->with($this->collectiveId) + ->willReturn($sites); + + $result = $this->service->getStaticSites($this->collectiveId, $this->userId); + + $this->assertSame($sites, $result); + } + + public function testGetStaticSitesThrowsWhenCollectiveNotAccessible(): void { + $this->collectiveService->method('getCollective') + ->willThrowException(new NotFoundException('Collective not found')); + + $this->expectException(NotFoundException::class); + $this->staticSiteMapper->expects($this->never())->method('findByCollectiveId'); + + $this->service->getStaticSites($this->collectiveId, $this->userId); + } + + // --- getArchive() --- + + public function testGetArchiveReturnsArchiveOfExistingStaticSite(): void { + $archive = $this->createMock(ISimpleFile::class); + $this->staticSiteMapper->method('findOneByStaticSiteId') + ->with($this->staticSiteId) + ->willReturn($this->makeStaticSite(StaticSite::STATUS_PROVIDED)); + $this->archiver->method('get')->with($this->staticSiteId)->willReturn($archive); + + $this->assertSame($archive, $this->service->getArchive($this->staticSiteId)); + } + + public function testGetArchiveThrowsIfStaticSiteDoesNotExist(): void { + $this->staticSiteMapper->method('findOneByStaticSiteId') + ->willThrowException(new DoesNotExistException('Not found')); + $this->archiver->expects($this->never())->method('get'); + + $this->expectException(NotFoundException::class); + $this->service->getArchive($this->staticSiteId); + } + + public function testGetArchiveThrowsIfArchiveDoesNotExist(): void { + $this->staticSiteMapper->method('findOneByStaticSiteId') + ->willReturn($this->makeStaticSite(StaticSite::STATUS_PENDING)); + $this->archiver->method('get')->willThrowException(new NotFoundException('Static site archive not found')); + + $this->expectException(NotFoundException::class); + $this->service->getArchive($this->staticSiteId); + } +} diff --git a/tests/Unit/Settings/AdminTest.php b/tests/Unit/Settings/AdminTest.php new file mode 100644 index 0000000000..55d31b6d8c --- /dev/null +++ b/tests/Unit/Settings/AdminTest.php @@ -0,0 +1,60 @@ +appConfig = $this->createMock(IAppConfig::class); + $initialState = $this->createMock(IInitialState::class); + $initialState->method('provideInitialState') + ->willReturnCallback(function (string $key, mixed $data): void { + $this->initialState[$key] = $data; + }); + $this->admin = new Admin($this->appConfig, $initialState); + } + + private function mockToken(string $token): void { + $this->appConfig->method('getValueString') + ->willReturnCallback(static fn (string $app, string $key, string $default = ''): string => match ($key) { + ConfigLexicon::STATIC_SITE_GENERATOR_TOKEN => $token, + default => $default, + }); + } + + public function testSavedTokenIsReplacedByPlaceholder(): void { + $this->mockToken('secret-token'); + + $this->admin->getForm(); + + self::assertSame(Admin::TOKEN_PLACEHOLDER, $this->initialState['adminSettings']['static_site_generator_token']); + self::assertStringNotContainsString('secret-token', json_encode($this->initialState)); + } + + public function testMissingTokenIsEmpty(): void { + $this->mockToken(''); + + $this->admin->getForm(); + + self::assertSame('', $this->initialState['adminSettings']['static_site_generator_token']); + } +}