]> git.agnieray.net Git - galette.git/commitdiff
Check if new Galette release is available
authorJohan Cwiklinski <johan@x-tnd.be>
Fri, 16 Feb 2024 08:25:31 +0000 (09:25 +0100)
committerJohan Cwiklinski <johan@x-tnd.be>
Sun, 18 Feb 2024 09:09:02 +0000 (10:09 +0100)
closes #1785

.composer-require-checker.config.json
galette/composer.json
galette/composer.lock
galette/config/paths.inc.php
galette/includes/main.inc.php
galette/lib/Galette/Util/Release.php [new file with mode: 0644]
tests/Galette/Util/tests/units/Release.php [new file with mode: 0644]

index 9014152b5d100b96bf99c7e6115f0bb4e5c4f9c0..3d3380f83fa6bd11e8b8f976b11416892c747dda 100644 (file)
@@ -33,6 +33,7 @@
     "GALETTE_CONFIG_PATH",
     "GALETTE_DATA_PATH",
     "GALETTE_DB_VERSION",
+    "GALETTE_DOWNLOADS_URI",
     "GALETTE_EXPORTS_PATH",
     "GALETTE_FILES_PATH",
     "GALETTE_IMPORTS_PATH",
index c64308d46fdb2e34bfc087b184fdbc0d4c242dec..798efb71c9b533e4a70cafbd627f846d81afa87b 100644 (file)
@@ -51,7 +51,8 @@
         "twig/twig": "^3.3",
         "slim/twig-view": "^3.3",
         "slim/psr7": "^1.6",
