Skip to content
Merged
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 .github/workflows/integration-sqlite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ jobs:
- name: Print logs
if: always()
run: |
cat data/nextcloud.log
cat $(./occ log:file |grep "Log file"|cut -d" " -f3)
docker ps -a
docker ps -aq | while read container ; do IMAGE=$(docker inspect --format='{{.Config.Image}}' $container); echo $IMAGE; docker logs $container; echo "\n\n" ; done

Expand Down
3 changes: 0 additions & 3 deletions build/psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4336,9 +4336,6 @@
</ParamNameMismatch>
</file>
<file src="lib/private/Route/Router.php">
<InvalidClass>
<code><![CDATA[\OC_APP]]></code>
</InvalidClass>
<InvalidNullableReturnType>
<code><![CDATA[string]]></code>
</InvalidNullableReturnType>
Expand Down
8 changes: 5 additions & 3 deletions lib/private/App/AppManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use OCP\Collaboration\Collaborators\ISearch as ICollaboratorSearch;
use OCP\Diagnostics\IEventLogger;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IAppConfig;
use OCP\ICacheFactory;
use OCP\IConfig;
use OCP\IGroup;
Expand Down Expand Up @@ -134,7 +135,8 @@ private function getUrlGenerator(): IURLGenerator {
*/
private function getEnabledAppsValues(): array {
if (!$this->enabledAppsCache) {
$values = $this->getAppConfig()->getValues(false, 'enabled');
/** @var array<string,string> */
$values = $this->getAppConfig()->searchValues('enabled', false, IAppConfig::VALUE_STRING);

$alwaysEnabledApps = $this->getAlwaysEnabledApps();
foreach ($alwaysEnabledApps as $appId) {
Expand Down Expand Up @@ -784,8 +786,8 @@ public function getAppVersion(string $appId, bool $useCache = true): string {
*
* @return array<string, string>
*/
public function getAppInstalledVersions(): array {
return $this->getAppConfig()->getAppInstalledVersions();
public function getAppInstalledVersions(bool $onlyEnabled = false): array {
return $this->getAppConfig()->getAppInstalledVersions($onlyEnabled);
}

/**
Expand Down
9 changes: 8 additions & 1 deletion lib/private/AppConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -1670,11 +1670,18 @@ private function getConfigDetailsFromLexicon(string $appId): array {
*
* @return array<string, string>
*/
public function getAppInstalledVersions(): array {
public function getAppInstalledVersions(bool $onlyEnabled = false): array {
if ($this->appVersionsCache === null) {
/** @var array<string, string> */
$this->appVersionsCache = $this->searchValues('installed_version', false, IAppConfig::VALUE_STRING);
}
if ($onlyEnabled) {
return array_filter(
$this->appVersionsCache,
fn (string $app): bool => $this->getValueString($app, 'enabled', 'no') !== 'no',
ARRAY_FILTER_USE_KEY
);
}
return $this->appVersionsCache;
}
}
4 changes: 2 additions & 2 deletions lib/private/AppFramework/Services/AppConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ public function deleteUserValue(string $userId, string $key): void {
*
* @return array<string, string>
*/
public function getAppInstalledVersions(): array {
return $this->appConfig->getAppInstalledVersions();
public function getAppInstalledVersions(bool $onlyEnabled = false): array {
return $this->appConfig->getAppInstalledVersions($onlyEnabled);
}
}
100 changes: 100 additions & 0 deletions lib/private/Route/CachingRouter.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,16 @@
use OCP\IRequest;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Matcher\CompiledUrlMatcher;
use Symfony\Component\Routing\Matcher\Dumper\CompiledUrlMatcherDumper;
use Symfony\Component\Routing\RouteCollection;

class CachingRouter extends Router {
protected ICache $cache;

protected array $legacyCreatedRoutes = [];

public function __construct(
ICacheFactory $cacheFactory,
LoggerInterface $logger,
Expand Down Expand Up @@ -54,4 +60,98 @@ public function generate($name, $parameters = [], $absolute = false) {
return $url;
}
}

private function serializeRouteCollection(RouteCollection $collection): array {
$dumper = new CompiledUrlMatcherDumper($collection);
return $dumper->getCompiledRoutes();
}

/**
* Find the route matching $url
*
* @param string $url The url to find
* @throws \Exception
* @return array
*/
public function findMatchingRoute(string $url): array {
$this->eventLogger->start('cacheroute:match', 'Match route');
$key = $this->context->getHost() . '#' . $this->context->getBaseUrl() . '#rootCollection';
$cachedRoutes = $this->cache->get($key);
if (!$cachedRoutes) {
parent::loadRoutes();
$cachedRoutes = $this->serializeRouteCollection($this->root);
$this->cache->set($key, $cachedRoutes, 3600);
Copy link
Contributor

Choose a reason for hiding this comment

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

The key does not include any version number, so this probably means its always cached also on updates?
Because then there is no way to flush on update if the routes have changed, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, the cache have a global prefix with all apps version numbers, see

$prefixClosure = function () use ($logQuery, $serverVersion): ?string {

}
$matcher = new CompiledUrlMatcher($cachedRoutes, $this->context);
$this->eventLogger->start('cacheroute:url:match', 'Symfony URL match call');
try {
$parameters = $matcher->match($url);
} catch (ResourceNotFoundException $e) {
if (!str_ends_with($url, '/')) {
// We allow links to apps/files? for backwards compatibility reasons
// However, since Symfony does not allow empty route names, the route
// we need to match is '/', so we need to append the '/' here.
try {
$parameters = $matcher->match($url . '/');
} catch (ResourceNotFoundException $newException) {
// If we still didn't match a route, we throw the original exception
throw $e;
}
} else {
throw $e;
}
}
$this->eventLogger->end('cacheroute:url:match');

$this->eventLogger->end('cacheroute:match');
return $parameters;
}

/**
* @param array{action:mixed, ...} $parameters
*/
protected function callLegacyActionRoute(array $parameters): void {
/*
* Closures cannot be serialized to cache, so for legacy routes calling an action we have to include the routes.php file again
*/
$app = $parameters['app'];
$this->useCollection($app);
parent::requireRouteFile($parameters['route-file'], $app);
$collection = $this->getCollection($app);
$parameters['action'] = $collection->get($parameters['_route'])?->getDefault('action');
parent::callLegacyActionRoute($parameters);
}

/**
* Create a \OC\Route\Route.
* Deprecated
*
* @param string $name Name of the route to create.
* @param string $pattern The pattern to match
* @param array $defaults An array of default parameter values
* @param array $requirements An array of requirements for parameters (regexes)
*/
public function create($name, $pattern, array $defaults = [], array $requirements = []): Route {
$this->legacyCreatedRoutes[] = $name;
return parent::create($name, $pattern, $defaults, $requirements);
}

/**
* Require a routes.php file
*/
protected function requireRouteFile(string $file, string $appName): void {
$this->legacyCreatedRoutes = [];
parent::requireRouteFile($file, $appName);
foreach ($this->legacyCreatedRoutes as $routeName) {
$route = $this->collection?->get($routeName);
if ($route === null) {
/* Should never happen */
throw new \Exception("Could not find route $routeName");
}
if ($route->hasDefault('action')) {
$route->setDefault('route-file', $file);
$route->setDefault('app', $appName);
}
}
}
}
10 changes: 2 additions & 8 deletions lib/private/Route/Route.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,15 +124,9 @@ public function action($class, $function = null) {
* The action to execute when this route matches, includes a file like
* it is called directly
* @param string $file
* @return void
*/
public function actionInclude($file) {
$function = function ($param) use ($file) {
unset($param['_route']);
$_GET = array_merge($_GET, $param);
unset($param);
require_once "$file";
} ;
$this->action($function);
$this->setDefault('file', $file);
return $this;
}
}
46 changes: 33 additions & 13 deletions lib/private/Route/Router.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public function __construct(
public function getRoutingFiles() {
if ($this->routingFiles === null) {
$this->routingFiles = [];
foreach (\OC_APP::getEnabledApps() as $app) {
foreach ($this->appManager->getEnabledApps() as $app) {
try {
$appPath = $this->appManager->getAppPath($app);
$file = $appPath . '/appinfo/routes.php';
Expand Down Expand Up @@ -117,7 +117,7 @@ public function loadRoutes($app = null) {
$routingFiles = $this->getRoutingFiles();

$this->eventLogger->start('route:load:attributes', 'Loading Routes from attributes');
foreach (\OC_App::getEnabledApps() as $enabledApp) {
foreach ($this->appManager->getEnabledApps() as $enabledApp) {
$this->loadAttributeRoutes($enabledApp);
}
$this->eventLogger->end('route:load:attributes');
Expand Down Expand Up @@ -312,23 +312,43 @@ public function match($url) {
$application = $this->getApplicationClass($caller[0]);
\OC\AppFramework\App::main($caller[1], $caller[2], $application->getContainer(), $parameters);
} elseif (isset($parameters['action'])) {
$action = $parameters['action'];
if (!is_callable($action)) {
throw new \Exception('not a callable action');
}
unset($parameters['action']);
unset($parameters['caller']);
$this->eventLogger->start('route:run:call', 'Run callable route');
call_user_func($action, $parameters);
$this->eventLogger->end('route:run:call');
$this->logger->warning('Deprecated action route used', ['parameters' => $parameters]);
$this->callLegacyActionRoute($parameters);
} elseif (isset($parameters['file'])) {
include $parameters['file'];
$this->logger->debug('Deprecated file route used', ['parameters' => $parameters]);
$this->includeLegacyFileRoute($parameters);
} else {
throw new \Exception('no action available');
}
$this->eventLogger->end('route:run');
}

/**
* @param array{file:mixed, ...} $parameters
*/
protected function includeLegacyFileRoute(array $parameters): void {
$param = $parameters;
unset($param['_route']);
$_GET = array_merge($_GET, $param);
unset($param);
require_once $parameters['file'];
}

/**
* @param array{action:mixed, ...} $parameters
*/
protected function callLegacyActionRoute(array $parameters): void {
$action = $parameters['action'];
if (!is_callable($action)) {
throw new \Exception('not a callable action');
}
unset($parameters['action']);
unset($parameters['caller']);
$this->eventLogger->start('route:run:call', 'Run callable route');
call_user_func($action, $parameters);
$this->eventLogger->end('route:run:call');
}

/**
* Get the url generator
*
Expand Down Expand Up @@ -492,7 +512,7 @@ private function getAttributeRoutes(string $app): array {
* @param string $file the route file location to include
* @param string $appName
*/
private function requireRouteFile($file, $appName) {
protected function requireRouteFile(string $file, string $appName): void {
$this->setupRoutes(include $file, $appName);
}

Expand Down
4 changes: 2 additions & 2 deletions lib/private/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -605,7 +605,7 @@ public function __construct($webRoot, \OC\Config $config) {
$prefixClosure = function () use ($logQuery, $serverVersion): ?string {
if (!$logQuery) {
try {
$v = \OCP\Server::get(IAppConfig::class)->getAppInstalledVersions();
$v = \OCP\Server::get(IAppConfig::class)->getAppInstalledVersions(true);
} catch (\Doctrine\DBAL\Exception $e) {
// Database service probably unavailable
// Probably related to https://github.com/nextcloud/server/issues/37424
Expand All @@ -620,7 +620,7 @@ public function __construct($webRoot, \OC\Config $config) {
];
}
$v['core'] = implode(',', $serverVersion->getVersion());
$version = implode(',', $v);
$version = implode(',', array_keys($v)) . implode(',', $v);
$instanceId = \OC_Util::getInstanceId();
$path = \OC::$SERVERROOT;
return md5($instanceId . '-' . $version . '-' . $path);
Expand Down
2 changes: 1 addition & 1 deletion lib/private/TemplateLayout.php
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ public function getPageTemplate(string $renderAs, string $appId): ITemplate {

if ($this->config->getSystemValueBool('installed', false)) {
if (empty(self::$versionHash)) {
$v = $this->appManager->getAppInstalledVersions();
$v = $this->appManager->getAppInstalledVersions(true);
$v['core'] = implode('.', $this->serverVersion->getVersion());
self::$versionHash = substr(md5(implode(',', $v)), 0, 8);
}
Expand Down
2 changes: 1 addition & 1 deletion lib/public/App/IAppManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public function getAppVersion(string $appId, bool $useCache = true): string;
* @return array<string, string>
* @since 32.0.0
*/
public function getAppInstalledVersions(): array;
public function getAppInstalledVersions(bool $onlyEnabled = false): array;

/**
* Returns the app icon or null if none is found
Expand Down
2 changes: 1 addition & 1 deletion lib/public/IAppConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -514,5 +514,5 @@ public function getFilteredValues($app);
* @return array<string, string>
* @since 32.0.0
*/
public function getAppInstalledVersions(): array;
public function getAppInstalledVersions(bool $onlyEnabled = false): array;
}
4 changes: 3 additions & 1 deletion lib/public/Route/IRoute.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ public function method($method);
* it is called directly
*
* @param string $file
* @return void
* @return $this
* @since 7.0.0
* @deprecated 32.0.0 Use a proper controller instead
*/
public function actionInclude($file);

Expand Down Expand Up @@ -70,6 +71,7 @@ public function delete();
* This function is called with $class set to a callable or
* to the class with $function
* @since 7.0.0
* @deprecated 32.0.0 Use a proper controller instead
*/
public function action($class, $function = null);

Expand Down
11 changes: 11 additions & 0 deletions tests/lib/App/AppManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,17 @@ protected function getAppConfig() {
return $values;
}
});
$config->expects($this->any())
->method('searchValues')
->willReturnCallback(function ($key, $lazy, $type) use (&$appConfig) {
$values = [];
foreach ($appConfig as $appid => $appData) {
if (isset($appData[$key])) {
$values[$appid] = $appData[$key];
}
}
return $values;
});

return $config;
}
Expand Down
Loading
Loading