Compare commits

...

13 Commits

Author SHA1 Message Date
Côme Chilliet
139ae8a88c fix: Use a CappedMemoryCache instead of an array to cache stuff in user_ldap
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2026-04-07 16:26:02 +02:00
Côme Chilliet
c6b5d3e2d3 fix: Remove static var is Access class
It’s actually more correct to cache this per-instance.
What’s less clear is whether this can always fit in memory.

Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2026-04-07 16:23:22 +02:00
Côme Chilliet
bbadda93bb fix: Remove static vars in preview Generator
There is only one instance so caching in a property is enough.
There were two levels of caching, removed one.

Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2026-04-07 16:18:33 +02:00
Côme Chilliet
b47c09be9c fix: Fix TemplateLayout tests
The behaviour of getAppNameFromPath is really different from what was
 mocked, so I’m not sure whether the class behaves as initially
 intended. I adapted the test to match the class behavior for now.

Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2026-04-07 16:08:01 +02:00
Côme Chilliet
11c9e5a83b fix: Add a factory for Memcached object instead of a static var
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2026-03-26 23:16:57 +01:00
Côme Chilliet
8c0a7fdf80 chore: Remove static vars in TemplateLayout
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2026-03-26 23:16:57 +01:00
Côme Chilliet
4db0114b2d chore: Move Util static property to top of the file
It’s not easy to remove this one but at least make it visible

Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2026-03-26 23:16:41 +01:00
Côme Chilliet
fdee34c8cb fixup! fix: Remove static vars in trashbin, versions and storages
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2026-03-26 22:28:42 +01:00
Côme Chilliet
91ba91a9d2 fix: Remove static vars in trashbin, versions and storages
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2026-03-24 14:22:04 +01:00
Côme Chilliet
aa7461afb6 feat: Add a psalm plugin to forbid static properties and variables
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2026-03-24 14:21:28 +01:00
Côme Chilliet
d6c8333f78 chore: Remove types from const properties
Support was added in PHP 8.3 and we need to support 8.2

Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2026-03-24 14:21:27 +01:00
Côme Chilliet
83cb75e3a4 fix: Remove static vars in TaskProcessing, TextProcessing, TextToImage
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2026-03-24 14:21:27 +01:00
Côme Chilliet
0295da5b42 fix: Remove static var in Memcache/Redis
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2026-03-24 14:21:04 +01:00
27 changed files with 385 additions and 233 deletions

View File

