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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
18 changes: 18 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' => '.*'],
Expand Down Expand Up @@ -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)']],
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
165 changes: 163 additions & 2 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
36 changes: 36 additions & 0 deletions lib/ConfigLexicon.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
),
];
}

Expand Down
69 changes: 69 additions & 0 deletions lib/Controller/StaticSiteArchiveController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

declare(strict_types=1);

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

namespace OCA\Collectives\Controller;

use OCA\Collectives\Service\NotFoundException;
use OCA\Collectives\Service\StaticSiteGeneratorClient;
use OCA\Collectives\Service\StaticSiteService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\BruteForceProtection;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\Http\FileDisplayResponse;
use OCP\AppFramework\Http\Response;
use OCP\DB\Exception as DBException;
use OCP\IRequest;

/**
* Provides static site archives to the external publish service.
*/
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
class StaticSiteArchiveController extends Controller {
public function __construct(
string $appName,
IRequest $request,
private readonly StaticSiteService $staticSiteService,
private readonly StaticSiteGeneratorClient $staticSiteGeneratorClient,
) {
parent::__construct($appName, $request);
}

/**
* Download the archive of a static site, authenticated by the bearer token of the external static site generator.
*
* The status of the static site isn't checked, as the generator may download the archive
* before the response to its notification was processed.
*
* @throws DBException
*/
#[PublicPage]
#[NoCSRFRequired]
#[BruteForceProtection(action: 'collectivesStaticSiteArchive')]
public function download(string $staticSiteId): Response {
if (!$this->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;
}
}
Loading
Loading