-        "symfony/yaml": "^6.2"
+        "symfony/yaml": "^6.2",
+        "guzzlehttp/guzzle": "^7.8"
     },
     "require-dev": {
         "squizlabs/php_codesniffer": "^3.7",
index 0738a55c8f4782700ccaa26da3f752dcbbcf82a8..e1c212aed3c4a6a809629aaa95ad9a07c1f507ae 100644 (file)
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "d9d666c177b29251a86016612325d3ca",
+    "content-hash": "dbbf63121881c53f2c16c57ee40027ed",
     "packages": [
         {
             "name": "akrabat/rka-slim-session-middleware",
             },
             "time": "2020-11-24T22:02:12+00:00"
         },
+        {
+            "name": "guzzlehttp/guzzle",
+            "version": "7.8.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/guzzle/guzzle.git",
+                "reference": "41042bc7ab002487b876a0683fc8dce04ddce104"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/41042bc7ab002487b876a0683fc8dce04ddce104",
+                "reference": "41042bc7ab002487b876a0683fc8dce04ddce104",
+                "shasum": ""
+            },
+            "require": {
+                "ext-json": "*",
+                "guzzlehttp/promises": "^1.5.3 || ^2.0.1",
+                "guzzlehttp/psr7": "^1.9.1 || ^2.5.1",
+                "php": "^7.2.5 || ^8.0",
+                "psr/http-client": "^1.0",
+                "symfony/deprecation-contracts": "^2.2 || ^3.0"
+            },
+            "provide": {
+                "psr/http-client-implementation": "1.0"
+            },
+            "require-dev": {
+                "bamarni/composer-bin-plugin": "^1.8.2",
+                "ext-curl": "*",
+                "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999",
+                "php-http/message-factory": "^1.1",
+                "phpunit/phpunit": "^8.5.36 || ^9.6.15",
+                "psr/log": "^1.1 || ^2.0 || ^3.0"
+            },
+            "suggest": {
+                "ext-curl": "Required for CURL handler support",
+                "ext-intl": "Required for Internationalized Domain Name (IDN) support",
+                "psr/log": "Required for using the Log middleware"
+            },
+            "type": "library",
+            "extra": {
+                "bamarni-bin": {
+                    "bin-links": true,
+                    "forward-command": false
+                }
+            },
+            "autoload": {
+                "files": [
+                    "src/functions_include.php"
+                ],
+                "psr-4": {
+                    "GuzzleHttp\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Graham Campbell",
+                    "email": "hello@gjcampbell.co.uk",
+                    "homepage": "https://github.com/GrahamCampbell"
+                },
+                {
+                    "name": "Michael Dowling",
+                    "email": "mtdowling@gmail.com",
+                    "homepage": "https://github.com/mtdowling"
+                },
+                {
+                    "name": "Jeremy Lindblom",
+                    "email": "jeremeamia@gmail.com",
+                    "homepage": "https://github.com/jeremeamia"
+                },
+                {
+                    "name": "George Mponos",
+                    "email": "gmponos@gmail.com",
+                    "homepage": "https://github.com/gmponos"
+                },
+                {
+                    "name": "Tobias Nyholm",
+                    "email": "tobias.nyholm@gmail.com",
+                    "homepage": "https://github.com/Nyholm"
+                },
+                {
+                    "name": "Márk Sági-Kazár",
+                    "email": "mark.sagikazar@gmail.com",
+                    "homepage": "https://github.com/sagikazarmark"
+                },
+                {
+                    "name": "Tobias Schultze",
+                    "email": "webmaster@tubo-world.de",
+                    "homepage": "https://github.com/Tobion"
+                }
+            ],
+            "description": "Guzzle is a PHP HTTP client library",
+            "keywords": [
+                "client",
+                "curl",
+                "framework",
+                "http",
+                "http client",
+                "psr-18",
+                "psr-7",
+                "rest",
+                "web service"
+            ],
+            "support": {
+                "issues": "https://github.com/guzzle/guzzle/issues",
+                "source": "https://github.com/guzzle/guzzle/tree/7.8.1"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/GrahamCampbell",
+                    "type": "github"
+                },
+                {
+                    "url": "https://github.com/Nyholm",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2023-12-03T20:35:24+00:00"
+        },
+        {
+            "name": "guzzlehttp/promises",
+            "version": "2.0.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/guzzle/promises.git",
+                "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/guzzle/promises/zipball/bbff78d96034045e58e13dedd6ad91b5d1253223",
+                "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2.5 || ^8.0"
+            },
+            "require-dev": {
+                "bamarni/composer-bin-plugin": "^1.8.2",
+                "phpunit/phpunit": "^8.5.36 || ^9.6.15"
+            },
+            "type": "library",
+            "extra": {
+                "bamarni-bin": {
+                    "bin-links": true,
+                    "forward-command": false
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "GuzzleHttp\\Promise\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Graham Campbell",
+                    "email": "hello@gjcampbell.co.uk",
+                    "homepage": "https://github.com/GrahamCampbell"
+                },
+                {
+                    "name": "Michael Dowling",
+                    "email": "mtdowling@gmail.com",
+                    "homepage": "https://github.com/mtdowling"
+                },
+                {
+                    "name": "Tobias Nyholm",
+                    "email": "tobias.nyholm@gmail.com",
+                    "homepage": "https://github.com/Nyholm"
+                },
+                {
+                    "name": "Tobias Schultze",
+                    "email": "webmaster@tubo-world.de",
+                    "homepage": "https://github.com/Tobion"
+                }
+            ],
+            "description": "Guzzle promises library",
+            "keywords": [
+                "promise"
+            ],
+            "support": {
+                "issues": "https://github.com/guzzle/promises/issues",
+                "source": "https://github.com/guzzle/promises/tree/2.0.2"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/GrahamCampbell",
+                    "type": "github"
+                },
+                {
+                    "url": "https://github.com/Nyholm",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2023-12-03T20:19:20+00:00"
+        },
+        {
+            "name": "guzzlehttp/psr7",
+            "version": "2.6.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/guzzle/psr7.git",
+                "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/guzzle/psr7/zipball/45b30f99ac27b5ca93cb4831afe16285f57b8221",
+                "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2.5 || ^8.0",
+                "psr/http-factory": "^1.0",
+                "psr/http-message": "^1.1 || ^2.0",
+                "ralouphie/getallheaders": "^3.0"
+            },
+            "provide": {
+                "psr/http-factory-implementation": "1.0",
+                "psr/http-message-implementation": "1.0"
+            },
+            "require-dev": {
+                "bamarni/composer-bin-plugin": "^1.8.2",
+                "http-interop/http-factory-tests": "^0.9",
+                "phpunit/phpunit": "^8.5.36 || ^9.6.15"
+            },
+            "suggest": {
+                "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
+            },
+            "type": "library",
+            "extra": {
+                "bamarni-bin": {
+                    "bin-links": true,
+                    "forward-command": false
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "GuzzleHttp\\Psr7\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Graham Campbell",
+                    "email": "hello@gjcampbell.co.uk",
+                    "homepage": "https://github.com/GrahamCampbell"
+                },
+                {
+                    "name": "Michael Dowling",
+                    "email": "mtdowling@gmail.com",
+                    "homepage": "https://github.com/mtdowling"
+                },
+                {
+                    "name": "George Mponos",
+                    "email": "gmponos@gmail.com",
+                    "homepage": "https://github.com/gmponos"
+                },
+                {
+                    "name": "Tobias Nyholm",
+                    "email": "tobias.nyholm@gmail.com",
+                    "homepage": "https://github.com/Nyholm"
+                },
+                {
+                    "name": "Márk Sági-Kazár",
+                    "email": "mark.sagikazar@gmail.com",
+                    "homepage": "https://github.com/sagikazarmark"
+                },
+                {
+                    "name": "Tobias Schultze",
+                    "email": "webmaster@tubo-world.de",
+                    "homepage": "https://github.com/Tobion"
+                },
+                {
+                    "name": "Márk Sági-Kazár",
+                    "email": "mark.sagikazar@gmail.com",
+                    "homepage": "https://sagikazarmark.hu"
+                }
+            ],
+            "description": "PSR-7 message implementation that also provides common utility methods",
+            "keywords": [
+                "http",
+                "message",
+                "psr-7",
+                "request",
+                "response",
+                "stream",
+                "uri",
+                "url"
+            ],
+            "support": {
+                "issues": "https://github.com/guzzle/psr7/issues",
+                "source": "https://github.com/guzzle/psr7/tree/2.6.2"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/GrahamCampbell",
+                    "type": "github"
+                },
+                {
+                    "url": "https://github.com/Nyholm",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2023-12-03T20:05:35+00:00"
+        },
         {
             "name": "laminas/laminas-db",
             "version": "2.18.0",
             },
             "time": "2019-01-08T18:20:26+00:00"
         },
