Skip to content

Remote Icons #107

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 29 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
47fc3cd
feat: allow remote icons
expxx Apr 3, 2025
85e29bc
fix: allow http url to "escape" directory
expxx Apr 3, 2025
24cfa19
fix: "escape" x2
expxx Apr 3, 2025
b746581
feat: too many things
expxx Apr 3, 2025
6d5f48d
Merge branch 'BlueprintFramework:main' into feat/remote-icon
expxx Apr 3, 2025
abd7cf4
fix: exclude links from escape protection
expxx Apr 3, 2025
f4ba269
Merge branch 'feat/remote-icon' of github.com:expxx/blueprint-framewo…
expxx Apr 3, 2025
e806ea1
fix: formatting
expxx Apr 3, 2025
9758ecf
fix: previously allows .svg anywhere, bad idea
expxx Apr 3, 2025
ce0ce57
fix: default icon
expxx Apr 3, 2025
bf40161
fix: use a common name for icons
expxx Apr 3, 2025
8d5bb03
chore(dbg): add temporary debug
expxx Apr 3, 2025
1c178b8
fix: icon display in admin panel
expxx Apr 3, 2025
d47a2be
fix: copy extension
expxx Apr 3, 2025
0a03258
fix: use remote url instead of downloading
expxx Apr 3, 2025
d44f4c3
fix: rendering on admin panel
expxx Apr 3, 2025
1d50fcc
fix: rendering on admin panel x4
expxx Apr 3, 2025
f6a9986
fix: security on escape protection & matching
expxx Apr 3, 2025
420aed5
feat: image proxy?
expxx Apr 3, 2025
ca0b007
feat: add extensionConfig to $blueprint
expxx Apr 3, 2025
6b08c8c
fix: if extension not found, return [] instead of error
expxx Apr 3, 2025
71b0ae8
fix: view -> response
expxx Apr 3, 2025
6a08e78
fix: all of the things
expxx Apr 3, 2025
287d23b
fix: better cache key & add .jpg
expxx Apr 3, 2025
ea61ebb
Merge pull request #2 from expxx/feat/remote-icon-schenanigans
expxx Apr 3, 2025
476110d
chore: remove comments (unnecessary)
expxx Apr 3, 2025
739fc4c
feat: cache for up to 1 day (i forgor)
expxx Apr 3, 2025
0a44d2f
Merge pull request #3 from expxx/feat/remote-icon-schenanigans
expxx Apr 3, 2025
242a625
fix: better cache key, less likely to interfere with something
expxx Apr 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,27 @@ public function extension(string $identifier): bool
return str_contains($this->fileRead(base_path('.blueprint/extensions/blueprint/private/db/installed_extensions')), $identifier);
}

/**
* Returns the extension's configuration as an associative array.
*
* @param string $identifier Extension identifier
* @return array Extension configuration
*
* [BlueprintExtensionLibrary documentation](https://blueprint.zip/docs/?page=documentation/$blueprint)
*/
public function extensionConfig(string $identifier): array
{
if (!file_exists(base_path(".blueprint/extensions/$identifier/private/.store/conf.yml")))
return [];
if (!is_readable(base_path(".blueprint/extensions/$identifier/private/.store/conf.yml")))
return [];

$conf = Yaml::parse($this->fileRead(base_path(".blueprint/extensions/$identifier/private/.store/conf.yml")));

return array_filter($conf['info'], fn ($k) => !!$k);
}


/**
* Returns an array containing all installed extensions's identifiers.
*
Expand Down
70 changes: 70 additions & 0 deletions app/Http/Controllers/Admin/ImageProxy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

namespace Pterodactyl\Http\Controllers\Admin;

use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Artisan;
use Illuminate\View\Factory as ViewFactory;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Services\Helpers\SoftwareVersionService;
use Pterodactyl\BlueprintFramework\Services\PlaceholderService\BlueprintPlaceholderService;
use Pterodactyl\BlueprintFramework\Libraries\ExtensionLibrary\Admin\BlueprintAdminLibrary as BlueprintExtensionLibrary;
use Database\Seeders\BlueprintSeeder;

class ImageProxy extends Controller
{
/**
* ExtensionsController constructor.
*/
public function __construct(
private BlueprintExtensionLibrary $blueprint,
)
{}

