Skip to content

Commit 3d9d5fe

Browse files
committed
Theme Modules: Updated install command to handle nested folder
Theme module ZIPs will now support their files being in a single nested directory within a ZIP, to support common ZIP structure approaches. Added test to cover. For #6066
1 parent 5e78dc6 commit 3d9d5fe

3 files changed

Lines changed: 83 additions & 2 deletions

File tree

app/Theming/ThemeModuleZip.php

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,41 @@ public function extractTo(string $destinationPath): void
1515
{
1616
$zip = new ZipArchive();
1717
$zip->open($this->path);
18-
$zip->extractTo($destinationPath);
18+
$prefix = $this->getZipContentPrefix($zip);
19+
20+
for ($i = 0; $i < $zip->numFiles; $i++) {
21+
$name = $zip->getNameIndex($i);
22+
$entryIsDir = str_ends_with($name, "/");
23+
if ($entryIsDir) {
24+
continue;
25+
}
26+
27+
$stream = $zip->getStreamIndex($i);
28+
29+
if ($prefix) {
30+
if (!str_starts_with($name, $prefix) || $name === $prefix) {
31+
continue;
32+
}
33+
$name = str_replace($prefix, '', $name);
34+
}
35+
36+
$targetPath = $destinationPath . DIRECTORY_SEPARATOR . $name;
37+
$targetPathDir = dirname($targetPath);
38+
if (!is_dir($targetPathDir)) {
39+
$dirCreated = mkdir($targetPathDir, 0777, true);
40+
if (!$dirCreated) {
41+
throw new ThemeModuleException("Failed to create directory {$targetPathDir} when extracting module files");
42+
}
43+
}
44+
45+
$targetFile = fopen($targetPath, 'w');
46+
$written = stream_copy_to_stream($stream, $targetFile);
47+
if (!$written) {
48+
throw new ThemeModuleException("Failed to write to {$targetPath} when extracting module files");
49+
}
50+
fclose($targetFile);
51+
}
52+
1953
$zip->close();
2054
}
2155

@@ -31,7 +65,8 @@ public function getModuleInstance(): ThemeModule
3165
throw new ThemeModuleException("Unable to open zip file at {$this->path}");
3266
}
3367

34-
$moduleJsonText = $zip->getFromName('bookstack-module.json');
68+
$prefix = $this->getZipContentPrefix($zip);
69+
$moduleJsonText = $zip->getFromName("{$prefix}bookstack-module.json");
3570
$zip->close();
3671

3772
if ($moduleJsonText === false) {
@@ -95,4 +130,20 @@ public function getContentsSize(): int
95130

96131
return $totalSize;
97132
}
133+
134+
protected function getZipContentPrefix(ZipArchive $zip): string
135+
{
136+
$index = $zip->locateName('bookstack-module.json', ZipArchive::FL_NODIR);
137+
if ($index === false) {
138+
return '';
139+
}
140+
141+
$location = $zip->getNameIndex($index);
142+
$pathParts = explode('/', $location);
143+
if (count($pathParts) !== 2) {
144+
return '';
145+
}
146+
147+
return $pathParts[0] . '/';
148+
}
98149
}

dev/docs/theme-system-modules.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ Here are some general best practices when it comes to creating modules:
6666
### Distribution Format
6767

6868
Modules are expected to be distributed as a compressed ZIP file, where the ZIP contents follow that of a module folder.
69+
Contents may optionally be placed within a nested folder inside the ZIP.
6970
BookStack provides a `php artisan bookstack:install-module` command which allows modules to be installed from these ZIP files, either from a local path or from a web URL.
7071
Currently, there's a hardcoded total filesize limit of 50MB for module contents installed via this method.
7172

tests/Commands/InstallModuleCommandTest.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,35 @@ public function test_run_with_invalid_module_data_has_early_exit()
175175
->assertExitCode(1);
176176
}
177177

178+
public function test_module_zip_when_files_in_nested_directory()
179+
{
180+
$this->usingThemeFolder(function ($themeFolder) {
181+
$zip = new ZipArchive();
182+
$zipFile = tempnam(sys_get_temp_dir(), 'bs-test-module');
183+
$zip->open($zipFile, ZipArchive::CREATE);
184+
185+
$zip->addEmptyDir('mod');
186+
$zip->addFromString('mod/bookstack-module.json', json_encode($metadata ?? [
187+
'name' => 'Test Module',
188+
'description' => 'A test module for BookStack',
189+
'version' => '1.0.0',
190+
]));
191+
$zip->addFromString('mod/functions.php', '<?php $a = "cat";');
192+
$zip->addEmptyDir('mod/a');
193+
$zip->addFromString('mod/a/cat.txt', 'Meow');
194+
$zip->close();
195+
196+
$this->artisan('bookstack:install-module', ['location' => $zipFile])
197+
->expectsConfirmation('Are you sure you want to install this module?', 'yes')
198+
->assertExitCode(0);
199+
200+
$modulePath = glob(theme_path('modules/*'), GLOB_ONLYDIR)[0];
201+
$this->assertFileExists($modulePath . '/a/cat.txt');
202+
$contents = file_get_contents($modulePath . '/a/cat.txt');
203+
$this->assertEquals('Meow', $contents);
204+
});
205+
}
206+
178207
public function test_local_module_install_without_active_theme_can_setup_theme_folder()
179208
{
180209
$zip = $this->getModuleZipPath();

0 commit comments

Comments
 (0)