+        {
+            "name": "psr/http-client",
+            "version": "1.0.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/http-client.git",
+                "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
+                "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.0 || ^8.0",
+                "psr/http-message": "^1.0 || ^2.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Http\\Client\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "https://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for HTTP clients",
+            "homepage": "https://github.com/php-fig/http-client",
+            "keywords": [
+                "http",
+                "http-client",
+                "psr",
+                "psr-18"
+            ],
+            "support": {
+                "source": "https://github.com/php-fig/http-client"
+            },
+            "time": "2023-09-23T14:17:50+00:00"
+        },
         {
             "name": "psr/http-factory",
             "version": "1.0.2",
index 7814d09a7f154c9f3a6d0d42a9a99fefda6192d1..b99e0a854f5b70152bb8164e594e2139ee1a5ce2 100644 (file)
@@ -114,3 +114,7 @@ if (!defined('GALETTE_TELEMETRY_URI')) {
 if (!defined('GALETTE_TPL_THEME_DIR')) {
     define('GALETTE_TPL_THEME_DIR', GALETTE_ROOT . 'templates/default/');
 }
+
+if (!defined('GALETTE_DOWNLOADS_URI')) {
+    define('GALETTE_DOWNLOADS_URI', 'https://download.tuxfamily.org/galette/');
+}
index 065d4d724c15a19534b5d891b26ce5839eb3788d..95bcd73d0710a7509f0fe3e44bc2e7f150cbe06c 100644 (file)
  * along with Galette. If not, see <http://www.gnu.org/licenses/>.
  */
 
+use Analog\Analog;
 use Galette\Middleware\Authenticate;
 use Galette\Middleware\Language;
 use Galette\Middleware\Telemetry;
 use Galette\Middleware\TrailingSlash;
 use Galette\Middleware\UpdateAndMaintenance;
+use Galette\Util\Release;
 use RKA\SessionMiddleware;
 use Slim\Routing\RouteContext;
 use Galette\Core\Galette;
@@ -143,6 +145,43 @@ $app->add(Language::class);
 //Telemetry update middleware
 $app->add(Telemetry::class);
 
+//check for new release
+if (
+    $container->get('login')->isSuperAdmin()
+    || $container->get('login')->isAdmin()
+    || $container->get('login')->isStaff()
+) {
+    try {
+        $release = new Release();
+        if ($release->checkNewRelease()) {
+            Analog::log(
+                sprintf(
+                    'A new Galette release is available: %s (current %s)',
+                    $release->getLatestRelease(),
+                    GALETTE_VERSION
+                ),
+                Analog::INFO
+            );
+            $container->get('flash')->addMessage(
+                'info',
+                [
+                    'title' => _T('A new Galette release is available.'),
+                    'message' => sprintf(
+                        _T('You currently use Galette %1$s, and %2$s is available.'),
+                        GALETTE_VERSION,
+                        $release->getLatestRelease()
+                    )
+                ]
+            );
+        }
+    } catch (\Throwable $e) {
+        Analog::log(
+            'Error looking for new release: ' . $e->getMessage(),
+            Analog::ERROR
+        );
+    }
+}
+
 require_once GALETTE_ROOT . 'includes/routes/authentication.routes.php';
 require_once GALETTE_ROOT . 'includes/routes/management.routes.php';
 require_once GALETTE_ROOT . 'includes/routes/members.routes.php';
diff --git a/galette/lib/Galette/Util/Release.php b/galette/lib/Galette/Util/Release.php
new file mode 100644 (file)
index 0000000..b388e28
--- /dev/null
@@ -0,0 +1,152 @@
+<?php
+
+/**
+ * Copyright © 2003-2024 The Galette Team
+ *
+ * This file is part of Galette (https://galette.eu).
+ *
+ * Galette is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Galette is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Galette. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace Galette\Util;
+
+use GuzzleHttp\Client;
+
+/**
+ * Check for new Galette release
+ *
+ * @author Johan Cwiklinski <johan@x-tnd.be>
+ */
+class Release
+{
+    /** @var array<string, mixed> */
+    private array $default_options = [
+        'timeout' => 2.0,
+    ];
+    private ?string $latest = null;
+
+    /**
+     * Set ups Guzzle client
+     *
+     * @return Client
+     */
+    public function setupClient(): Client
+    {
+        $this->client = new Client(
+            $this->getDefaultOptions()
+        );
+        return $this->client;
+    }
+
+    /**
+     * Get default options
+     *
+     * @return array<string, mixed>
+     */
+    public function getDefaultOptions(): array
+    {
+        return $this->default_options;
+    }
+
+    /**
+     * Get the latest release
+     *
+     * @return ?string
+     */
+    public function getLatestRelease(): ?string
+    {
+        if (!isset($this->latest)) {
+            $this->latest = $this->findLatestRelease();
+        }
+        return $this->latest;
+    }
+
+    /**
+     * Get the latest release
+     *
+     * @return ?string
+     */
+    public function findLatestRelease(): ?string
+    {
+        if (isset($this->latest)) {
+            return $this->getLatestRelease();
+        }
+
+        $client = $this->setupClient();
+        $response = $client->request('GET', $this->getReleasesURL());
+        $contents = $response->getBody()->getContents();
+
+        $releases = [];
+        preg_match_all(
+            '/href="(galette-.[^"]+\.tar\.bz2)"/',
+            $contents,
+            $releases
+        );
+
+        $latest = null;
+        foreach ($releases[1] as $release) {
+            $release = str_replace('galette-', '', $release);
+            $release = str_replace('.tar.bz2', '', $release);
+            if ($release === 'dev') {
+                continue;
+            }
+            if (version_compare($release, $latest ?? 0, '>')) {
+                $latest = $release;
+            }
+        }
+
+        return $latest;
+    }
+
+    /**
+     * Check if a new release is available
+     *
+     * @return bool
+     */
+    public function checkNewRelease(): bool
+    {
+        $current = $this->getCurrentRelease();
+        if (str_ends_with($current, '-dev')) {
+            //current version is a dev version
+            return false;
+        }
+
+        $this->latest = $this->getLatestRelease();
+        if ($this->latest === null) {
+            return false;
+        }
+
+        return version_compare($this->latest, ltrim($this->getCurrentRelease(), 'v'), '>');
+    }
+
+    /**
+     * Get the current release
+     *
+     * @return string
+     */
+    public function getCurrentRelease(): string
+    {
+        return GALETTE_VERSION;
+    }
+
+    /**
+     * Get the URL to download releases
+     *
+     * @return string
+     */
+    public function getReleasesURL(): string
+    {
+        return GALETTE_DOWNLOADS_URI;
+    }
+}
diff --git a/tests/Galette/Util/tests/units/Release.php b/tests/Galette/Util/tests/units/Release.php
new file mode 100644 (file)
index 0000000..e76429b
--- /dev/null
@@ -0,0 +1,257 @@
+<?php
+
+/**
+ * Copyright © 2003-2024 The Galette Team
+ *
+ * This file is part of Galette (https://galette.eu).
+ *
+ * Galette is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Galette is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Galette. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace Galette\Util\test\units;
+
+use PHPUnit\Framework\TestCase;
+
+/**
+ * Release tests class
+ *
+ * @author Johan Cwiklinski <johan@x-tnd.be>
+ */
+class Release extends TestCase
+{
+    /**
+     * Releases provider
+     *
+     * @return array<int, array<int, string|bool>>
+     */
+    public static function releasesProvider(): array
+    {
+        return [
+            ['0.7.0', '1.1.0', true],
+            ['1.0.0', '1.1.0', true],
+            ['1.0.1', '1.1.0', true],
+            ['1.1.0', '1.1.0', false],
+            ['1.2.0', '1.1.0', false],
+        ];
+    }
+
+    /**
+     * Test checkNewRelease
+     *
+     * @param string $current  Current release
+     * @param string $latest   Latest release
+     * @param bool   $expected Expected result
+     *
+     * @dataProvider releasesProvider
+     * @return void
+     */
+    public function testNewRelease(string $current, string $latest, bool $expected): void
+    {
+        $release = $this->getMockBuilder(\Galette\Util\Release::class)
+            ->onlyMethods(array('getCurrentRelease', 'getLatestRelease'))
+            ->getMock();
+
+        $release->method('getCurrentRelease')->willReturn($current);
+        $release->method('getLatestRelease')->willReturn($latest);
+
+        $this->assertSame($expected, $release->checkNewRelease());
+    }
+
+    /**
+     * Releases provider
+     *
+     * @return array<int, array<int, string|bool>>
+     */
+    public static function releasesPageProvider(): array
+    {
+        return [
+            [
+                '0.7.0',
+                '1.0.2',
+                true,
+                '<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
+<head>
+<title>Index of /galette/</title>
+<style type="text/css">
+a, a:active {text-decoration: none; color: blue;}
+a:visited {color: #48468F;}
+a:hover, a:focus {text-decoration: underline; color: red;}
+body {background-color: #F5F5F5;}
+h2 {margin-bottom: 12px;}
+table {margin-left: 12px;}
+th, td { font: 90% monospace; text-align: left;}
+th { font-weight: bold; padding-right: 14px; padding-bottom: 3px;}
+td {padding-right: 14px;}
+td.s, th.s {text-align: right;}
+div.list { background-color: white; border-top: 1px solid #646464; border-bottom: 1px solid #646464; padding-top: 10px; padding-bottom: 14px;}
+div.foot { font: 90% monospace; color: #787878; padding-top: 4px;}
+</style>
+</head>
+<body>
+<h2>Index of /galette/</h2>
+<div class="list">
+<table summary="Directory Listing" cellpadding="0" cellspacing="0">
+<thead><tr><th class="n">Name</th><th class="m">Last Modified</th><th class="s">Size</th><th class="t">Type</th></tr></thead>
+<tbody>
+<tr><td class="n"><a href="../">Parent Directory</a>/</td><td class="m">&nbsp;</td><td class="s">- &nbsp;</td><td class="t">Directory</td></tr>
+<tr><td class="n"><a href="archives/">archives</a>/</td><td class="m">2024-Jan-16 09:01:57</td><td class="s">- &nbsp;</td><td class="t">Directory</td></tr>
+<tr><td class="n"><a href="com/">com</a>/</td><td class="m">2016-Sep-22 22:23:10</td><td class="s">- &nbsp;</td><td class="t">Directory</td></tr>
+<tr><td class="n"><a href="dev/">dev</a>/</td><td class="m">2023-Nov-22 18:56:34</td><td class="s">- &nbsp;</td><td class="t">Directory</td></tr>
+<tr><td class="n"><a href="listes-galette/">listes-galette</a>/</td><td class="m">2017-Feb-02 02:06:43</td><td class="s">- &nbsp;</td><td class="t">Directory</td></tr>
+<tr><td class="n"><a href="plugins/">plugins</a>/</td><td class="m">2024-Jan-16 17:23:37</td><td class="s">- &nbsp;</td><td class="t">Directory</td></tr>
+<tr><td class="n"><a href="README">README</a></td><td class="m">2021-Sep-22 07:29:48</td><td class="s">0.1K</td><td class="t">text/plain</td></tr>
+<tr><td class="n"><a href="galette-1.0.0.tar.bz2">galette-1.0.0.tar.bz2</a></td><td class="m">2023-Dec-07 07:57:12</td><td class="s">8.7M</td><td class="t">application/octet-stream</td></tr>
+<tr><td class="n"><a href="galette-1.0.1.tar.bz2">galette-1.0.1.tar.bz2</a></td><td class="m">2024-Jan-16 09:00:19</td><td class="s">8.7M</td><td class="t">application/octet-stream</td></tr>
+<tr><td class="n"><a href="galette-1.0.2.tar.bz2">galette-1.0.2.tar.bz2</a></td><td class="m">2024-Feb-03 09:32:46</td><td class="s">8.7M</td><td class="t">application/octet-stream</td></tr>
+<tr><td class="n"><a href="galette-1.0.2.tar.bz2.asc">galette-1.0.2.tar.bz2.asc</a></td><td class="m">2024-Feb-03 09:32:46</td><td class="s">0.1K</td><td class="t">text/plain</td></tr>
+<tr><td class="n"><a href="galette-dev.tar.bz2">galette-dev.tar.bz2</a></td><td class="m">2024-Feb-16 00:34:58</td><td class="s">8.7M</td><td class="t">application/octet-stream</td></tr>
+<tr><td class="n"><a href="galette-dev.tar.bz2.asc">galette-dev.tar.bz2.asc</a></td><td class="m">2023-Oct-19 17:32:27</td><td class="s">0.1K</td><td class="t">text/plain</td></tr>
+</tbody>
+</table>
+</div>
+<div class="foot">lighttpd/1.4.35</div>
+</body>
+</html>' //real content as of 1.0.2 release
+            ],
+            [
+                '1.0.0',
+                '1.1.0',
+                true,
+                '<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
+<head>
+<title>Index of /galette/</title>
+<style type="text/css">
+a, a:active {text-decoration: none; color: blue;}
+a:visited {color: #48468F;}
+a:hover, a:focus {text-decoration: underline; color: red;}
+body {background-color: #F5F5F5;}
+h2 {margin-bottom: 12px;}
+table {margin-left: 12px;}
+th, td { font: 90% monospace; text-align: left;}
+th { font-weight: bold; padding-right: 14px; padding-bottom: 3px;}
+td {padding-right: 14px;}
+td.s, th.s {text-align: right;}
+div.list { background-color: white; border-top: 1px solid #646464; border-bottom: 1px solid #646464; padding-top: 10px; padding-bottom: 14px;}
+div.foot { font: 90% monospace; color: #787878; padding-top: 4px;}
+</style>
+</head>
+<body>
+<h2>Index of /galette/</h2>
+<div class="list">
+<table summary="Directory Listing" cellpadding="0" cellspacing="0">
+<thead><tr><th class="n">Name</th><th class="m">Last Modified</th><th class="s">Size</th><th class="t">Type</th></tr></thead>
+<tbody>
+<tr><td class="n"><a href="../">Parent Directory</a>/</td><td class="m">&nbsp;</td><td class="s">- &nbsp;</td><td class="t">Directory</td></tr>
+<tr><td class="n"><a href="archives/">archives</a>/</td><td class="m">2024-Jan-16 09:01:57</td><td class="s">- &nbsp;</td><td class="t">Directory</td></tr>
+<tr><td class="n"><a href="com/">com</a>/</td><td class="m">2016-Sep-22 22:23:10</td><td class="s">- &nbsp;</td><td class="t">Directory</td></tr>
+<tr><td class="n"><a href="dev/">dev</a>/</td><td class="m">2023-Nov-22 18:56:34</td><td class="s">- &nbsp;</td><td class="t">Directory</td></tr>
+<tr><td class="n"><a href="listes-galette/">listes-galette</a>/</td><td class="m">2017-Feb-02 02:06:43</td><td class="s">- &nbsp;</td><td class="t">Directory</td></tr>
+<tr><td class="n"><a href="plugins/">plugins</a>/</td><td class="m">2024-Jan-16 17:23:37</td><td class="s">- &nbsp;</td><td class="t">Directory</td></tr>
+<tr><td class="n"><a href="README">README</a></td><td class="m">2021-Sep-22 07:29:48</td><td class="s">0.1K</td><td class="t">text/plain</td></tr>
+<tr><td class="n"><a href="galette-1.0.0.tar.bz2">galette-1.0.0.tar.bz2</a></td><td class="m">2023-Dec-07 07:57:12</td><td class="s">8.7M</td><td class="t">application/octet-stream</td></tr>
+<tr><td class="n"><a href="galette-1.0.1.tar.bz2">galette-1.0.1.tar.bz2</a></td><td class="m">2024-Jan-16 09:00:19</td><td class="s">8.7M</td><td class="t">application/octet-stream</td></tr>
+<tr><td class="n"><a href="galette-1.0.2.tar.bz2">galette-1.0.2.tar.bz2</a></td><td class="m">2024-Feb-03 09:32:46</td><td class="s">8.7M</td><td class="t">application/octet-stream</td></tr>
+<tr><td class="n"><a href="galette-1.0.2.tar.bz2.asc">galette-1.0.2.tar.bz2.asc</a></td><td class="m">2024-Feb-03 09:32:46</td><td class="s">0.1K</td><td class="t">text/plain</td></tr>
+<tr><td class="n"><a href="galette-1.1.0.tar.bz2">galette-1.1.0.tar.bz2</a></td><td class="m">2024-Feb-16 09:09:13</td><td class="s">10M</td><td class="t">application/octet-stream</td></tr>
+<tr><td class="n"><a href="galette-1.1.0.tar.bz2.asc">galette-1.1.0.tar.bz2.asc</a></td><td class="m">2024-Feb-16 09:09:13</td><td class="s">0.1K</td><td class="t">text/plain</td></tr>
+<tr><td class="n"><a href="galette-dev.tar.bz2">galette-dev.tar.bz2</a></td><td class="m">2024-Feb-16 00:34:58</td><td class="s">8.7M</td><td class="t">application/octet-stream</td></tr>
+<tr><td class="n"><a href="galette-dev.tar.bz2.asc">galette-dev.tar.bz2.asc</a></td><td class="m">2023-Oct-19 17:32:27</td><td class="s">0.1K</td><td class="t">text/plain</td></tr>
+</tbody>
+</table>
+</div>
+<div class="foot">lighttpd/1.4.35</div>
+</body>
+</html>' //fakse 1.1.0 release
+            ],
+            [
+                '1.1.0',
+                '1.1.0',
+                false,
+                '<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
+<body>
+<div class="list">
+<table summary="Directory Listing" cellpadding="0" cellspacing="0">
+<tbody>
+<tr><td class="n"><a href="galette-1.1.0.tar.bz2">galette-1.1.0.tar.bz2</a></td><td class="m">2024-Feb-16 09:09:13</td><td class="s">10M</td><td class="t">application/octet-stream</td></tr>
+</tbody>
+</table>
+</div>
+</body>
+</html>' //fake 1.1.0 release
+            ],
+            [
+                '1.2.0',
+                '1.1.0',
+                false,
+                '<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
+<body>
+<div class="list">
+<table summary="Directory Listing" cellpadding="0" cellspacing="0">
+<tbody>
+<tr><td class="n"><a href="galette-1.1.0.tar.bz2">galette-1.1.0.tar.bz2</a></td><td class="m">2024-Feb-16 09:09:13</td><td class="s">10M</td><td class="t">application/octet-stream</td></tr>
+</tbody>
+</table>
+</div>
+</body>
+</html>' //fake 1.1.0 release
+            ],
+        ];
+    }
+
+    /**
+     * Test findLatestRelease
+     *
+     * @param string $current  Current release
+     * @param string $latest   Latest release
+     * @param bool   $expected Expected result
+     * @param string $page     Page content
+     *
+     * @dataProvider releasesPageProvider
+     * @return void
+     */
+    public function testFindLatestRelease(string $current, string $latest, bool $expected, string $page): void
+    {
+        $release = $this->getMockBuilder(\Galette\Util\Release::class)
+            ->onlyMethods(array('setupClient', 'getCurrentRelease'))
+            ->getMock();
+
+        // Create a mock and queue two responses.
+        $mock = new \GuzzleHttp\Handler\MockHandler([
+            new \GuzzleHttp\Psr7\Response(
+                200,
+                ['X-Foo' => 'Bar'],
+                $page
+            )
+        ]);
+
+        $handlerStack = \GuzzleHttp\HandlerStack::create($mock);
+        $client = new \GuzzleHttp\Client(['handler' => $handlerStack]);
+
+        $release->method('getCurrentRelease')->willReturn($current);
+        $release->method('setupClient')->willReturnCallback(function () use ($client) {
+            return $client;
+        });
+
+        $this->assertSame($expected, $release->checkNewRelease());
+        $this->assertSame($latest, $release->getLatestRelease());
+    }
+}