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
49 changes: 49 additions & 0 deletions Caddyfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
localhost {
php_server {
worker index.php
}

log {
level ERROR
output stderr
}

encode gzip

redir /.well-known/carddav /remote.php/dav 301
redir /.well-known/caldav /remote.php/dav 301

# Rule: Maps most RFC 8615 compliant well-known URIs to our main frontend controller (/index.php) by default
@wellKnown {
path "/.well-known/"
not {
path /.well-known/acme-challenge
path /.well-known/pki-validation
}
}
rewrite @wellKnown /index.php

rewrite /ocm-provider/ /index.php

@forbidden {
path /.htaccess
path /data/*
path /config/*
path /db_structure
path /.xml
path /README
path /3rdparty/*
path /lib/*
path /templates/*
path /occ
path /build
path /tests
path /console.php
path /autotest
path /issue
path /indi
path /db_
path /console
}
respond @forbidden 404
}
5 changes: 5 additions & 0 deletions apps/testing/appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@
<types>
<authentication/>
</types>

<commands>
<command>OCA\Testing\Command\StaticHunt</command>
</commands>

<category>monitoring</category>
<bugs>https://github.com/nextcloud/server/issues</bugs>
<dependencies>
Expand Down
1 change: 1 addition & 0 deletions apps/testing/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
'OCA\\Testing\\AlternativeHomeUserBackend' => $baseDir . '/../lib/AlternativeHomeUserBackend.php',
'OCA\\Testing\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php',
'OCA\\Testing\\Command\\StaticHunt' => $baseDir . '/../lib/Command/StaticHunt.php',
'OCA\\Testing\\Controller\\ConfigController' => $baseDir . '/../lib/Controller/ConfigController.php',
'OCA\\Testing\\Controller\\LockingController' => $baseDir . '/../lib/Controller/LockingController.php',
'OCA\\Testing\\Controller\\RateLimitTestController' => $baseDir . '/../lib/Controller/RateLimitTestController.php',
Expand Down
1 change: 1 addition & 0 deletions apps/testing/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class ComposerStaticInitTesting
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'OCA\\Testing\\AlternativeHomeUserBackend' => __DIR__ . '/..' . '/../lib/AlternativeHomeUserBackend.php',
'OCA\\Testing\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php',
'OCA\\Testing\\Command\\StaticHunt' => __DIR__ . '/..' . '/../lib/Command/StaticHunt.php',
'OCA\\Testing\\Controller\\ConfigController' => __DIR__ . '/..' . '/../lib/Controller/ConfigController.php',
'OCA\\Testing\\Controller\\LockingController' => __DIR__ . '/..' . '/../lib/Controller/LockingController.php',
'OCA\\Testing\\Controller\\RateLimitTestController' => __DIR__ . '/..' . '/../lib/Controller/RateLimitTestController.php',
Expand Down
73 changes: 73 additions & 0 deletions apps/testing/lib/Command/StaticHunt.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

declare(strict_types=1);

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

namespace OCA\Testing\Command;

use OCA\Theming\ImageManager;
use OCA\Theming\ThemingDefaults;
use OCP\IConfig;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class StaticHunt extends Command {
public function __construct(
private IConfig $config,
) {
parent::__construct();
}

protected function configure() {
$this
->setName('testing:static-hunt')
->setDescription('Hunt for static properties in classes');
}


protected function execute(InputInterface $input, OutputInterface $output): int {
//TODO run on all apps namespaces
$folders = [
'\\OC' => __DIR__ . '/../../../../lib/private',
'' => __DIR__ . '/../../../../lib/private/legacy',
];
foreach ($folders as $namespace => $folder) {
$this->scanFolder($folder, $namespace, $output);
}

return 0;
}

private function scanFolder(string $folder, string $namespace, OutputInterface $output): void {
$folder = realpath($folder);
foreach (glob($folder.'/**.php') as $filename) {
try {
$filename = realpath($filename);
$classname = $namespace.substr(str_replace('/', '\\', substr($filename, strlen($folder))), 0, -4);
if (!class_exists($classname)) {
continue;
}
$rClass = new \ReflectionClass($classname);
$staticProperties = $rClass->getStaticProperties();
if (empty($staticProperties)) {
continue;
}
$output->writeln('<info># ' . str_replace(\OC::$SERVERROOT, '', $filename) . " $classname</info>");
foreach ($staticProperties as $property => $value) {
$propertyObject = $rClass->getProperty($property);
$output->write("$propertyObject");
}
$output->writeln("");
} catch (\Throwable $t) {
$output->writeln("$t");
}
}
}
}
181 changes: 109 additions & 72 deletions index.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,90 +19,127 @@
use OCP\Template\ITemplateManager;
use Psr\Log\LoggerInterface;

try {
require_once __DIR__ . '/lib/base.php';
require_once __DIR__ . '/lib/OC.php';

OC::handleRequest();
} catch (ServiceUnavailableException $ex) {
Server::get(LoggerInterface::class)->error($ex->getMessage(), [
'app' => 'index',
'exception' => $ex,
]);
\OC::boot();

//show the user a detailed error page
Server::get(ITemplateManager::class)->printExceptionErrorPage($ex, 503);
} catch (HintException $ex) {
function cleanupStaticCrap() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love it!

// FIXME needed because these use a static var
\OC_Hook::clear();
\OC_Util::$styles = [];
\OC_Util::$headers = [];
\OC_User::setIncognitoMode(false);
\OC_User::$_setupedBackends = [];
\OC_App::reset();
\OC_Helper::reset();
}

$handler = static function () {
try {
Server::get(ITemplateManager::class)->printErrorPage($ex->getMessage(), $ex->getHint(), 503);
} catch (Exception $ex2) {
// In worker mode, script name is empty in FrankenPHP
if ($_SERVER['SCRIPT_NAME'] === '') {

Check failure on line 40 in index.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

TypeDoesNotContainType

index.php:40:7: TypeDoesNotContainType: '' cannot be identical to non-empty-string (see https://psalm.dev/056)
$_SERVER['SCRIPT_NAME'] = $_SERVER['PHP_SELF'];
}
cleanupStaticCrap();
OC::init();
OC::handleRequest();
} catch (ServiceUnavailableException $ex) {
Server::get(LoggerInterface::class)->error($ex->getMessage(), [
'app' => 'index',
'exception' => $ex,
]);

//show the user a detailed error page
Server::get(ITemplateManager::class)->printExceptionErrorPage($ex, 503);
} catch (HintException $ex) {
try {
Server::get(ITemplateManager::class)->printErrorPage($ex->getMessage(), $ex->getHint(), 503);
} catch (Exception $ex2) {
try {
Server::get(LoggerInterface::class)->error($ex->getMessage(), [
'app' => 'index',
'exception' => $ex,
]);
Server::get(LoggerInterface::class)->error($ex2->getMessage(), [
'app' => 'index',
'exception' => $ex2,
]);
} catch (Throwable $e) {
// no way to log it properly - but to avoid a white page of death we try harder and ignore this one here
}

//show the user a detailed error page
Server::get(ITemplateManager::class)->printExceptionErrorPage($ex, 500);
}
} catch (LoginException $ex) {
$request = Server::get(IRequest::class);
/**
* Routes with the @CORS annotation and other API endpoints should
* not return a webpage, so we only print the error page when html is accepted,
* otherwise we reply with a JSON array like the SecurityMiddleware would do.
*/
if (stripos($request->getHeader('Accept'), 'html') === false) {
http_response_code(401);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['message' => $ex->getMessage()]);
exit();
}
Server::get(ITemplateManager::class)->printErrorPage($ex->getMessage(), $ex->getMessage(), 401);
} catch (MaxDelayReached $ex) {
$request = Server::get(IRequest::class);
/**
* Routes with the @CORS annotation and other API endpoints should
* not return a webpage, so we only print the error page when html is accepted,
* otherwise we reply with a JSON array like the BruteForceMiddleware would do.
*/
if (stripos($request->getHeader('Accept'), 'html') === false) {
http_response_code(429);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['message' => $ex->getMessage()]);
exit();
}
http_response_code(429);
Server::get(ITemplateManager::class)->printGuestPage('core', '429');
} catch (Exception $ex) {
Server::get(LoggerInterface::class)->error($ex->getMessage(), [
'app' => 'index',
'exception' => $ex,
]);

//show the user a detailed error page
Server::get(ITemplateManager::class)->printExceptionErrorPage($ex, 500);
} catch (Error $ex) {
try {
Server::get(LoggerInterface::class)->error($ex->getMessage(), [
'app' => 'index',
'exception' => $ex,
]);
Server::get(LoggerInterface::class)->error($ex2->getMessage(), [
'app' => 'index',
'exception' => $ex2,
]);
} catch (Throwable $e) {
// no way to log it properly - but to avoid a white page of death we try harder and ignore this one here
}
} catch (Error $e) {
http_response_code(500);
header('Content-Type: text/plain; charset=utf-8');
print("Internal Server Error\n\n");
print("The server encountered an internal error and was unable to complete your request.\n");
print("Please contact the server administrator if this error reappears multiple times, please include the technical details below in your report.\n");
print("More details can be found in the webserver log.\n");

//show the user a detailed error page
throw $ex;
}
Server::get(ITemplateManager::class)->printExceptionErrorPage($ex, 500);
}
} catch (LoginException $ex) {
$request = Server::get(IRequest::class);
/**
* Routes with the @CORS annotation and other API endpoints should
* not return a webpage, so we only print the error page when html is accepted,
* otherwise we reply with a JSON array like the SecurityMiddleware would do.
*/
if (stripos($request->getHeader('Accept'), 'html') === false) {
http_response_code(401);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['message' => $ex->getMessage()]);
exit();
}
Server::get(ITemplateManager::class)->printErrorPage($ex->getMessage(), $ex->getMessage(), 401);
} catch (MaxDelayReached $ex) {
$request = Server::get(IRequest::class);
/**
* Routes with the @CORS annotation and other API endpoints should
* not return a webpage, so we only print the error page when html is accepted,
* otherwise we reply with a JSON array like the BruteForceMiddleware would do.
*/
if (stripos($request->getHeader('Accept'), 'html') === false) {
http_response_code(429);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['message' => $ex->getMessage()]);
exit();
}
http_response_code(429);
Server::get(ITemplateManager::class)->printGuestPage('core', '429');
} catch (Exception $ex) {
Server::get(LoggerInterface::class)->error($ex->getMessage(), [
'app' => 'index',
'exception' => $ex,
]);
};

//show the user a detailed error page
Server::get(ITemplateManager::class)->printExceptionErrorPage($ex, 500);
} catch (Error $ex) {
try {
Server::get(LoggerInterface::class)->error($ex->getMessage(), [
'app' => 'index',
'exception' => $ex,
]);
} catch (Error $e) {
http_response_code(500);
header('Content-Type: text/plain; charset=utf-8');
print("Internal Server Error\n\n");
print("The server encountered an internal error and was unable to complete your request.\n");
print("Please contact the server administrator if this error reappears multiple times, please include the technical details below in your report.\n");
print("More details can be found in the webserver log.\n");
if (function_exists('frankenphp_handle_request')) {
$maxRequests = (int)($_SERVER['MAX_REQUESTS'] ?? 0);
for ($nbRequests = 0; !$maxRequests || $nbRequests < $maxRequests; ++$nbRequests) {
$keepRunning = \frankenphp_handle_request($handler);

throw $ex;
// Call the garbage collector to reduce the chances of it being triggered in the middle of a page generation
gc_collect_cycles();

if (!$keepRunning) {
break;
}
}
Server::get(ITemplateManager::class)->printExceptionErrorPage($ex, 500);
} else {
$handler();
}
Loading
Loading