Skip to content

Commit 53569d3

Browse files
authored
Fix issue with properly resolving url in http-based repository (#351)
Fix issue with properly resolving url in http-based repository
1 parent 3aebd3b commit 53569d3

File tree

10 files changed

+1101
-58
lines changed

10 files changed

+1101
-58
lines changed

.codacy.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
---
12
exclude_paths:
23
- '*.md'
34
- '**/tests/**'

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"ext-zip": "*",
3131
"guzzlehttp/guzzle": "^7.4",
3232
"laravel/framework": "^8.83.10",
33+
"league/uri": "^6.5",
3334
"phpstan/extension-installer": "^1.1",
3435
"phpstan/phpstan-phpunit": "^1.1"
3536
},

src/Exceptions/ReleaseException.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,24 @@ public static function noReleaseFound(string $version): self
1212

1313
return new self(sprintf('No release found for version "%s". Please check the repository you\'re pulling from', $version));
1414
}
15+
16+
public static function cannotExtractDownloadLink(string $pattern): self
17+
{
18+
return new self(sprintf('Cannot extract download/release link from source. Pattern "%s" not found.', $pattern));
19+
}
20+
21+
public static function archiveFileNotFound(string $path): self
22+
{
23+
return new self(sprintf('Archive file "%s" not found.', $path));
24+
}
25+
26+
public static function archiveNotAZipFile(string $mimeType): self
27+
{
28+
return new self(sprintf('File is not a zip archive. File is "%s"', $mimeType));
29+
}
30+
31+
public static function cannotExtractArchiveFile(string $path): self
32+
{
33+
return new self(sprintf('Cannot open zip archive "%s"', $path));
34+
}
1535
}

src/Models/Release.php

Lines changed: 22 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44

55
namespace Codedge\Updater\Models;
66

7+
use Codedge\Updater\Exceptions\ReleaseException;
78
use Codedge\Updater\Traits\SupportPrivateAccessToken;
89
use Exception;
9-
use Illuminate\Filesystem\Filesystem;
1010
use Illuminate\Http\Client\Response;
11+
use Illuminate\Support\Facades\File;
1112
use Illuminate\Support\Facades\Http;
1213
use Illuminate\Support\Str;
1314
use Symfony\Component\Finder\Finder;
@@ -45,13 +46,6 @@ final class Release
4546
*/
4647
private ?string $downloadUrl = null;
4748