/**
* Return the admin index view.
*/
public function index(string $extension)
{
$ext = $this->blueprint->extensionConfig($extension);
if ($ext === null || $ext === []) {
abort(404, 'Extension not found.');
}

if(!isset($ext['icon'])) {

return response()->file(public_path('assets/extensions/' . $extension . '/icon.jpg'), [
'Content-Type' => 'image/jpeg',
'Content-Disposition' => 'inline; filename="icon.jpg"'
]);
}

if (!\str_starts_with($ext['icon'], 'http')) {
return response()->file('public/assets/extensions/' . $extension . '/icon.' . pathinfo($ext['icon'], PATHINFO_EXTENSION), [
'Content-Type' => 'image/image',
'Content-Disposition' => 'inline'
]);
}

$cache_key = 'blueprint_extension_icon_' . $extension;
if (!Cache::has($cache_key)) {
$iconContent = @file_get_contents($ext['icon']);
if ($iconContent === false) {
abort(404, 'Failed to fetch the icon from the provided URL.');
}
Cache::put($cache_key, $iconContent, now()->addDays(1));
}
$icon = Cache::get($cache_key);
if (empty($icon)) {
abort(404, 'Icon not found.');
}

$tempFile = tempnam(sys_get_temp_dir(), 'icon_') . '.jpg';
file_put_contents($tempFile, $icon);

return response()->file($tempFile, [
'Content-Type' => 'image/jpeg',
'Content-Disposition' => 'inline; filename="icon.jpg"'
])->deleteFileAfterSend(true);
}
}
4 changes: 1 addition & 3 deletions resources/views/admin/extensions.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,7 @@
'EXTENSION_ID' => $extension['identifier'],
'EXTENSION_NAME' => $extension['name'],
'EXTENSION_VERSION' => $extension['version'],
'EXTENSION_ICON' => !empty($extension['icon'])
? '/assets/extensions/'.$extension['identifier'].'/icon.'.pathinfo($extension['icon'], PATHINFO_EXTENSION)
: '/assets/extensions/'.$extension['identifier'].'/icon.jpg'
'EXTENSION_ICON' => '/admin/extensions/img/'.$extension['identifier'].'.jpg',
])
@endforeach

Expand Down
1 change: 1 addition & 0 deletions routes/blueprint.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
Route::group(['prefix' => 'extensions'], function () {
Route::get('/', [Admin\ExtensionsController::class, 'index'])->name('admin.extensions');
Route::get('/img/{extension}.jpg', [Admin\ImageProxy::class, 'index'])->name('admin.extensions.img');
});

Route::group(['prefix' => 'extensions/blueprint'], function () {
Expand Down
19 changes: 16 additions & 3 deletions scripts/commands/extensions/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ InstallExtension() {
((PROGRESS_NOW++))

# "prevent" folder "escaping"
if [[ ( $icon == "/"* ) || ( $icon == *"/.."* ) || ( $icon == *"../"* ) || ( $icon == *"/../"* ) || ( $icon == *"~"* ) || ( $icon == *"\\"* ) ]] \
if [[(( $icon == "/"* ) || ( $icon == *"/.."* ) || ( $icon == *"../"* ) || ( $icon == *"/../"* ) || ( $icon == *"~"* ) || ( $icon == *"\\"* ) ) && !( ${icon} =~ ^https?:// ) ]] \
|| [[ ( $admin_view == "/"* ) || ( $admin_view == *"/.."* ) || ( $admin_view == *"../"* ) || ( $admin_view == *"/../"* ) || ( $admin_view == *"~"* ) || ( $admin_view == *"\\"* ) ]] \
|| [[ ( $admin_controller == "/"* ) || ( $admin_controller == *"/.."* ) || ( $admin_controller == *"../"* ) || ( $admin_controller == *"/../"* ) || ( $admin_controller == *"~"* ) || ( $admin_controller == *"\\"* ) ]] \
|| [[ ( $admin_css == "/"* ) || ( $admin_css == *"/.."* ) || ( $admin_css == *"../"* ) || ( $admin_css == *"/../"* ) || ( $admin_css == *"~"* ) || ( $admin_css == *"\\"* ) ]] \
Expand Down Expand Up @@ -410,7 +410,7 @@ InstallExtension() {

# Validate paths to files and directories defined in conf.yml.
if \
[[ ( ! -f ".blueprint/tmp/$n/$icon" ) && ( ${icon} != "" ) ]] || # file: icon (optional)
[[ ( ! -f ".blueprint/tmp/$n/$icon" ) && ( ${icon} != "" ) && ! ( ${icon} =~ ^https?:// ) ]] || # file: icon (optional)
[[ ( ! -f ".blueprint/tmp/$n/$admin_view" ) ]] || # file: admin_view
[[ ( ! -f ".blueprint/tmp/$n/$admin_controller" ) && ( ${admin_controller} != "" ) ]] || # file: admin_controller (optional)
[[ ( ! -f ".blueprint/tmp/$n/$admin_css" ) && ( ${admin_css} != "" ) ]] || # file: admin_css (optional)
Expand Down Expand Up @@ -1129,9 +1129,22 @@ InstallExtension() {
*.webp) local ICON_EXT="webp" ;;
*) local ICON_EXT="jpg" ;;
esac
cp ".blueprint/tmp/$n/$icon" ".blueprint/extensions/$identifier/assets/icon.$ICON_EXT"
if [[ $icon == "http"* ]]; then
if [[ $icon != *".svg" && $icon != *".png" && $icon != *".gif" && $icon != *".jpeg" && $icon != *".webp" ]]; then
PRINT WARNING "Icon URL does not end with a valid image extension, using default icon instead."
icnNUM=$(( 1 + RANDOM % 5 ))
cp ".blueprint/assets/Extensions/Defaults/$icnNUM.jpg" ".blueprint/extensions/$identifier/assets/icon.jpg"
else
# download icon from url
PRINT INFO "parsing icon.."
ICON_OVERRIDE=$icon
fi
fi
fi;
ICON="/assets/extensions/$identifier/icon.$ICON_EXT"
if [[ $ICON_OVERRIDE != "" ]]; then
ICON=$ICON_OVERRIDE
fi

((PROGRESS_NOW++))

Expand Down