Skip to content
11 changes: 6 additions & 5 deletions apps/files_external/lib/Service/BackendService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -71,14 +72,14 @@ public function registerBackendProvider(IBackendProvider $provider) {
$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;
}
}

Expand Down
6 changes: 3 additions & 3 deletions apps/files_trashbin/lib/Command/RestoreAllFiles.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -218,8 +218,8 @@ protected function parseArgs(InputInterface $input): array {
}

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'");
Expand Down
14 changes: 7 additions & 7 deletions apps/files_versions/lib/Storage.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,11 @@
public const DELETE_TRIGGER_QUOTA_EXCEEDED = 2;

// files for which we can remove the versions after the delete operation was successful
private static $deletedFiles = [];

Check failure on line 59 in apps/files_versions/lib/Storage.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

ImpureStaticProperty

apps/files_versions/lib/Storage.php:59:2: ImpureStaticProperty: Static property should not be used as they do not follow requests lifecycle (see https://psalm.dev/221)

private static $sourcePathAndUser = [];

Check failure on line 61 in apps/files_versions/lib/Storage.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

ImpureStaticProperty

apps/files_versions/lib/Storage.php:61:2: ImpureStaticProperty: Static property should not be used as they do not follow requests lifecycle (see https://psalm.dev/221)

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
Expand All @@ -76,7 +76,7 @@
];

/** @var Application */
private static $application;

Check failure on line 79 in apps/files_versions/lib/Storage.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

ImpureStaticProperty

apps/files_versions/lib/Storage.php:79:2: ImpureStaticProperty: Static property should not be used as they do not follow requests lifecycle (see https://psalm.dev/221)

/**
* get the UID of the owner of the file and the path to the file relative to
Expand Down Expand Up @@ -763,11 +763,11 @@
$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) {

Check failure on line 767 in apps/files_versions/lib/Storage.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

TypeDoesNotContainType

apps/files_versions/lib/Storage.php:767:7: TypeDoesNotContainType: Type 10 for OCA\Files_Versions\Storage::MAX_VERSIONS_PER_INTERVAL[$interval]['intervalEndsAfter'] is never =int(-1) (see https://psalm.dev/056)

Check failure on line 767 in apps/files_versions/lib/Storage.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

TypeDoesNotContainType

apps/files_versions/lib/Storage.php:767:7: TypeDoesNotContainType: -1 cannot be identical to 10 (see https://psalm.dev/056)
$nextInterval = -1;
} else {
$nextInterval = $time - Storage::$max_versions_per_interval[$interval]['intervalEndsAfter'];
$nextInterval = $time - Storage::MAX_VERSIONS_PER_INTERVAL[$interval]['intervalEndsAfter'];
}

$firstVersion = reset($versions);
Expand Down Expand Up @@ -797,12 +797,12 @@
$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'];

Check failure on line 800 in apps/files_versions/lib/Storage.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

InvalidArrayOffset

apps/files_versions/lib/Storage.php:800:14: InvalidArrayOffset: Cannot access value on variable OCA\Files_Versions\Storage::MAX_VERSIONS_PER_INTERVAL using a int<2, max> offset, expecting 1|2|3|4|5|6 (see https://psalm.dev/115)
$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
}
Expand Down
54 changes: 54 additions & 0 deletions build/psalm/StaticVarsChecker.php
Original file line number Diff line number Diff line change
@@ -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;
}
}
23 changes: 23 additions & 0 deletions build/psalm/StaticVarsTest.php
Original file line number Diff line number Diff line change
@@ -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;
}
}
4 changes: 2 additions & 2 deletions lib/composer/composer/InstalledVersions.php
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ public static function getRawData()
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();
Expand Down Expand Up @@ -378,7 +378,7 @@ private static function getInstalled()
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;
Expand Down
69 changes: 66 additions & 3 deletions lib/composer/composer/installed.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
15 changes: 12 additions & 3 deletions lib/composer/composer/installed.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
),
),
);
24 changes: 12 additions & 12 deletions lib/private/Files/Cache/SearchBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
*/
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',
Expand All @@ -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',
Expand All @@ -50,7 +50,7 @@ class SearchBuilder {
];

/** @var array<string, string> */
protected static $fieldTypes = [
private const FIELD_TYPES = [
'mimetype' => 'string',
'mtime' => 'integer',
'name' => 'string',
Expand All @@ -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,
Expand Down Expand Up @@ -134,7 +134,7 @@ public function searchOperatorToDBExpr(
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');
}
Expand All @@ -147,7 +147,7 @@ public function searchOperatorToDBExpr(
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));
}
Expand Down Expand Up @@ -193,7 +193,7 @@ private function getOperatorFieldAndValue(ISearchComparison $operator): array {
* @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 = [];
Expand Down Expand Up @@ -263,10 +263,10 @@ private function validateComparison(ISearchComparison $operator) {
'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());
Expand Down Expand Up @@ -317,9 +317,9 @@ private function getParameterForValue(IQueryBuilder $builder, $value, string $pa
$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);
}
Expand Down
Loading
Loading