48-
protected Filesystem $filesystem;
49-
50-
public function __construct(Filesystem $filesystem)
51-
{
52-
$this->filesystem = $filesystem;
53-
}
54-
5549
public function getRelease(): ?string
5650
{
5751
return $this->release;
@@ -73,8 +67,8 @@ public function setStoragePath(string $storagePath): self
7367
{
7468
$this->storagePath = $storagePath;
7569

76-
if (!$this->filesystem->exists($this->storagePath)) {
77-
$this->filesystem->makeDirectory($this->storagePath, 493, true, true);
70+
if (!File::exists($this->storagePath)) {
71+
File::makeDirectory($this->storagePath, 493, true, true);
7872
}
7973

8074
return $this;
@@ -135,6 +129,10 @@ public function setDownloadUrl(string $downloadUrl): self
135129

136130
public function extract(bool $deleteSource = true): bool
137131
{
132+
if (!File::exists($this->getStoragePath())) {
133+
throw ReleaseException::archiveFileNotFound($this->getStoragePath());
134+
}
135+
138136
$extractTo = createFolderFromFile($this->getStoragePath());
139137
$extension = pathinfo($this->getStoragePath(), PATHINFO_EXTENSION);
140138

@@ -143,30 +141,22 @@ public function extract(bool $deleteSource = true): bool
143141

144142
// Create the final release directory
145143
if ($extracted && $this->createReleaseFolder() && $deleteSource) {
146-
$this->filesystem->delete($this->storagePath);
144+
File::delete($this->getStoragePath());
147145
}
148146

149147
return true;
150148
} else {
151-
throw new Exception('File is not a zip archive. File is '.$this->filesystem->mimeType($this->getStoragePath()).'.');
149+
throw ReleaseException::archiveNotAZipFile(File::mimeType($this->getStoragePath()));
152150
}
153151
}
154152

155153
protected function extractZip(string $extractTo): bool
156154
{
157155
$zip = new \ZipArchive();
158-
159-
/*
160-
* @see https://bugs.php.net/bug.php?id=79296
161-
*/
162-
if (filesize($this->getStoragePath()) === 0) {
163-
$res = $zip->open($this->getStoragePath(), \ZipArchive::OVERWRITE);
164-
} else {
165-
$res = $zip->open($this->getStoragePath());
166-
}
156+
$res = $zip->open($this->getStoragePath());
167157

168158
if ($res !== true) {
169-
throw new Exception("Cannot open zip archive [{$this->getStoragePath()}]. Error: $res");
159+
throw ReleaseException::cannotExtractArchiveFile($this->getStoragePath());
170160
}
171161

172162
$extracted = $zip->extractTo($extractTo);
@@ -202,11 +192,11 @@ public function download(): Response
202192
*/
203193
protected function createReleaseFolder(): bool
204194
{
205-
$folders = $this->filesystem->directories(createFolderFromFile($this->getStoragePath()));
195+
$folders = File::directories(createFolderFromFile($this->getStoragePath()));
206196

207197
if (count($folders) === 1) {
208198
// Only one sub-folder inside extracted directory
209-
$moved = $this->filesystem->moveDirectory(
199+
$moved = File::moveDirectory(
210200
$folders[0],
211201
createFolderFromFile($this->getStoragePath()).now()->toDateString()
212202
);
@@ -215,14 +205,14 @@ protected function createReleaseFolder(): bool
215205
return false;
216206
}
217207

218-
$this->filesystem->moveDirectory(
208+
File::moveDirectory(
219209
createFolderFromFile($this->getStoragePath()).now()->toDateString(),
220210
createFolderFromFile($this->getStoragePath()),
221211
true
222212
);
223213
}
224214

225-
$this->filesystem->delete($this->getStoragePath());
215+
File::delete($this->getStoragePath());
226216

227217
return true;
228218
}
@@ -235,20 +225,20 @@ public function isSourceAlreadyFetched(): bool
235225
$extractionDir = createFolderFromFile($this->getStoragePath());
236226

237227
// Check if source archive is (probably) deleted but extracted folder is there.
238-
if (!$this->filesystem->exists($this->getStoragePath())
239-
&& $this->filesystem->exists($extractionDir)) {
228+
if (!File::exists($this->getStoragePath())
229+
&& File::exists($extractionDir)) {
240230
return true;
241231
}
242232

243233
// Check if source archive is there but not extracted
244-
if ($this->filesystem->exists($this->getStoragePath())
245-
&& !$this->filesystem->exists($extractionDir)) {
234+
if (File::exists($this->getStoragePath())
235+
&& !File::exists($extractionDir)) {
246236
return true;
247237
}
248238

249239
// Check if source archive and folder exists
250-
if ($this->filesystem->exists($this->getStoragePath())
251-
&& $this->filesystem->exists($extractionDir)) {
240+
if (File::exists($this->getStoragePath())
241+
&& File::exists($extractionDir)) {
252242
return true;
253243
}
254244

src/SourceRepositoryTypes/HttpRepositoryType.php

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Illuminate\Support\Facades\Log;
2121
use Illuminate\Support\Str;
2222
use InvalidArgumentException;
23+
use League\Uri\Uri;
2324

2425
class HttpRepositoryType implements SourceRepositoryTypeContract
2526
{
@@ -79,7 +80,7 @@ public function isNewVersionAvailable(string $currentVersion = ''): bool
7980
/**
8081
* Fetches the latest version. If you do not want the latest version, specify one and pass it.
8182
*
82-
* @throws GuzzleException|ReleaseException
83+
* @throws ReleaseException
8384
*/
8485
public function fetch(string $version = ''): Release
8586
{
@@ -176,26 +177,41 @@ final public function getReleases(): Response
176177
return Http::withHeaders($headers)->get($repositoryUrl);
177178
}
178179

179-
private function extractFromHtml(string $content): Collection
180+
/**
181+
* @throws ReleaseException
182+
*/
183+
public function extractFromHtml(string $content): Collection
180184
{
181-
$format = str_replace(
182-
'_VERSION_',
183-
'(\d+\.\d+\.\d+)',
184-
str_replace('.', '\.', $this->config['pkg_filename_format'])
185-
).'.zip';
186-
$linkPattern = '<a.*href="(.*'.$format.')"';
185+
$format = str_replace('_VERSION_', '(\d+\.\d+\.\d+)', $this->config['pkg_filename_format']).'.zip';
186+
$linkPattern = 'a.*href="(.*'.$format.')"';
187+
188+
preg_match_all('<'.$linkPattern.'>i', $content, $files);
189+
$files = array_filter($files);
190+
191+
if (count($files) === 0) {
192+
throw ReleaseException::cannotExtractDownloadLink($format);
193+
}
194+
195+
// Special handling when file version cannot be properly detected
196+
if (!array_key_exists(2, $files)) {
197+
foreach ($files[1] as $key=>$val) {
198+
preg_match('/[a-zA-Z\-]([.\d]*)(?=\.\w+$)/', $val, $versions);
199+
$files[][$key] = $versions[1];
200+
}
201+
}
202+
203+
$releaseVersions = array_combine($files[2], $files[1]);
187204

188-
preg_match_all('/'.$linkPattern.'/i', $content, $files);
189-
$releaseVersions = $files[2];
205+
$uri = Uri::createFromString($this->config['repository_url']);
206+
$baseUrl = $uri->getScheme().'://'.$uri->getHost();
190207

191-
// Extract domain only
192-
preg_match('/(?:\w+:)?\/\/[^\/]+([^?#]+)/', $this->config['repository_url'], $matches);
193-
$baseUrl = preg_replace('#'.$matches[1].'#', '', $this->config['repository_url']);
208+
$releases = collect($releaseVersions)->map(function ($item, $key) use ($baseUrl) {
209+
$uri = Uri::createFromString($item);
210+
$item = $uri->getHost() ? $item : $baseUrl.Str::start($item, '/');
194211

195-
$releases = collect($files[1])->map(function ($item, $key) use ($baseUrl, $releaseVersions) {
196212
return (object) [
197-
'name' => $releaseVersions[$key],
198-
'zipball_url' => $baseUrl.$item,
213+
'name' => $key,
214+
'zipball_url' => $item,
199215
];
200216
});
201217

tests/Data/Http/releases-http_local.json

Lines changed: 983 additions & 0 deletions
Large diffs are not rendered by default.

tests/SourceRepositoryTypes/GitlabRepositoryTypeTest.php

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -107,12 +107,9 @@ public function it_cannot_fetch_releases_because_there_is_no_release(): void
107107
/** @var GitlabRepositoryType $gitlab */
108108
$gitlab = resolve(GitlabRepositoryType::class);
109109

110-
Http::fakeSequence()
111-
->pushResponse($this->getResponse200Type('gitlab'))
112-
->pushResponse($this->getResponseEmpty())
113-
->pushResponse($this->getResponseEmpty());
114-
115-
$this->assertInstanceOf(Release::class, $gitlab->fetch());
110+
Http::fake([
111+
'*' => $this->getResponseEmpty(),
112+
]);
116113

117114
$this->expectException(ReleaseException::class);
118115
$this->expectExceptionMessage('No release found for version "latest version". Please check the repository you\'re pulling from');

tests/SourceRepositoryTypes/HttpRepositoryTypeTest.php

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44

55
namespace Codedge\Updater\Tests\SourceRepositoryTypes;
66

7+
use Codedge\Updater\Exceptions\ReleaseException;
78
use Codedge\Updater\Exceptions\VersionException;
89
use Codedge\Updater\Models\Release;
910
use Codedge\Updater\SourceRepositoryTypes\HttpRepositoryType;
1011
use Codedge\Updater\Tests\TestCase;
1112
use Exception;
13+
use Illuminate\Support\Facades\File;
1214
use Illuminate\Support\Facades\Http;
1315

1416
final class HttpRepositoryTypeTest extends TestCase
@@ -68,9 +70,9 @@ public function it_cannot_fetch_releases_because_there_is_no_release(): void
6870
->pushResponse($this->getResponse200HttpType())
6971
->pushResponse($this->getResponse200ZipFile());
7072

71-
$this->assertInstanceOf(Release::class, $http->fetch());
73+
//$this->expectException(ReleaseException::class);
74+
//$this->expectExceptionMessage('Archive file "/tmp/self-updater/v/invoiceninja/invoiceninja/archive/v4.5.17.zip" not found.');
7275

73-
$this->expectException(Exception::class);
7476
$this->assertInstanceOf(Release::class, $http->fetch());
7577
}
7678

@@ -86,6 +88,8 @@ public function it_can_fetch_http_releases(): void
8688
->pushResponse($this->getResponse200HttpType())
8789
->pushResponse($this->getResponse200HttpType());
8890

91+
File::shouldReceive('exists')->andReturnTrue();
92+
8993
$release = $http->fetch();
9094

9195
$this->assertInstanceOf(Release::class, $release);
@@ -169,4 +173,35 @@ public function it_can_get_new_version_available_without_version_file(): void
169173
$this->assertTrue($http->isNewVersionAvailable('4.5'));
170174
$this->assertFalse($http->isNewVersionAvailable('5.0'));
171175
}
176+
177+
/** @test */
178+
public function it_can_build_releases_from_local_source(): void
179+
{
180+
config(['self-update.repository_types.http.repository_url' => 'http://update-server.localhost/']);
181+
config(['self-update.repository_types.http.pkg_filename_format' => 'my-test-project-\d+\.\d+']);
182+
183+
/** @var HttpRepositoryType $http */
184+
$http = $this->app->make(HttpRepositoryType::class);
185+
$content = file_get_contents('tests/Data/Http/releases-http_local.json');
186+
187+
$collection = $http->extractFromHtml($content);
188+
189+
$this->assertSame('1.0', $collection->first()->name);
190+
$this->assertSame('http://update-server.localhost/my534/my-test-project/-/archive/1.0/my-test-project-1.0.zip', $collection->first()->zipball_url);
191+
}
192+
193+
/** @test */
194+
public function it_can_build_releases_from_github_source(): void
195+
{
196+
config(['self-update.repository_types.http.repository_url' => 'https://github.com/']);
197+
198+
/** @var HttpRepositoryType $http */
199+
$http = $this->app->make(HttpRepositoryType::class);
200+
$content = file_get_contents('tests/Data/Http/releases-http_gh.json');
201+
202+
$collection = $http->extractFromHtml($content);
203+
204+
$this->assertSame('4.5.17', $collection->first()->name);
205+
$this->assertSame('https://github.com/invoiceninja/invoiceninja/archive/v4.5.17.zip', $collection->first()->zipball_url);
206+
}
172207
}

tests/TestCase.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ abstract class TestCase extends Orchestra
2121
protected array $mockedResponses = [
2222
'tag' => 'releases-tag.json',
2323
'branch' => 'releases-branch.json',
24-
'http' => 'releases-http.json',
24+
'http' => 'releases-http_gh.json',
2525
'gitlab' => 'releases-gitlab.json',
2626
];
2727

@@ -58,7 +58,7 @@ protected function getEnvironmentSetUp($app): void
5858

5959
protected function getResponse200HttpType(): PromiseInterface
6060
{
61-
$stream = Utils::streamFor(fopen('tests/Data/'.$this->mockedResponses['http'], 'r'));
61+
$stream = Utils::streamFor(fopen('tests/Data/Http/'.$this->mockedResponses['http'], 'r'));
6262
$response = $stream->getContents();
6363

6464
return Http::response($response, 200, [

0 commit comments

Comments
 (0)