@@ -54,7 +54,8 @@ class BackendService {
/** @var callable[] */
private $configHandlerLoaders = [];
private $configHandlers = [];
private array $configHandlers = [];
private bool $eventSent = false;
public function __construct(
protected readonly IAppConfig $appConfig,
@@ -71,14 +72,14 @@ class BackendService {
$this->backendProviders[] = $provider;
}
private function callForRegistrations() {
static $eventSent = false;
if (!$eventSent) {
private function callForRegistrations(): void {
$instance = Server::get(self::class);
if (!$instance->eventSent) {
Server::get(IEventDispatcher::class)->dispatch(
'OCA\\Files_External::loadAdditionalBackends',
new GenericEvent()
);
$eventSent = true;
$instance->eventSent = true;
}
}

View File

@@ -29,7 +29,7 @@ class RestoreAllFiles extends Base {
private const SCOPE_USER = 1;
private const SCOPE_GROUPFOLDERS = 2;
private static array $SCOPE_MAP = [
private const SCOPE_MAP = [
'user' => self::SCOPE_USER,
'groupfolders' => self::SCOPE_GROUPFOLDERS,
'all' => self::SCOPE_ALL
@@ -218,8 +218,8 @@ class RestoreAllFiles extends Base {
}
protected function parseScope(string $scope): int {
if (isset(self::$SCOPE_MAP[$scope])) {
return self::$SCOPE_MAP[$scope];
if (isset(self::SCOPE_MAP[$scope])) {
return self::SCOPE_MAP[$scope];
}
throw new InvalidOptionException("Invalid scope '$scope'");

View File

@@ -60,7 +60,7 @@ class Storage {
private static $sourcePathAndUser = [];
private static $max_versions_per_interval = [
private const MAX_VERSIONS_PER_INTERVAL = [
//first 10sec, one version every 2sec
1 => ['intervalEndsAfter' => 10, 'step' => 2],
//next minute, one version every 10sec
@@ -763,11 +763,11 @@ class Storage {
$toDelete = []; // versions we want to delete
$interval = 1;
$step = Storage::$max_versions_per_interval[$interval]['step'];
if (Storage::$max_versions_per_interval[$interval]['intervalEndsAfter'] === -1) {
$step = Storage::MAX_VERSIONS_PER_INTERVAL[$interval]['step'];
if (Storage::MAX_VERSIONS_PER_INTERVAL[$interval]['intervalEndsAfter'] === -1) {
$nextInterval = -1;
} else {
$nextInterval = $time - Storage::$max_versions_per_interval[$interval]['intervalEndsAfter'];
$nextInterval = $time - Storage::MAX_VERSIONS_PER_INTERVAL[$interval]['intervalEndsAfter'];
}
$firstVersion = reset($versions);
@@ -797,12 +797,12 @@ class Storage {
$newInterval = false; // version checked so we can move to the next one
} else { // time to move on to the next interval
$interval++;
$step = Storage::$max_versions_per_interval[$interval]['step'];
$step = Storage::MAX_VERSIONS_PER_INTERVAL[$interval]['step'];
$nextVersion = $prevTimestamp - $step;
if (Storage::$max_versions_per_interval[$interval]['intervalEndsAfter'] === -1) {
if (Storage::MAX_VERSIONS_PER_INTERVAL[$interval]['intervalEndsAfter'] === -1) {
$nextInterval = -1;
} else {
$nextInterval = $time - Storage::$max_versions_per_interval[$interval]['intervalEndsAfter'];
$nextInterval = $time - Storage::MAX_VERSIONS_PER_INTERVAL[$interval]['intervalEndsAfter'];
}
$newInterval = true; // we changed the interval -> check same version with new interval
}

View File

@@ -15,6 +15,7 @@ use OCA\User_LDAP\Exceptions\NoMoreResults;
use OCA\User_LDAP\Mapping\AbstractMapping;
use OCA\User_LDAP\User\Manager;
use OCA\User_LDAP\User\OfflineUser;
use OCP\Cache\CappedMemoryCache;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\HintException;
use OCP\IAppConfig;
@@ -48,6 +49,7 @@ class Access extends LDAPUtility {
protected $groupMapper;
private string $lastCookie = '';
private CappedMemoryCache $intermediates;
public function __construct(
ILDAPWrapper $ldap,
@@ -61,6 +63,7 @@ class Access extends LDAPUtility {
) {
parent::__construct($ldap);
$this->userManager->setLdapAccess($this);
$this->intermediates = new CappedMemoryCache();
}
/**
@@ -483,8 +486,7 @@ class Access extends LDAPUtility {
* @throws \Exception
*/
public function dn2ocname($fdn, $ldapName = null, $isUser = true, &$newlyMapped = null, ?array $record = null, bool $autoMapping = true) {
static $intermediates = [];
if (isset($intermediates[($isUser ? 'user-' : 'group-') . $fdn])) {
if (isset($this->intermediates[($isUser ? 'user-' : 'group-') . $fdn])) {
return false; // is a known intermediate
}
@@ -531,7 +533,7 @@ class Access extends LDAPUtility {
$record = $this->readAttributes($fdn, $attributesToRead, $filter);
if ($record === false) {
$this->logger->debug('Cannot read attributes for ' . $fdn . '. Skipping.', ['filter' => $filter]);
$intermediates[($isUser ? 'user-' : 'group-') . $fdn] = true;
$this->intermediates[($isUser ? 'user-' : 'group-') . $fdn] = true;
return false;
}
}
@@ -578,7 +580,7 @@ class Access extends LDAPUtility {
$ldapName = $record[$nameAttribute];
if (!isset($ldapName[0]) || empty($ldapName[0])) {
$this->logger->debug('No or empty name for ' . $fdn . ' with filter ' . $filter . '.', ['app' => 'user_ldap']);
$intermediates['group-' . $fdn] = true;
$this->intermediates['group-' . $fdn] = true;
return false;
}
$ldapName = $ldapName[0];

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
use PhpParser\Node\Stmt\Property;
use Psalm\CodeLocation;
use Psalm\IssueBuffer;
use Psalm\Plugin\EventHandler\AfterClassLikeVisitInterface;
use Psalm\Plugin\EventHandler\AfterStatementAnalysisInterface;
use Psalm\Plugin\EventHandler\Event\AfterClassLikeVisitEvent;
use Psalm\Plugin\EventHandler\Event\AfterStatementAnalysisEvent;
/**
* Complains about static property in classes and static vars in methods
*/
class StaticVarsChecker implements AfterClassLikeVisitInterface, AfterStatementAnalysisInterface {
public static function afterClassLikeVisit(AfterClassLikeVisitEvent $event): void {
$classLike = $event->getStmt();
$statementsSource = $event->getStatementsSource();
foreach ($classLike->stmts as $stmt) {
if ($stmt instanceof Property) {
if ($stmt->isStatic()) {
IssueBuffer::maybeAdd(
// ImpureStaticProperty is close enough, all static properties are impure to my eyes
new \Psalm\Issue\ImpureStaticProperty(
'Static property should not be used as they do not follow requests lifecycle',
new CodeLocation($statementsSource, $stmt),
)
);
}
}
}
}
public static function afterStatementAnalysis(AfterStatementAnalysisEvent $event): ?bool {
$stmt = $event->getStmt();
if ($stmt instanceof PhpParser\Node\Stmt\Static_) {
IssueBuffer::maybeAdd(
// Same logic
new \Psalm\Issue\ImpureStaticVariable(
'Static var should not be used as they do not follow requests lifecycle and are hard to reset',
new CodeLocation($event->getStatementsSource(), $stmt),
)
);
}
return null;
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
class StaticVarsTest {
public static $forbiddenStaticProperty;
protected static $forbiddenProtectedStaticProperty;
private static $forbiddenPrivateStaticProperty;
private static $forbiddenPrivateStaticPropertyWithValue = [];
public function normalFunction(): void {
static $forbiddenStaticVar = false;
}
public static function staticFunction(): void {
static $forbiddenStaticVar = false;
}
}

View File

@@ -277,7 +277,7 @@ class InstalledVersions
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C' && is_file(__DIR__ . '/installed.php')) {
if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = include __DIR__ . '/installed.php';
} else {
self::$installed = array();
@@ -378,7 +378,7 @@ class InstalledVersions
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C' && is_file(__DIR__ . '/installed.php')) {
if (substr(__DIR__, -8, 1) !== 'C') {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require __DIR__ . '/installed.php';
self::$installed = $required;

View File

@@ -1,5 +1,68 @@
{
"packages": [],
"dev": false,
"dev-package-names": []
"packages": [
{
"name": "bamarni/composer-bin-plugin",
"version": "1.9.1",
"version_normalized": "1.9.1.0",
"source": {
"type": "git",
"url": "https://github.com/bamarni/composer-bin-plugin.git",
"reference": "641d0663f5ac270b1aeec4337b7856f76204df47"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/bamarni/composer-bin-plugin/zipball/641d0663f5ac270b1aeec4337b7856f76204df47",
"reference": "641d0663f5ac270b1aeec4337b7856f76204df47",
"shasum": ""
},
"require": {
"composer-plugin-api": "^2.0",
"php": "^7.2.5 || ^8.0"
},
"require-dev": {
"composer/composer": "^2.2.26",
"ext-json": "*",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan": "^1.8 || ^2.0",
"phpstan/phpstan-phpunit": "^1.1 || ^2.0",
"phpunit/phpunit": "^8.5 || ^9.6 || ^10.0",
"symfony/console": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0",
"symfony/finder": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0",
"symfony/process": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0"
},
"time": "2026-02-04T10:18:12+00:00",
"type": "composer-plugin",
"extra": {
"class": "Bamarni\\Composer\\Bin\\BamarniBinPlugin"
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Bamarni\\Composer\\Bin\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "No conflicts for your bin dependencies",
"keywords": [
"composer",
"conflict",
"dependency",
"executable",
"isolation",
"tool"
],
"support": {
"issues": "https://github.com/bamarni/composer-bin-plugin/issues",
"source": "https://github.com/bamarni/composer-bin-plugin/tree/1.9.1"
},
"install-path": "../bamarni/composer-bin-plugin"
}
],
"dev": true,
"dev-package-names": [
"bamarni/composer-bin-plugin"
]
}

View File

@@ -3,21 +3,30 @@
'name' => '__root__',
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => '671cec33f134e670bb21c5e3c49c685bd78fc339',
'reference' => '91ba91a9d21106ee519f065d6feea2f8f757651c',
'type' => 'library',
'install_path' => __DIR__ . '/../../../',
'aliases' => array(),
'dev' => false,
'dev' => true,
),
'versions' => array(
'__root__' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => '671cec33f134e670bb21c5e3c49c685bd78fc339',
'reference' => '91ba91a9d21106ee519f065d6feea2f8f757651c',
'type' => 'library',
'install_path' => __DIR__ . '/../../../',
'aliases' => array(),
'dev_requirement' => false,
),
'bamarni/composer-bin-plugin' => array(
'pretty_version' => '1.9.1',
'version' => '1.9.1.0',
'reference' => '641d0663f5ac270b1aeec4337b7856f76204df47',
'type' => 'composer-plugin',
'install_path' => __DIR__ . '/../bamarni/composer-bin-plugin',
'aliases' => array(),
'dev_requirement' => true,
),
),
);

View File

@@ -24,7 +24,7 @@ use OCP\FilesMetadata\IMetadataQuery;
*/
class SearchBuilder {
/** @var array<string, string> */
protected static $searchOperatorMap = [
private const SEARCH_OPERATOR_MAP = [
ISearchComparison::COMPARE_LIKE => 'iLike',
ISearchComparison::COMPARE_LIKE_CASE_SENSITIVE => 'like',
ISearchComparison::COMPARE_EQUAL => 'eq',
@@ -37,7 +37,7 @@ class SearchBuilder {
];
/** @var array<string, string> */
protected static $searchOperatorNegativeMap = [
private const SEARCH_OPERATOR_NEGATIVE_MAP = [
ISearchComparison::COMPARE_LIKE => 'notLike',
ISearchComparison::COMPARE_LIKE_CASE_SENSITIVE => 'notLike',
ISearchComparison::COMPARE_EQUAL => 'neq',
@@ -50,7 +50,7 @@ class SearchBuilder {
];
/** @var array<string, string> */
protected static $fieldTypes = [
private const FIELD_TYPES = [
'mimetype' => 'string',
'mtime' => 'integer',
'name' => 'string',
@@ -69,14 +69,14 @@ class SearchBuilder {
];
/** @var array<string, int|string> */
protected static $paramTypeMap = [
private const PARAM_TYPE_MAP = [
'string' => IQueryBuilder::PARAM_STR,
'integer' => IQueryBuilder::PARAM_INT,
'boolean' => IQueryBuilder::PARAM_BOOL,
];
/** @var array<string, int> */
protected static $paramArrayTypeMap = [
private const PARAM_ARRAY_TYPE_MAP = [
'string' => IQueryBuilder::PARAM_STR_ARRAY,
'integer' => IQueryBuilder::PARAM_INT_ARRAY,
'boolean' => IQueryBuilder::PARAM_INT_ARRAY,
@@ -134,7 +134,7 @@ class SearchBuilder {
case ISearchBinaryOperator::OPERATOR_NOT:
$negativeOperator = $operator->getArguments()[0];
if ($negativeOperator instanceof ISearchComparison) {
return $this->searchComparisonToDBExpr($builder, $negativeOperator, self::$searchOperatorNegativeMap, $metadataQuery);
return $this->searchComparisonToDBExpr($builder, $negativeOperator, self::SEARCH_OPERATOR_NEGATIVE_MAP, $metadataQuery);
} else {
throw new \InvalidArgumentException('Binary operators inside "not" is not supported');
}
@@ -147,7 +147,7 @@ class SearchBuilder {
throw new \InvalidArgumentException('Invalid operator type: ' . $operator->getType());
}
} elseif ($operator instanceof ISearchComparison) {
return $this->searchComparisonToDBExpr($builder, $operator, self::$searchOperatorMap, $metadataQuery);
return $this->searchComparisonToDBExpr($builder, $operator, self::SEARCH_OPERATOR_MAP, $metadataQuery);
} else {
throw new \InvalidArgumentException('Invalid operator type: ' . get_class($operator));
}
@@ -193,7 +193,7 @@ class SearchBuilder {
* @return list{string, ParamValue, string, string}
*/
private function getOperatorFieldAndValueInner(string $field, mixed $value, string $type, bool $pathEqHash): array {
$paramType = self::$fieldTypes[$field];
$paramType = self::FIELD_TYPES[$field];
if ($type === ISearchComparison::COMPARE_IN) {
$resultField = $field;
$values = [];
@@ -263,10 +263,10 @@ class SearchBuilder {
'upload_time' => ['eq', 'gt', 'lt', 'gte', 'lte'],
];
if (!isset(self::$fieldTypes[$operator->getField()])) {
if (!isset(self::FIELD_TYPES[$operator->getField()])) {
throw new \InvalidArgumentException('Unsupported comparison field ' . $operator->getField());
}
$type = self::$fieldTypes[$operator->getField()];
$type = self::FIELD_TYPES[$operator->getField()];
if ($operator->getType() === ISearchComparison::COMPARE_IN) {
if (!is_array($operator->getValue())) {
throw new \InvalidArgumentException('Invalid type for field ' . $operator->getField());
@@ -317,9 +317,9 @@ class SearchBuilder {
$value = $value->getTimestamp();
}
if (is_array($value)) {
$type = self::$paramArrayTypeMap[$paramType];
$type = self::PARAM_ARRAY_TYPE_MAP[$paramType];
} else {
$type = self::$paramTypeMap[$paramType];
$type = self::PARAM_TYPE_MAP[$paramType];
}
return $builder->createNamedParameter($value, $type);
}

View File

@@ -7,73 +7,18 @@
*/
namespace OC\Memcache;
use OC\SystemConfig;
use OCP\HintException;
use OCP\IConfig;
use OCP\IMemcache;
use OCP\Server;
class Memcached extends Cache implements IMemcache {
use CASTrait;
/**
* @var \Memcached $cache
*/
private static $cache = null;
private \Memcached $cache;
use CADTrait;
public function __construct($prefix = '') {
parent::__construct($prefix);
if (is_null(self::$cache)) {
self::$cache = new \Memcached();
$defaultOptions = [
\Memcached::OPT_CONNECT_TIMEOUT => 50,
\Memcached::OPT_RETRY_TIMEOUT => 50,
\Memcached::OPT_SEND_TIMEOUT => 50,
\Memcached::OPT_RECV_TIMEOUT => 50,
\Memcached::OPT_POLL_TIMEOUT => 50,
// Enable compression
\Memcached::OPT_COMPRESSION => true,
// Turn on consistent hashing
\Memcached::OPT_LIBKETAMA_COMPATIBLE => true,
// Enable Binary Protocol
\Memcached::OPT_BINARY_PROTOCOL => true,
];
/**
* By default enable igbinary serializer if available
*
* Psalm checks depend on if igbinary is installed or not with memcached
* @psalm-suppress RedundantCondition
* @psalm-suppress TypeDoesNotContainType
*/
if (\Memcached::HAVE_IGBINARY) {
$defaultOptions[\Memcached::OPT_SERIALIZER]
= \Memcached::SERIALIZER_IGBINARY;
}
$options = Server::get(IConfig::class)->getSystemValue('memcached_options', []);
if (is_array($options)) {
$options = $options + $defaultOptions;
self::$cache->setOptions($options);
} else {
throw new HintException("Expected 'memcached_options' config to be an array, got $options");
}
$servers = Server::get(SystemConfig::class)->getValue('memcached_servers');
if (!$servers) {
$server = Server::get(SystemConfig::class)->getValue('memcached_server');
if ($server) {
$servers = [$server];
} else {
$servers = [['localhost', 11211]];
}
}
self::$cache->addServers($servers);
}
$this->cache = \OCP\Server::get(MemcachedFactory::class)->getInstance();
}
/**
@@ -84,8 +29,8 @@ class Memcached extends Cache implements IMemcache {
}
public function get($key) {
$result = self::$cache->get($this->getNameSpace() . $key);
if ($result === false && self::$cache->getResultCode() === \Memcached::RES_NOTFOUND) {
$result = $this->cache->get($this->getNameSpace() . $key);
if ($result === false && $this->cache->getResultCode() === \Memcached::RES_NOTFOUND) {
return null;
} else {
return $result;
@@ -94,26 +39,26 @@ class Memcached extends Cache implements IMemcache {
public function set($key, $value, $ttl = 0) {
if ($ttl > 0) {
$result = self::$cache->set($this->getNameSpace() . $key, $value, $ttl);
$result = $this->cache->set($this->getNameSpace() . $key, $value, $ttl);
} else {
$result = self::$cache->set($this->getNameSpace() . $key, $value);
$result = $this->cache->set($this->getNameSpace() . $key, $value);
}
return $result || $this->isSuccess();
}
public function hasKey($key) {
self::$cache->get($this->getNameSpace() . $key);
return self::$cache->getResultCode() === \Memcached::RES_SUCCESS;
$this->cache->get($this->getNameSpace() . $key);
return $this->cache->getResultCode() === \Memcached::RES_SUCCESS;
}
public function remove($key) {
$result = self::$cache->delete($this->getNameSpace() . $key);
return $result || $this->isSuccess() || self::$cache->getResultCode() === \Memcached::RES_NOTFOUND;
$result = $this->cache->delete($this->getNameSpace() . $key);
return $result || $this->isSuccess() || $this->cache->getResultCode() === \Memcached::RES_NOTFOUND;
}
public function clear($prefix = '') {
// Newer Memcached doesn't like getAllKeys(), flush everything
self::$cache->flush();
$this->cache->flush();
return true;
}
@@ -126,7 +71,7 @@ class Memcached extends Cache implements IMemcache {
* @return bool
*/
public function add($key, $value, $ttl = 0) {
$result = self::$cache->add($this->getPrefix() . $key, $value, $ttl);
$result = $this->cache->add($this->getPrefix() . $key, $value, $ttl);
return $result || $this->isSuccess();
}
@@ -139,9 +84,9 @@ class Memcached extends Cache implements IMemcache {
*/
public function inc($key, $step = 1) {
$this->add($key, 0);
$result = self::$cache->increment($this->getPrefix() . $key, $step);
$result = $this->cache->increment($this->getPrefix() . $key, $step);
if (self::$cache->getResultCode() !== \Memcached::RES_SUCCESS) {
if ($this->cache->getResultCode() !== \Memcached::RES_SUCCESS) {
return false;
}
@@ -156,9 +101,9 @@ class Memcached extends Cache implements IMemcache {
* @return int | bool
*/
public function dec($key, $step = 1) {
$result = self::$cache->decrement($this->getPrefix() . $key, $step);
$result = $this->cache->decrement($this->getPrefix() . $key, $step);
if (self::$cache->getResultCode() !== \Memcached::RES_SUCCESS) {
if ($this->cache->getResultCode() !== \Memcached::RES_SUCCESS) {
return false;
}
@@ -170,6 +115,6 @@ class Memcached extends Cache implements IMemcache {
}
private function isSuccess(): bool {
return self::$cache->getResultCode() === \Memcached::RES_SUCCESS;
return $this->cache->getResultCode() === \Memcached::RES_SUCCESS;
}
}

View File

@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OC\Memcache;
use OC\SystemConfig;
use OCP\HintException;
use OCP\IConfig;
use OCP\Server;
/**
* Factory for \Memcached instance, to create a singleton
*/
class MemcachedFactory {
private ?\Memcached $instance = null;
public function getInstance(): \Memcached {
if ($this->instance === null) {
$this->init();
}
return $this->instance;
}
/** @psalm-assert \Memcached $this->instance */
public function init(): void {
$this->instance = new \Memcached();
$defaultOptions = [
\Memcached::OPT_CONNECT_TIMEOUT => 50,
\Memcached::OPT_RETRY_TIMEOUT => 50,
\Memcached::OPT_SEND_TIMEOUT => 50,
\Memcached::OPT_RECV_TIMEOUT => 50,
\Memcached::OPT_POLL_TIMEOUT => 50,
// Enable compression
\Memcached::OPT_COMPRESSION => true,
// Turn on consistent hashing
\Memcached::OPT_LIBKETAMA_COMPATIBLE => true,
// Enable Binary Protocol
\Memcached::OPT_BINARY_PROTOCOL => true,
];
/**
* By default enable igbinary serializer if available
*
* Psalm checks depend on if igbinary is installed or not with memcached
* @psalm-suppress RedundantCondition
* @psalm-suppress TypeDoesNotContainType
*/
if (\Memcached::HAVE_IGBINARY) {
$defaultOptions[\Memcached::OPT_SERIALIZER]
= \Memcached::SERIALIZER_IGBINARY;
}
$options = Server::get(IConfig::class)->getSystemValue('memcached_options', []);
if (is_array($options)) {
$options = $options + $defaultOptions;
$this->instance->setOptions($options);
} else {
throw new HintException("Expected 'memcached_options' config to be an array, got $options");
}
$servers = Server::get(SystemConfig::class)->getValue('memcached_servers');
if (!$servers) {
$server = Server::get(SystemConfig::class)->getValue('memcached_server');
if ($server) {
$servers = [$server];
} else {
$servers = [['localhost', 11211]];
}
}
$this->instance->addServers($servers);
}
}

View File

@@ -7,6 +7,7 @@
*/
namespace OC\Memcache;
use OC\RedisFactory;
use OCP\IMemcacheTTL;
use OCP\Server;
@@ -37,24 +38,18 @@ class Redis extends Cache implements IMemcacheTTL {
private const MAX_TTL = 30 * 24 * 60 * 60; // 1 month
/**
* @var \Redis|\RedisCluster $cache
*/
private static $cache = null;
private \Redis|\RedisCluster $cache;
public function __construct($prefix = '', string $logFile = '') {
parent::__construct($prefix);
$this->cache = \OCP\Server::get(RedisFactory::class)->getInstance();
}
/**
* @return \Redis|\RedisCluster|null
* @throws \Exception
*/
public function getCache() {
if (is_null(self::$cache)) {
self::$cache = Server::get('RedisFactory')->getInstance();
}
return self::$cache;
public function getCache(): \Redis|\RedisCluster {
return $this->cache;
}
public function get($key) {

View File

@@ -33,6 +33,8 @@ class Generator {
public const SEMAPHORE_ID_ALL = 0x0a11;
public const SEMAPHORE_ID_NEW = 0x07ea;
private array $cachedNumConcurrentPreviews = [];
public function __construct(
private readonly IConfig $config,
private readonly IAppConfig $appConfig,
@@ -259,22 +261,14 @@ class Generator {
*
* @return int number of concurrent threads, or 0 if it cannot be determined
*/
public static function getHardwareConcurrency(): int {
static $width;
if (!isset($width)) {
if (function_exists('ini_get')) {
$openBasedir = ini_get('open_basedir');
if (empty($openBasedir) || strpos($openBasedir, '/proc/cpuinfo') !== false) {
$width = is_readable('/proc/cpuinfo') ? substr_count(file_get_contents('/proc/cpuinfo'), 'processor') : 0;
} else {
$width = 0;
}
} else {
$width = 0;
private static function getHardwareConcurrency(): int {
if (function_exists('ini_get')) {
$openBasedir = ini_get('open_basedir');
if (empty($openBasedir) || strpos($openBasedir, '/proc/cpuinfo') !== false) {
return is_readable('/proc/cpuinfo') ? substr_count(file_get_contents('/proc/cpuinfo'), 'processor') : 0;
}
}
return $width;
return 0;
}
/**
@@ -293,9 +287,8 @@ class Generator {
* @return int number of concurrent preview generations, or -1 if $type is invalid
*/
public function getNumConcurrentPreviews(string $type): int {
static $cached = [];
if (array_key_exists($type, $cached)) {
return $cached[$type];
if (array_key_exists($type, $this->cachedNumConcurrentPreviews)) {
return $this->cachedNumConcurrentPreviews[$type];
}
$hardwareConcurrency = self::getHardwareConcurrency();
@@ -304,16 +297,16 @@ class Generator {
$fallback = $hardwareConcurrency > 0 ? $hardwareConcurrency * 2 : 8;
$concurrency_all = $this->config->getSystemValueInt($type, $fallback);
$concurrency_new = $this->getNumConcurrentPreviews('preview_concurrency_new');
$cached[$type] = max($concurrency_all, $concurrency_new);
$this->cachedNumConcurrentPreviews[$type] = max($concurrency_all, $concurrency_new);
break;
case 'preview_concurrency_new':
$fallback = $hardwareConcurrency > 0 ? $hardwareConcurrency : 4;
$cached[$type] = $this->config->getSystemValueInt($type, $fallback);
$this->cachedNumConcurrentPreviews[$type] = $this->config->getSystemValueInt($type, $fallback);
break;
default:
return -1;
}
return $cached[$type];
return $this->cachedNumConcurrentPreviews[$type];
}
/**

View File

@@ -131,15 +131,14 @@ class RedisFactory {
}
public function getInstance(): \Redis|\RedisCluster {
if (!$this->isAvailable()) {
throw new \Exception('Redis support is not available');
}
if ($this->instance === null) {
if (!$this->isAvailable()) {
throw new \Exception('Redis support is not available');
}
$this->create();
}
if ($this->instance === null) {
throw new \Exception('Redis support is not available');
if ($this->instance === null) {
throw new \Exception('Redis support is not available');
}
}
return $this->instance;

View File

@@ -669,10 +669,7 @@ class Server extends ServerContainer implements IServerContainer {
});
$this->registerAlias(ICacheFactory::class, Factory::class);
$this->registerService('RedisFactory', function (Server $c) {
$systemConfig = $c->get(SystemConfig::class);
return new RedisFactory($systemConfig, $c->get(IEventLogger::class));
});
$this->registerDeprecatedAlias('RedisFactory', RedisFactory::class);
$this->registerService(\OCP\Activity\IManager::class, function (Server $c) {
$l10n = $this->get(IFactory::class)->get('lib');

View File

@@ -76,12 +76,12 @@ class Task extends Entity {
/**
* @var string[]
*/
public static array $columns = ['id', 'last_updated', 'type', 'input', 'output', 'status', 'user_id', 'app_id', 'custom_id', 'completion_expected_at', 'error_message', 'progress', 'webhook_uri', 'webhook_method', 'scheduled_at', 'started_at', 'ended_at', 'allow_cleanup', 'user_facing_error_message', 'include_watermark'];
public const COLUMNS = ['id', 'last_updated', 'type', 'input', 'output', 'status', 'user_id', 'app_id', 'custom_id', 'completion_expected_at', 'error_message', 'progress', 'webhook_uri', 'webhook_method', 'scheduled_at', 'started_at', 'ended_at', 'allow_cleanup', 'user_facing_error_message', 'include_watermark'];
/**
* @var string[]
*/
public static array $fields = ['id', 'lastUpdated', 'type', 'input', 'output', 'status', 'userId', 'appId', 'customId', 'completionExpectedAt', 'errorMessage', 'progress', 'webhookUri', 'webhookMethod', 'scheduledAt', 'startedAt', 'endedAt', 'allowCleanup', 'userFacingErrorMessage', 'includeWatermark'];
public const FIELDS = ['id', 'lastUpdated', 'type', 'input', 'output', 'status', 'userId', 'appId', 'customId', 'completionExpectedAt', 'errorMessage', 'progress', 'webhookUri', 'webhookMethod', 'scheduledAt', 'startedAt', 'endedAt', 'allowCleanup', 'userFacingErrorMessage', 'includeWatermark'];
public function __construct() {
@@ -109,9 +109,9 @@ class Task extends Entity {
}
public function toRow(): array {
return array_combine(self::$columns, array_map(function ($field) {
return array_combine(self::COLUMNS, array_map(function ($field) {
return $this->{'get' . ucfirst($field)}();
}, self::$fields));
}, self::FIELDS));
}
public static function fromPublicTask(OCPTask $task): self {

View File

@@ -38,7 +38,7 @@ class TaskMapper extends QBMapper {
*/
public function find(int $id): Task {
$qb = $this->db->getQueryBuilder();
$qb->select(Task::$columns)
$qb->select(Task::COLUMNS)
->from($this->tableName)
->where($qb->expr()->eq('id', $qb->createPositionalParameter($id)));
return $this->findEntity($qb);
@@ -53,7 +53,7 @@ class TaskMapper extends QBMapper {
*/
public function findOldestScheduledByType(array $taskTypes, array $taskIdsToIgnore): Task {
$qb = $this->db->getQueryBuilder();
$qb->select(Task::$columns)
$qb->select(Task::COLUMNS)
->from($this->tableName)
->where($qb->expr()->eq('status', $qb->createPositionalParameter(\OCP\TaskProcessing\Task::STATUS_SCHEDULED, IQueryBuilder::PARAM_INT)))
->setMaxResults(1)
@@ -85,7 +85,7 @@ class TaskMapper extends QBMapper {
*/
public function findByIdAndUser(int $id, ?string $userId): Task {
$qb = $this->db->getQueryBuilder();
$qb->select(Task::$columns)
$qb->select(Task::COLUMNS)
->from($this->tableName)
->where($qb->expr()->eq('id', $qb->createPositionalParameter($id)));
if ($userId === null) {
@@ -105,7 +105,7 @@ class TaskMapper extends QBMapper {
*/
public function findByUserAndTaskType(?string $userId, ?string $taskType = null, ?string $customId = null): array {
$qb = $this->db->getQueryBuilder();
$qb->select(Task::$columns)
$qb->select(Task::COLUMNS)
->from($this->tableName)
->where($qb->expr()->eq('user_id', $qb->createPositionalParameter($userId)));
if ($taskType !== null) {
@@ -126,7 +126,7 @@ class TaskMapper extends QBMapper {
*/
public function findUserTasksByApp(?string $userId, string $appId, ?string $customId = null): array {
$qb = $this->db->getQueryBuilder();
$qb->select(Task::$columns)
$qb->select(Task::COLUMNS)
->from($this->tableName)
->where($qb->expr()->eq('user_id', $qb->createPositionalParameter($userId)))
->andWhere($qb->expr()->eq('app_id', $qb->createPositionalParameter($appId)));
@@ -151,7 +151,7 @@ class TaskMapper extends QBMapper {
?string $userId, ?string $taskType = null, ?string $appId = null, ?string $customId = null,
?int $status = null, ?int $scheduleAfter = null, ?int $endedBefore = null): array {
$qb = $this->db->getQueryBuilder();
$qb->select(Task::$columns)
$qb->select(Task::COLUMNS)
->from($this->tableName);
// empty string: no userId filter
@@ -205,7 +205,7 @@ class TaskMapper extends QBMapper {
*/
public function getTasksToCleanup(int $timeout, bool $force = false): \Generator {
$qb = $this->db->getQueryBuilder();
$qb->select(Task::$columns)
$qb->select(Task::COLUMNS)
->from($this->tableName)
->where($qb->expr()->lt('last_updated', $qb->createPositionalParameter($this->timeFactory->getDateTime()->getTimestamp() - $timeout)));
if (!$force) {
@@ -243,7 +243,7 @@ class TaskMapper extends QBMapper {
*/
public function findNOldestScheduledByType(array $taskTypes, array $taskIdsToIgnore, int $numberOfTasks) {
$qb = $this->db->getQueryBuilder();
$qb->select(Task::$columns)
$qb->select(Task::COLUMNS)
->from($this->tableName)
->where($qb->expr()->eq('status', $qb->createPositionalParameter(\OCP\TaskProcessing\Task::STATUS_SCHEDULED, IQueryBuilder::PARAM_INT)))
->setMaxResults($numberOfTasks)

View File

@@ -43,9 +43,9 @@ use OCP\Template\ITemplateManager;
use OCP\Util;
class TemplateLayout {
private static string $versionHash = '';
private string $versionHash = '';
/** @var string[] */
private static array $cacheBusterCache = [];
private array $cacheBusterCache = [];
public ?CSSResourceLocator $cssLocator = null;
public ?JSResourceLocator $jsLocator = null;
@@ -211,13 +211,13 @@ class TemplateLayout {
$page->assign('enabledThemes', $themesService?->getEnabledThemes() ?? []);
if ($this->config->getSystemValueBool('installed', false)) {
if (empty(self::$versionHash)) {
if (empty($this->versionHash)) {
$v = $this->appManager->getAppInstalledVersions(true);
$v['core'] = implode('.', $this->serverVersion->getVersion());
self::$versionHash = substr(md5(implode(',', $v)), 0, 8);
$this->versionHash = substr(md5(implode(',', $v)), 0, 8);
}
} else {
self::$versionHash = md5('not installed');
$this->versionHash = md5('not installed');
}
// Add the js files
@@ -247,7 +247,7 @@ class TemplateLayout {
if (Server::get(ContentSecurityPolicyNonceManager::class)->browserSupportsCspV3()) {
$page->assign('inline_ocjs', $config);
} else {
$page->append('jsfiles', Server::get(IURLGenerator::class)->linkToRoute('core.OCJS.getConfig', ['v' => self::$versionHash]));
$page->append('jsfiles', Server::get(IURLGenerator::class)->linkToRoute('core.OCJS.getConfig', ['v' => $this->versionHash]));
}
}
foreach ($jsFiles as $info) {
@@ -280,7 +280,7 @@ class TemplateLayout {
$page->assign('cssfiles', []);
$page->assign('printcssfiles', []);
$this->initialState->provideInitialState('core', 'versionHash', self::$versionHash);
$this->initialState->provideInitialState('core', 'versionHash', $this->versionHash);
foreach ($cssFiles as $info) {
$web = $info[1];
$file = $info[2];
@@ -320,7 +320,7 @@ class TemplateLayout {
if ($this->config->getSystemValueBool('installed', false) === false) {
// if not installed just return the version hash
return '?v=' . self::$versionHash;
return '?v=' . $this->versionHash;
}
$hash = false;
@@ -334,7 +334,7 @@ class TemplateLayout {
}
// As a last resort we use the server version hash
if ($hash === false) {
$hash = self::$versionHash;
$hash = $this->versionHash;
}
// The theming app is force-enabled thus the cache buster is always available
@@ -344,7 +344,7 @@ class TemplateLayout {
}
private function getVersionHashByPath(string $path): string|false {
if (array_key_exists($path, self::$cacheBusterCache) === false) {
if (array_key_exists($path, $this->cacheBusterCache) === false) {
// Not yet cached, so lets find the cache buster string
$appId = $this->getAppNamefromPath($path);
if ($appId === false) {
@@ -354,20 +354,20 @@ class TemplateLayout {
if ($appId === 'core') {
// core is not a real app but the server itself
$hash = self::$versionHash;
$hash = $this->versionHash;
} else {
$appVersion = $this->appManager->getAppVersion($appId);
// For shipped apps the app version is not a single source of truth, we rather also need to consider the Nextcloud version
if ($this->appManager->isShipped($appId)) {
$appVersion .= '-' . self::$versionHash;
$appVersion .= '-' . $this->versionHash;
}
$hash = substr(md5($appVersion), 0, 8);
}
self::$cacheBusterCache[$path] = $hash;
$this->cacheBusterCache[$path] = $hash;
}
return self::$cacheBusterCache[$path];
return $this->cacheBusterCache[$path];
}
private function findStylesheetFiles(array $styles): array {

View File

@@ -46,13 +46,12 @@ class Task extends Entity {
/**
* @var string[]
*/
public static array $columns = ['id', 'last_updated', 'type', 'input', 'output', 'status', 'user_id', 'app_id', 'identifier', 'completion_expected_at'];
public const COLUMNS = ['id', 'last_updated', 'type', 'input', 'output', 'status', 'user_id', 'app_id', 'identifier', 'completion_expected_at'];
/**
* @var string[]
*/
public static array $fields = ['id', 'lastUpdated', 'type', 'input', 'output', 'status', 'userId', 'appId', 'identifier', 'completionExpectedAt'];
public const FIELDS = ['id', 'lastUpdated', 'type', 'input', 'output', 'status', 'userId', 'appId', 'identifier', 'completionExpectedAt'];
public function __construct() {
// add types in constructor
@@ -69,9 +68,9 @@ class Task extends Entity {
}
public function toRow(): array {
return array_combine(self::$columns, array_map(function ($field) {
return array_combine(self::COLUMNS, array_map(function ($field) {
return $this->{'get' . ucfirst($field)}();
}, self::$fields));
}, self::FIELDS));
}
public static function fromPublicTask(OCPTask $task): Task {

View File

@@ -37,7 +37,7 @@ class TaskMapper extends QBMapper {
*/
public function find(int $id): Task {
$qb = $this->db->getQueryBuilder();
$qb->select(Task::$columns)
$qb->select(Task::COLUMNS)
->from($this->tableName)
->where($qb->expr()->eq('id', $qb->createPositionalParameter($id)));
return $this->findEntity($qb);
@@ -53,7 +53,7 @@ class TaskMapper extends QBMapper {
*/
public function findByIdAndUser(int $id, ?string $userId): Task {
$qb = $this->db->getQueryBuilder();
$qb->select(Task::$columns)
$qb->select(Task::COLUMNS)
->from($this->tableName)
->where($qb->expr()->eq('id', $qb->createPositionalParameter($id)));
if ($userId === null) {
@@ -73,7 +73,7 @@ class TaskMapper extends QBMapper {
*/
public function findUserTasksByApp(string $userId, string $appId, ?string $identifier = null): array {
$qb = $this->db->getQueryBuilder();
$qb->select(Task::$columns)
$qb->select(Task::COLUMNS)
->from($this->tableName)
->where($qb->expr()->eq('user_id', $qb->createPositionalParameter($userId)))
->andWhere($qb->expr()->eq('app_id', $qb->createPositionalParameter($appId)));

View File

@@ -44,7 +44,10 @@ class Manager implements IManager {
/** @var ?IProvider[] */
private ?array $providers = null;
private static array $taskProcessingCompatibleTaskTypes = [
/**
* @var array<class-string, string>
*/
private const COMPATIBLE_TASK_TYPES = [
FreePromptTaskType::class => TextToText::ID,
HeadlineTaskType::class => TextToTextHeadline::ID,
SummaryTaskType::class => TextToTextSummary::ID,
@@ -91,7 +94,7 @@ class Manager implements IManager {
public function hasProviders(): bool {
// check if task processing equivalent types are available
$taskTaskTypes = $this->taskProcessingManager->getAvailableTaskTypes();
foreach (self::$taskProcessingCompatibleTaskTypes as $textTaskTypeClass => $taskTaskTypeId) {
foreach (self::COMPATIBLE_TASK_TYPES as $textTaskTypeClass => $taskTaskTypeId) {
if (isset($taskTaskTypes[$taskTaskTypeId])) {
return true;
}
@@ -115,7 +118,7 @@ class Manager implements IManager {
// check if task processing equivalent types are available
$taskTaskTypes = $this->taskProcessingManager->getAvailableTaskTypes();
foreach (self::$taskProcessingCompatibleTaskTypes as $textTaskTypeClass => $taskTaskTypeId) {
foreach (self::COMPATIBLE_TASK_TYPES as $textTaskTypeClass => $taskTaskTypeId) {
if (isset($taskTaskTypes[$taskTaskTypeId])) {
$tasks[$textTaskTypeClass] = true;
}
@@ -134,9 +137,9 @@ class Manager implements IManager {
public function runTask(OCPTask $task): string {
// try to run a task processing task if possible
$taskTypeClass = $task->getType();
if (isset(self::$taskProcessingCompatibleTaskTypes[$taskTypeClass]) && isset($this->taskProcessingManager->getAvailableTaskTypes()[self::$taskProcessingCompatibleTaskTypes[$taskTypeClass]])) {
if (isset(self::COMPATIBLE_TASK_TYPES[$taskTypeClass]) && isset($this->taskProcessingManager->getAvailableTaskTypes()[self::COMPATIBLE_TASK_TYPES[$taskTypeClass]])) {
try {
$taskProcessingTaskTypeId = self::$taskProcessingCompatibleTaskTypes[$taskTypeClass];
$taskProcessingTaskTypeId = self::COMPATIBLE_TASK_TYPES[$taskTypeClass];
$taskProcessingTask = new \OCP\TaskProcessing\Task(
$taskProcessingTaskTypeId,
['input' => $task->getInput()],
@@ -222,8 +225,8 @@ class Manager implements IManager {
$task->setStatus(OCPTask::STATUS_SCHEDULED);
$providers = $this->getPreferredProviders($task);
$equivalentTaskProcessingTypeAvailable = (
isset(self::$taskProcessingCompatibleTaskTypes[$task->getType()])
&& isset($this->taskProcessingManager->getAvailableTaskTypes()[self::$taskProcessingCompatibleTaskTypes[$task->getType()]])
isset(self::COMPATIBLE_TASK_TYPES[$task->getType()])
&& isset($this->taskProcessingManager->getAvailableTaskTypes()[self::COMPATIBLE_TASK_TYPES[$task->getType()]])
);
if (count($providers) === 0 && !$equivalentTaskProcessingTypeAvailable) {
throw new PreConditionNotMetException('No LanguageModel provider is installed that can handle this task');

View File

@@ -49,12 +49,12 @@ class Task extends Entity {
/**
* @var string[]
*/
public static array $columns = ['id', 'last_updated', 'input', 'status', 'user_id', 'app_id', 'identifier', 'number_of_images', 'completion_expected_at'];
public const COLUMNS = ['id', 'last_updated', 'input', 'status', 'user_id', 'app_id', 'identifier', 'number_of_images', 'completion_expected_at'];
/**
* @var string[]
*/
public static array $fields = ['id', 'lastUpdated', 'input', 'status', 'userId', 'appId', 'identifier', 'numberOfImages', 'completionExpectedAt'];
public const FIELDS = ['id', 'lastUpdated', 'input', 'status', 'userId', 'appId', 'identifier', 'numberOfImages', 'completionExpectedAt'];
public function __construct() {
@@ -71,9 +71,9 @@ class Task extends Entity {
}
public function toRow(): array {
return array_combine(self::$columns, array_map(function ($field) {
return array_combine(self::COLUMNS, array_map(function ($field) {
return $this->{'get' . ucfirst($field)}();
}, self::$fields));
}, self::FIELDS));
}
public static function fromPublicTask(OCPTask $task): Task {

View File

@@ -38,7 +38,7 @@ class TaskMapper extends QBMapper {
*/
public function find(int $id): Task {
$qb = $this->db->getQueryBuilder();
$qb->select(Task::$columns)
$qb->select(Task::COLUMNS)
->from($this->tableName)
->where($qb->expr()->eq('id', $qb->createPositionalParameter($id)));
return $this->findEntity($qb);
@@ -54,7 +54,7 @@ class TaskMapper extends QBMapper {
*/
public function findByIdAndUser(int $id, ?string $userId): Task {
$qb = $this->db->getQueryBuilder();
$qb->select(Task::$columns)
$qb->select(Task::COLUMNS)
->from($this->tableName)
->where($qb->expr()->eq('id', $qb->createPositionalParameter($id)));
if ($userId === null) {
@@ -74,7 +74,7 @@ class TaskMapper extends QBMapper {
*/
public function findUserTasksByApp(?string $userId, string $appId, ?string $identifier = null): array {
$qb = $this->db->getQueryBuilder();
$qb->select(Task::$columns)
$qb->select(Task::COLUMNS)
->from($this->tableName)
->where($qb->expr()->eq('user_id', $qb->createPositionalParameter($userId)))
->andWhere($qb->expr()->eq('app_id', $qb->createPositionalParameter($appId)));

View File

@@ -27,6 +27,7 @@ class Util {
private static array $scriptsInit = [];
private static array $scripts = [];
private static array $scriptDeps = [];
private static ?bool $needUpgradeCache = null;
/**
* get the current installed version of Nextcloud
@@ -575,8 +576,6 @@ class Util {
return \OC_Util::isDefaultExpireDateEnforced();
}
protected static $needUpgradeCache = null;
/**
* Checks whether the current version needs upgrade.
*
@@ -584,7 +583,7 @@ class Util {
* @since 7.0.0
*/
public static function needUpgrade() {
if (!isset(self::$needUpgradeCache)) {
if (self::$needUpgradeCache === null) {
self::$needUpgradeCache = \OC_Util::needUpgrade(\OCP\Server::get(\OC\SystemConfig::class));
}
return self::$needUpgradeCache;

View File

@@ -18,6 +18,7 @@
<plugin filename="build/psalm/AppFrameworkTainter.php" />
<plugin filename="build/psalm/AttributeNamedParameters.php" />
<plugin filename="build/psalm/LogicalOperatorChecker.php" />
<plugin filename="build/psalm/StaticVarsChecker.php" />
</plugins>
<projectFiles>
<directory name="apps/admin_audit"/>

View File

@@ -48,8 +48,8 @@ class TemplateLayoutTest extends \Test\TestCase {
#[\PHPUnit\Framework\Attributes\DataProvider('dataVersionHash')]
public function testVersionHash(
string|false $path,
string|false $file,
string $path,
string $file,
bool $installed,
bool $debug,
string $expected,
@@ -57,13 +57,13 @@ class TemplateLayoutTest extends \Test\TestCase {
$this->appManager->expects(self::any())
->method('getAppVersion')
->willReturnCallback(fn ($appId) => match ($appId) {
'shippedApp' => 'shipped_1',
'otherApp' => 'other_2',
'shipped' => 'shipped_1',
'other' => 'other_2',
default => "$appId",
});
$this->appManager->expects(self::any())
->method('isShipped')
->willReturnCallback(fn (string $app) => $app === 'shippedApp');
->willReturnCallback(fn (string $app) => $app === 'shipped');
$this->config->expects(self::atLeastOnce())
->method('getSystemValueBool')
@@ -79,31 +79,20 @@ class TemplateLayoutTest extends \Test\TestCase {
$this->request->method('getPathInfo')
->willReturn('/' . $path);
$this->templateLayout = $this->getMockBuilder(TemplateLayout::class)
->onlyMethods(['getAppNamefromPath'])
->setConstructorArgs([
$this->config,
$this->appConfig,
$this->appManager,
$this->initialState,
$this->navigationManager,
$this->templateManager,
$this->serverVersion,
$this->request,
])
->getMock();
$this->templateLayout = new TemplateLayout(
$this->config,
$this->appConfig,
$this->appManager,
$this->initialState,
$this->navigationManager,
$this->templateManager,
$this->serverVersion,
$this->request,
);
$layout = $this->templateLayout->getPageTemplate(TemplateResponse::RENDER_AS_ERROR, '');
self::invokePrivate(TemplateLayout::class, 'versionHash', ['version_hash']);
$this->templateLayout->expects(self::any())
->method('getAppNamefromPath')
->willReturnCallback(fn ($appName) => match($appName) {
'apps/shipped' => 'shippedApp',
'other/app.css' => 'otherApp',
default => false,
});
self::invokePrivate($this->templateLayout, 'versionHash', ['version_hash']);
$hash = self::invokePrivate($this->templateLayout, 'getVersionHashSuffix', [$path, $file]);
self::assertEquals($expected, $hash);
@@ -113,9 +102,8 @@ class TemplateLayoutTest extends \Test\TestCase {
return [
'no hash if in debug mode' => ['apps/shipped', 'style.css', true, true, ''],
'only version hash if not installed' => ['apps/shipped', 'style.css', false, false, '?v=version_hash'],
'version hash with cache buster if app not found' => ['unknown/path', '', true, false, '?v=version_hash-42'],
'version hash with cache buster if neither path nor file provided' => [false, false, true, false, '?v=version_hash-42'],
'app version hash if external app' => ['', 'other/app.css', true, false, '?v=' . substr(md5('other_2'), 0, 8) . '-42'],
'version hash with cache buster if neither path nor file provided' => ['', '', true, false, '?v=version_hash-42'],
'app version hash if external app' => ['apps/other', 'app.css', true, false, '?v=' . substr(md5('other_2'), 0, 8) . '-42'],
'app version and version hash if shipped app' => ['apps/shipped', 'style.css', true, false, '?v=' . substr(md5('shipped_1-version_hash'), 0, 8) . '-42'],
'prefer path over file' => ['apps/shipped', 'other/app.css', true, false, '?v=' . substr(md5('shipped_1-version_hash'), 0, 8) . '-42'],
];