Files
neonoverlordandClaude Opus 5.5 6f27163369 1.1.0: Pinnacle unbundled -- the Social page frames <instance>/pinnacle/
Pinnacle now ships inside the node GoToSocial image. This app keeps the
dashboard widgets and one framed Social page so widget clicks still land on
the exact status/account/notification inside Nextcloud (?path= deep links).

Removed: /client/* file server + routes, its CSP carve-out, NC-theming colour
injection, the auth bridge, and CI's Pinnacle bundling step. The page's CSP
allows framing the instance origin; Pinnacle's own CSP allows cloud.bawnet.io.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 23:03:24 -07:00

94 lines
3.2 KiB
PHP

<?php
/**
* Nextcloud - GoToSocial
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*/
namespace OCA\GoToSocial\Controller;
use OCA\GoToSocial\AppInfo\Application;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IAppConfig;
use OCP\IConfig;
use OCP\IRequest;
/**
* The Social page: Pinnacle inside the Nextcloud frame.
*
* SINCE 1.1.0 (the fixed-instance build): Pinnacle is no
* longer bundled here. It ships INSIDE the node GoToSocial image and GTS
* serves it at <instance>/pinnacle/ (patch/gotosocial 0004). This app keeps
* only what Nextcloud is for: the dashboard widgets, and this one framed page
* so a widget click still lands on that exact status/account/notification
* inside Nextcloud (?path=/statuses/<id>).
*
* Removed with the bundle: the /client/* file server, its CSP carve-out, the
* NC-theming colour injection (Pinnacle now carries social.bawnet.io's amber),
* and the auth bridge (NC could inject its token only into pages NC served —
* Pinnacle now does its own login once, top-level, and the browser keeps it).
*
* Framing works because Pinnacle's own CSP allows it: frame-ancestors 'self'
* https://cloud.bawnet.io (bawnet/pinnacle build-gts, pinnacle-headers.json).
*/
class ClientController extends Controller {
public function __construct(
string $appName,
IRequest $request,
private IAppConfig $appConfig,
private IConfig $config,
private ?string $userId,
) {
parent::__construct($appName, $request);
}
/**
* The GoToSocial instance this user is connected to (their own setting,
* else the admin default) — the same resolution the widgets use.
*/
private function gtsUrl(): string {
$adminUrl = $this->appConfig->getAppValueString('oauth_instance_url', lazy: true);
if ($this->userId === null) {
return $adminUrl;
}
return $this->config->getUserValue($this->userId, Application::APP_ID, 'url', $adminUrl) ?: $adminUrl;
}
#[NoAdminRequired]
#[NoCSRFRequired]
public function page(string $path = ''): TemplateResponse {
$scheme = parse_url($this->gtsUrl(), PHP_URL_SCHEME) ?: 'https';
$host = parse_url($this->gtsUrl(), PHP_URL_HOST);
$port = parse_url($this->gtsUrl(), PHP_URL_PORT);
$origin = $host ? $scheme . '://' . $host . ($port ? ':' . $port : '') : '';
$clientUrl = $origin . '/pinnacle/';
// deep link from the dashboard widgets: ?path=/statuses/<id> lands the
// framed client on that route. Strict allowlist: absolute client-route
// paths only, no traversal, no scheme, no query — anything else falls
// back to the client root.
if ($path !== ''
&& preg_match('#^/[A-Za-z0-9_@.~/-]*$#', $path)
&& !str_contains($path, '..')
&& !str_contains($path, '//')) {
$clientUrl .= ltrim($path, '/');
}
$response = new TemplateResponse(Application::APP_ID, 'clientPage', [
'clientUrl' => $clientUrl,
]);
$csp = new ContentSecurityPolicy();
if ($origin !== '') {
$csp->addAllowedFrameDomain($origin);
}
$response->setContentSecurityPolicy($csp);
return $response;
}
}