Skip to content
Open
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
75 changes: 75 additions & 0 deletions lib/AppStoreReachability.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

declare(strict_types=1);

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

namespace OCA\ServerInfo;

use OCP\Http\Client\IClientService;
use OCP\IAppConfig;

class AppStoreReachability {

Check failure on line 15 in lib/AppStoreReachability.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis

UnusedClass

lib/AppStoreReachability.php:15:7: UnusedClass: Class OCA\ServerInfo\AppStoreReachability is never used (see https://psalm.dev/075)
private const CACHE_TTL = 3600;
private const TIMEOUT = 4;

public function __construct(
private IClientService $clientService,
private IAppConfig $appConfig,
) {
}

/**
* @return array{
* reachable: bool,
* statusCode: int,
* latencyMs: int,
* checkedAt: int,
* cached: bool
* }
*/
public function check(): array {
$cachedAt = $this->appConfig->getValueInt('serverinfo', 'appstore_check_at', 0);
if ($cachedAt > 0 && (time() - $cachedAt) < self::CACHE_TTL) {
return [
'reachable' => $this->appConfig->getValueBool('serverinfo', 'appstore_check_reachable'),
'statusCode' => $this->appConfig->getValueInt('serverinfo', 'appstore_check_status'),
'latencyMs' => $this->appConfig->getValueInt('serverinfo', 'appstore_check_latency'),
'checkedAt' => $cachedAt,
'cached' => true,
];
}

$client = $this->clientService->newClient();
$start = microtime(true);
$reachable = false;
$status = 0;
try {
$response = $client->head('https://apps.nextcloud.com/api/v1/platform/29.0.0/apps.json', [
'timeout' => self::TIMEOUT,
'connect_timeout' => self::TIMEOUT,
'verify' => true,
]);
$status = $response->getStatusCode();
$reachable = $status >= 200 && $status < 400;
} catch (\Throwable) {
$reachable = false;
}
$latency = (int)round((microtime(true) - $start) * 1000);

Check failure on line 61 in lib/AppStoreReachability.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis

InvalidOperand

lib/AppStoreReachability.php:61:25: InvalidOperand: Cannot process ints and floats in strict binary operands mode, please cast explicitly (see https://psalm.dev/058)
$now = time();
$this->appConfig->setValueInt('serverinfo', 'appstore_check_at', $now);
$this->appConfig->setValueBool('serverinfo', 'appstore_check_reachable', $reachable);
$this->appConfig->setValueInt('serverinfo', 'appstore_check_status', $status);
$this->appConfig->setValueInt('serverinfo', 'appstore_check_latency', $latency);
return [
'reachable' => $reachable,
'statusCode' => $status,
'latencyMs' => $latency,
'checkedAt' => $now,
'cached' => false,
];
}
}
105 changes: 105 additions & 0 deletions lib/EolInfo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php

declare(strict_types=1);

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

namespace OCA\ServerInfo;

use OCP\IConfig;
use OCP\ServerVersion;

class EolInfo {

Check failure on line 15 in lib/EolInfo.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis

UnusedClass

lib/EolInfo.php:15:7: UnusedClass: Class OCA\ServerInfo\EolInfo is never used (see https://psalm.dev/075)
/**
* PHP version end-of-life dates (security support end).
* Source: https://www.php.net/supported-versions.php
*
* @var array<string, string> version "8.1" => "2025-12-31"
*/
private const PHP_EOL = [
'7.4' => '2022-11-28',
'8.0' => '2023-11-26',
'8.1' => '2025-12-31',
'8.2' => '2026-12-31',
'8.3' => '2027-12-31',
'8.4' => '2028-12-31',
'8.5' => '2029-12-31',
];

/**
* Nextcloud major version EOL (community/security end).
* Approximate based on the Nextcloud release schedule.
*
* @var array<string, string> "27" => "2024-06-30"
*/
private const NC_EOL = [

Check failure on line 38 in lib/EolInfo.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis

InvalidConstantAssignmentValue

lib/EolInfo.php:38:16: InvalidConstantAssignmentValue: OCA\ServerInfo\EolInfo::NC_EOL with declared type array<string, string> cannot be assigned type array{27: '2024-06-30', 28: '2024-12-31', 29: '2025-06-30', 30: '2025-12-31', 31: '2026-06-30', 32: '2026-12-31', 33: '2027-06-30', 34: '2028-06-30', 35: '2028-12-31'} (see https://psalm.dev/304)
'27' => '2024-06-30',
'28' => '2024-12-31',
'29' => '2025-06-30',
'30' => '2025-12-31',
'31' => '2026-06-30',
'32' => '2026-12-31',
'33' => '2027-06-30',
'34' => '2028-06-30',
'35' => '2028-12-31',
];

public function __construct(
private IConfig $config,
private ServerVersion $serverVersion,
) {
}

/**
* @return array{
* php: array{version: string, eol: ?string, daysUntilEol: ?int, status: string},
* nextcloud: array{version: string, major: string, eol: ?string, daysUntilEol: ?int, status: string}
* }
*/
public function getEolInfo(): array {
$phpMajor = PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION;
$phpEol = self::PHP_EOL[$phpMajor] ?? null;
$ncVersionParts = $this->serverVersion->getVersion();
$ncVersion = implode('.', $ncVersionParts);
$ncMajor = (string)(int)($ncVersionParts[0] ?? 0);
$ncEol = self::NC_EOL[$ncMajor] ?? null;

Check failure on line 68 in lib/EolInfo.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis

InvalidArrayOffset

lib/EolInfo.php:68:12: InvalidArrayOffset: Cannot access value on variable OCA\ServerInfo\EolInfo::NC_EOL using a numeric-string offset, expecting 27|28|29|30|31|32|33|34|35 (see https://psalm.dev/115)

return [
'php' => $this->makeEntry($phpMajor, $phpEol),
'nextcloud' => array_merge(
$this->makeEntry($ncMajor, $ncEol),
['version' => $ncVersion, 'major' => $ncMajor],
),
];
}

/**
* @return array{version: string, eol: ?string, daysUntilEol: ?int, status: string}
*/
private function makeEntry(string $version, ?string $eolDate): array {
if ($eolDate === null) {
return ['version' => $version, 'eol' => null, 'daysUntilEol' => null, 'status' => 'unknown'];
}
try {
$eolTs = (new \DateTimeImmutable($eolDate))->getTimestamp();
} catch (\Throwable) {
return ['version' => $version, 'eol' => $eolDate, 'daysUntilEol' => null, 'status' => 'unknown'];
}
$days = (int)floor(($eolTs - time()) / 86400);
$status = 'ok';
if ($days < 0) {
$status = 'critical';
} elseif ($days < 90) {
$status = 'warning';
}
return [
'version' => $version,
'eol' => $eolDate,
'daysUntilEol' => $days,
'status' => $status,
];
}
}
130 changes: 130 additions & 0 deletions lib/FederationStats.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
<?php

declare(strict_types=1);

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

namespace OCA\ServerInfo;

use OCP\App\IAppManager;
use OCP\IDBConnection;
use OCP\Share\IShare;

class FederationStats {

Check failure on line 16 in lib/FederationStats.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis

UnusedClass

lib/FederationStats.php:16:7: UnusedClass: Class OCA\ServerInfo\FederationStats is never used (see https://psalm.dev/075)
public function __construct(
private IAppManager $appManager,
private IDBConnection $db,
) {
}

/**
* @return array{
* enabled: bool,
* sharesSent: int,
* sharesReceived: int,
* sharesSentToGroups: int,
* trustedServers: int,
* topPeers: list<array{server: string, count: int}>
* }
*/
public function getFederationStats(): array {
$enabled = $this->appManager->isInstalled('federation') || $this->appManager->isInstalled('federatedfilesharing');

try {
$sent = $this->countShareType(IShare::TYPE_REMOTE);
$sentGroup = $this->countShareType(IShare::TYPE_REMOTE_GROUP);
$received = $this->countReceived();
$trusted = $this->countTrustedServers();
$top = $this->topPeers();
} catch (\Throwable) {
$sent = 0;
$sentGroup = 0;
$received = 0;
$trusted = 0;
$top = [];
}

return [
'enabled' => $enabled,
'sharesSent' => $sent,
'sharesSentToGroups' => $sentGroup,
'sharesReceived' => $received,
'trustedServers' => $trusted,
'topPeers' => $top,
];
}

private function countShareType(int $type): int {
$qb = $this->db->getQueryBuilder();
$qb->select($qb->func()->count('id'))
->from('share')
->where($qb->expr()->eq('share_type', $qb->createNamedParameter($type)));
$result = $qb->executeQuery();
$count = (int)$result->fetchOne();
$result->closeCursor();
return $count;
}

private function countReceived(): int {
try {
$qb = $this->db->getQueryBuilder();
$qb->select($qb->func()->count('id'))->from('share_external');
$result = $qb->executeQuery();
$count = (int)$result->fetchOne();
$result->closeCursor();
return $count;
} catch (\Throwable) {
return 0;
}
}

private function countTrustedServers(): int {
try {
$qb = $this->db->getQueryBuilder();
$qb->select($qb->func()->count('id'))->from('trusted_servers');
$result = $qb->executeQuery();
$count = (int)$result->fetchOne();
$result->closeCursor();
return $count;
} catch (\Throwable) {
return 0;
}
}

/**
* @return list<array{server: string, count: int}>
*/
private function topPeers(int $limit = 5): array {
try {
$qb = $this->db->getQueryBuilder();
$qb->select('share_with')
->selectAlias($qb->func()->count('id'), 'count')
->from('share')
->where($qb->expr()->in('share_type', $qb->createNamedParameter(
[IShare::TYPE_REMOTE, IShare::TYPE_REMOTE_GROUP],
\OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT_ARRAY,
)))
->groupBy('share_with')
->orderBy('count', 'DESC')
->setMaxResults($limit);
$result = $qb->executeQuery();
$out = [];
while (($row = $result->fetch()) !== false) {
$with = (string)($row['share_with'] ?? '');
$at = strrpos($with, '@');
$server = $at !== false ? substr($with, $at + 1) : $with;
$out[] = [
'server' => $server,
'count' => (int)($row['count'] ?? 0),
];
}
$result->closeCursor();
return $out;
} catch (\Throwable) {
return [];
}
}
}
Loading
Loading