From 29c4b1deb1068f89898870fd1156e8ea62b42cb7 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Fri, 3 Jul 2026 22:10:42 -0300 Subject: [PATCH 1/3] refactor(downloads): add release downloads counter Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../GitHub/GitHubReleaseDownloadsCounter.php | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 support/GitHub/GitHubReleaseDownloadsCounter.php diff --git a/support/GitHub/GitHubReleaseDownloadsCounter.php b/support/GitHub/GitHubReleaseDownloadsCounter.php new file mode 100644 index 00000000..fc9b3873 --- /dev/null +++ b/support/GitHub/GitHubReleaseDownloadsCounter.php @@ -0,0 +1,67 @@ +fetchPage($page, $token); + + if (empty($releases)) { + break; + } + + foreach ($releases as $release) { + foreach ($release['assets'] ?? [] as $asset) { + $total += $asset['download_count'] ?? 0; + } + } + + if (count($releases) < 100) { + break; + } + + $page++; + } + + return $total > 0 ? $total : null; + } + + protected function fetchPage(int $page, ?string $token): ?array + { + $headers = ['User-Agent: libresign-site-build']; + if (!empty($token)) { + $headers[] = 'Authorization: Bearer ' . $token; + } + + $context = stream_context_create([ + 'http' => [ + 'header' => implode("\r\n", $headers), + 'timeout' => $this->timeout, + ], + ]); + + $url = sprintf( + 'https://api.github.com/repos/%s/releases?per_page=100&page=%d', + $this->repository, + $page, + ); + + $json = @file_get_contents($url, false, $context); + + return $json ? (json_decode($json, true) ?: null) : null; + } +} From c916fbc66a5625f85eb0cdcf1f7ff1e863290456 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Fri, 3 Jul 2026 22:10:50 -0300 Subject: [PATCH 2/3] test(downloads): add release downloads counter tests Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../GitHubReleaseDownloadsCounterTest.php | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 tests/Unit/Support/GitHub/GitHubReleaseDownloadsCounterTest.php diff --git a/tests/Unit/Support/GitHub/GitHubReleaseDownloadsCounterTest.php b/tests/Unit/Support/GitHub/GitHubReleaseDownloadsCounterTest.php new file mode 100644 index 00000000..032925ab --- /dev/null +++ b/tests/Unit/Support/GitHub/GitHubReleaseDownloadsCounterTest.php @@ -0,0 +1,69 @@ +count('token')); + } + + public static function countProvider(): iterable + { + yield 'returns null when first page fails' => [ + [], + null, + ]; + + yield 'returns null when releases are empty' => [ + [1 => []], + null, + ]; + + yield 'sums downloads from a single page' => [ + [ + 1 => [ + ['assets' => [['download_count' => 10], ['download_count' => 5]]], + ['assets' => [['download_count' => 7]]], + ], + ], + 22, + ]; + + yield 'continues to the second page when first page has 100 items' => [ + [ + 1 => array_fill(0, 100, ['assets' => [['download_count' => 1]]]), + 2 => [ + ['assets' => [['download_count' => 3]]], + ], + ], + 103, + ]; + } +} + +final class FakeGitHubReleaseDownloadsCounter extends GitHubReleaseDownloadsCounter +{ + /** + * @param array>> $pages + */ + public function __construct(private readonly array $pages) + { + parent::__construct(); + } + + protected function fetchPage(int $page, ?string $token): ?array + { + return $this->pages[$page] ?? null; + } +} From 3f92de24f20352068468cbac0037db97a7cdf428 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Fri, 3 Jul 2026 22:11:29 -0300 Subject: [PATCH 3/3] refactor(downloads): wire downloads counter in config Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- config.php | 31 +++---------------------------- 1 file changed, 3 insertions(+), 28 deletions(-) diff --git a/config.php b/config.php index 28bb21ad..43926ac0 100644 --- a/config.php +++ b/config.php @@ -1,5 +1,6 @@ build($wooCommerceConsumerKey, $wooCommerceConsumerSecret); +$gitHubReleaseDownloadsCounter = new GitHubReleaseDownloadsCounter(); $wooCommerceProductCollection = new WooCommerceProductCollection($wooCommerceAuthenticatedHeaders); return [ @@ -63,34 +65,7 @@ 'title' => 'LibreSign - Open Source Electronic Signature for Nextcloud', 'description' => 'LibreSign is a free and open source electronic signature app for Nextcloud. Sign, request, and manage digital documents securely in your own self-hosted environment.', 'authorGravatars' => $authorGravatars, - 'githubDownloads' => (function() { - $total = 0; - $page = 1; - $token = getenv('GITHUB_TOKEN'); - $headers = ['User-Agent: libresign-site-build']; - if ($token) { - $headers[] = 'Authorization: Bearer ' . $token; - } - $context = stream_context_create(['http' => [ - 'header' => implode("\r\n", $headers), - 'timeout' => 15, - ]]); - while (true) { - $url = "https://api.github.com/repos/LibreSign/libresign/releases?per_page=100&page={$page}"; - $json = @file_get_contents($url, false, $context); - if ($json === false) break; - $releases = json_decode($json, true); - if (empty($releases)) break; - foreach ($releases as $release) { - foreach ($release['assets'] ?? [] as $asset) { - $total += $asset['download_count'] ?? 0; - } - } - if (count($releases) < 100) break; - $page++; - } - return $total > 0 ? $total : null; - })(), + 'githubDownloads' => $gitHubReleaseDownloadsCounter->count(getenv('GITHUB_TOKEN') ?: null), 'locales' => function ($page) { return available_locales($page); },