. */ namespace FireflyIII\Services\FireflyIIIOrg\Update; use Carbon\Carbon; use Exception; use GuzzleHttp\Client; use GuzzleHttp\Exception\ClientException; use Illuminate\Support\Facades\Log; use Override; use function Safe\file_get_contents; use function Safe\json_decode; class GitHubUpdateRequest implements UpdateRequestInterface { private string $currentVersion = '1.0.0'; private Carbon $currentBuild; private string $channel = 'stable'; private bool $localDebug = false; #[Override] public function getUpdateInformation(string $currentVersion, Carbon $currentBuild, string $channel): UpdateResponse { $this->currentVersion = $currentVersion; $this->channel = $channel; $this->currentBuild = $currentBuild; Log::debug(sprintf('Now in getUpdateInformation(%s, %s)', $currentVersion, $channel)); $response = new UpdateResponse(); $releases = $this->getReleases(); $filtered = $this->filterReleases($releases); Log::debug(sprintf('Left with %d release(s) to compare', count($filtered))); if (0 === count($filtered)) { $response->setNewVersionAvailable(false); return $response; } $newest = $this->getNewest($filtered); Log::debug(sprintf('Newest release is "%s" released on %s', $newest['version'], $newest['published_at']->format('Y-m-d H:i'))); $response->setNewVersion($newest['version']); $response->setPublishedAt($newest['published_at']); // main question, is this release newer than the current one? // if current version is "develop", compare date. if (str_contains($currentVersion, 'develop')) { Log::debug(sprintf('Compare with current version "%s", built on %s', $currentVersion, $currentBuild->format('Y-m-d H:i'))); if ($currentBuild->lt($newest['published_at']) && $currentBuild->diffInMinutes($newest['published_at']) > 10) { Log::debug(sprintf( 'Current build %s is older than newest build %s, so a new release is available.', $currentBuild->format('Y-m-d H:i:s e'), $newest['published_at']->format('Y-m-d H:i:s e') )); $response->setNewVersionAvailable(true); return $response; } if ($currentBuild->gt($newest['published_at'])) { Log::debug(sprintf( 'Current build %s is newer than newest build %s, so NO new release is available.', $currentBuild->format('Y-m-d H:i'), $newest['published_at']->format('Y-m-d H:i') )); $response->setNewVersionAvailable(false); return $response; } return $response; } // if not, compare version. $res = version_compare($newest['version'], $currentVersion); if ($res < 1) { $response->setNewVersionAvailable(false); return $response; } $response->setNewVersionAvailable(true); return $response; } private function filterReleases(array $releases): array { $return = []; $weekAgo = now()->subWeek(); /** @var array $release */ foreach ($releases as $release) { if ($release['published_at']->lte($this->currentBuild)) { Log::debug(sprintf('Skip older version "%s"', $release['version'])); continue; } // new version must be at least a week old, unless it is a develop release. if ($release['published_at']->gt($weekAgo) && !str_contains($release['version'], 'develop')) { Log::debug(sprintf('Skip too new version "%s"', $release['version'])); continue; } // if channel is stable, and version is "alpha", continue. // if channel is stable, and version is "beta", continue. // if channel is beta, and version is "alpha", continue. if (('stable' === $this->channel || 'beta' === $this->channel) && str_contains($release['version'], 'alpha')) { Log::debug(sprintf('Skip version "%s"', $release['version'])); continue; } if ('stable' === $this->channel && str_contains($release['version'], 'beta')) { Log::debug(sprintf('Skip version "%s"', $release['version'])); continue; } $return[] = $release; } return $return; } private function getNewest(array $releases): array { $latest = ['version' => '1.0.0', 'published_at' => now(config('app.timezone'))->startOfYear()]; foreach ($releases as $release) { Log::debug(sprintf('Comparing current version "%s" with latest version "%s"', $release['version'], $latest['version'])); if (str_contains($release['version'], 'develop') || str_contains($latest['version'], 'develop')) { Log::debug('Compare based on build time.'); // compare build times. if ($latest['published_at']->lt($release['published_at'])) { Log::debug(sprintf( 'Current date "%s" is newer than latest date "%s"', $release['published_at']->format('Y-m-d H:i'), $latest['published_at']->format('Y-m-d H:i') )); $latest = $release; } } if (!str_contains($release['version'], 'develop') && !str_contains($latest['version'], 'develop')) { Log::debug('[a] Compare based on version.'); // compare version. $res = version_compare($release['version'], $latest['version']); if (1 === $res) { Log::debug(sprintf('Version "%s" is newer than version "%s".', $release['version'], $latest['version'])); $latest = $release; } } } Log::debug(sprintf('Latest version seems to be "%s", released at %s', $latest['version'], $latest['published_at']->format('Y-m-d H:i e'))); return $latest; } private function getReleases(): array { $client = new Client(); $opts = ['timeout' => 5.0, 'headers' => ['User-Agent' => 'FireflyIII/'.config('firefly.version')]]; $return = []; $body = ''; if ($this->localDebug && file_exists('json.json')) { $body = file_get_contents('json.json'); } if (!$this->localDebug) { try { $res = $client->get('https://api.github.com/repos/firefly-iii/firefly-iii/releases', $opts); } catch (ClientException|Exception $e) { Log::error($e->getMessage()); return []; } Log::debug('Successfully contacted GitHub'); $body = (string) $res->getBody(); } if (!json_validate($body)) { Log::debug('GitHub returned invalid JSON'); return []; } $json = json_decode($body, true); /** @var array $release */ foreach ($json as $release) { $version = $release['tag_name']; $version = 'v' === $version[0] ? substr($version, 1) : $version; $published = Carbon::parse($release['published_at'], config('app.timezone')); // always skip "develop" releases, unless user is also running develop. if (str_contains($version, 'develop') && !str_contains($this->currentVersion, 'develop')) { Log::debug(sprintf('Skip version "%s"', $release['tag_name'])); continue; } $return[] = ['version' => $version, 'published_at' => $published]; } return $return; } }