diff --git a/app/Http/Controllers/ImageProxyController.php b/app/Http/Controllers/ImageProxyController.php new file mode 100644 index 00000000..a6f881bb --- /dev/null +++ b/app/Http/Controllers/ImageProxyController.php @@ -0,0 +1,82 @@ +get('url'); + + if (!$url || !filter_var($url, FILTER_VALIDATE_URL)) { + Log::warning('ImageProxyController: Invalid URL provided', ['url' => $url]); + abort(400, 'Invalid URL'); + } + + $parsed = parse_url($url); + $host = $parsed['host'] ?? null; + + if (!$host) { + Log::warning('ImageProxyController: Invalid host', ['url' => $url]); + abort(400, 'Invalid host'); + } + + $cacheKey = 'image_proxy_' . md5($url); + + $cachedImage = Cache::get($cacheKey); + if ($cachedImage) { + Log::info('ImageProxyController: Serving cached image', ['url' => $url]); + return response($cachedImage['content'])->header('Content-Type', $cachedImage['content_type']); + } + + $timeout = config('image_proxy.default_timeout', 10); + try { + $response = Http::timeout($timeout)->get($url); + } catch (\Exception $e) { + Log::warning('ImageProxyController: External image request failed', [ + 'url' => $url, + 'error' => $e->getMessage() + ]); + abort(404, 'Image not found'); + } + + if (!$response->successful()) { + Log::warning('ImageProxyController: External image request failed', [ + 'url' => $url, + 'status' => $response->status() + ]); + abort(404, 'Image not found'); + } + + $content = $response->body(); + $contentType = $response->header('Content-Type') ?? ''; + + if (!str_starts_with($contentType, 'image/')) { + Log::warning('ImageProxyController: URL does not point to an image', [ + 'url' => $url, + 'content_type' => $contentType + ]); + abort(400, 'URL does not point to an image'); + } + + $maxCacheBytes = config('image_proxy.max_cache_bytes', self::MAX_CACHE_BYTES); + $bytes = strlen($content); + + if ($bytes <= $maxCacheBytes) { + Cache::put($cacheKey, ['content' => $content, 'content_type' => $contentType], config('image_proxy.cache_ttl', 3600)); + Log::info('ImageProxyController: Image cached and served', ['url' => $url, 'content_type' => $contentType, 'bytes' => $bytes]); + } else { + Log::warning('ImageProxyController: Skipping cache, payload too large', ['url' => $url, 'bytes' => $bytes, 'max' => $maxCacheBytes]); + } + + return response($content)->header('Content-Type', $contentType); + } +} \ No newline at end of file diff --git a/app/Policies/CustomCSPPolicy.php b/app/Policies/CustomCSPPolicy.php index 0b7cf006..2408ef28 100644 --- a/app/Policies/CustomCSPPolicy.php +++ b/app/Policies/CustomCSPPolicy.php @@ -24,7 +24,7 @@ public function configure() $this->addDirective(Directive::IMG, ['blob:']) ->addDirective(Directive::STYLE, ['unsafe-inline']); } - $this->addDirective(Directive::IMG, ['data:', 'https://tile.openstreetmap.org/']) + $this->addDirective(Directive::IMG, [Keyword::SELF, 'data:', 'https://tile.openstreetmap.org/']) ->addDirective(Directive::STYLE, [ // 'unsafe-inline', 'https://fonts.googleapis.com/', diff --git a/catatan_rilis.md b/catatan_rilis.md index 2ace5bc0..bf532140 100644 --- a/catatan_rilis.md +++ b/catatan_rilis.md @@ -1,4 +1,4 @@ -Di rilis ini, versi 2605.0.1 berisi penambahan dan perbaikan yang diminta pengguna. +Di rilis ini, versi 2606.0.0 berisi penambahan dan perbaikan yang diminta pengguna. #### Penambahan Fitur @@ -21,6 +21,7 @@ Di rilis ini, versi 2605.0.1 berisi penambahan dan perbaikan yang diminta penggu 1. [#1023](https://github.com/OpenSID/OpenKab/issues/1023) Percobaan login gagal terkadang error 500 2. [#1026](https://github.com/OpenSID/OpenKab/issues/1026) Perbaikan fungsi insert media dan gambar pada tinymce artikel 3. [#1032](https://github.com/OpenSID/OpenKab/issues/1032) Perbaikan Tombol enter refresh halaman di kategori artikel opensid +4. [#1037](https://github.com/OpenSID/OpenKab/issues/1037) Perbaikan Gambar desa aktif pada halaman website openkab masih statis #### Perubahan Teknis diff --git a/config/image_proxy.php b/config/image_proxy.php new file mode 100644 index 00000000..5f58bd56 --- /dev/null +++ b/config/image_proxy.php @@ -0,0 +1,12 @@ + 1024 * 1024 * 5, // 5MB + + 'default_timeout' => 10, + + 'cache_ttl' => 3600, + + 'enabled' => env('IMAGE_PROXY_ENABLED', true), +]; \ No newline at end of file diff --git a/resources/views/web/partials/property.blade.php b/resources/views/web/partials/property.blade.php index 56d450ef..56b36aae 100644 --- a/resources/views/web/partials/property.blade.php +++ b/resources/views/web/partials/property.blade.php @@ -16,7 +16,7 @@
-
@@ -57,6 +57,9 @@ class="fa fa-home text-primary me-2">0 result.data.forEach((item, index) => { _elm = $('.replace-content-property .kelurahan-item').eq(index) _elm.find('.nama-desa-elm').text(item.attributes.nama_desa) + if(item.attributes.logo){ + _elm.find('.logo-desa-elm').attr('src','/image-proxy?url=' + encodeURIComponent(item.attributes.logo)) + } _elm.find('.website-elm').attr('href', item.attributes.website ?? '#') _elm.find('.penduduk-elm').html( '' + item.attributes @@ -67,10 +70,15 @@ class="fa fa-home text-primary me-2">0 _elm.find('.keluarga-elm').html( '' + item .attributes.keluarga + ' Keluarga') - _elm.find('.rtm-elm').html('' + - item.attributes.rtm + ' RTM') - }) - } + _elm.find('.rtm-elm').html('' + + item.attributes.rtm + ' RTM') + }) + + // Error handler untuk gambar logo yang gagal load + $('.logo-desa-elm').off('error').on('error', function() { + $(this).attr('src', '{{ asset('web/img/keluarahan-1.jpg') }}'); + }); + } }, 'json') }); diff --git a/routes/web.php b/routes/web.php index c5fe9747..e420dd6b 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,6 +1,7 @@ name('presisi.keluarga'); Route::get('/module/kesehatan/{id}', [PresisiController::class, 'kesehatan'])->name('presisi.kesehatan'); Route::get('/statistik-kesehatan', [PresisiController::class, 'kesehatan'])->name('presisi.kesehatan'); + +// Image proxy route with rate limiting +Route::middleware('throttle:30,1')->get('/image-proxy', [ImageProxyController::class, 'proxy'])->name('image.proxy'); diff --git a/tests/Feature/Http/Controllers/ImageProxyControllerTest.php b/tests/Feature/Http/Controllers/ImageProxyControllerTest.php new file mode 100644 index 00000000..a4b7c552 --- /dev/null +++ b/tests/Feature/Http/Controllers/ImageProxyControllerTest.php @@ -0,0 +1,135 @@ +get('/image-proxy?url=invalid-url'); + + $response->assertStatus(400); + } + + /** + * Test proxy with empty URL. + */ + public function test_proxy_with_empty_url() + { + $response = $this->get('/image-proxy'); + + $response->assertStatus(400); + } + + /** + * Test proxy with valid URL but non-image content type. + */ + public function test_proxy_with_non_image_content() + { + $url = 'https://example.com/image.jpg'; + + Http::fake([ + $url => Http::response('not an image', 200, ['Content-Type' => 'text/html']), + ]); + + $response = $this->get('/image-proxy?url=' . urlencode($url)); + + $response->assertStatus(400); + } + + /** + * Test proxy with successful image fetch. + */ + public function test_proxy_with_successful_image_fetch() + { + $url = 'https://example.com/image.jpg'; + $imageContent = 'fake image data'; + $contentType = 'image/jpeg'; + + Http::fake([ + $url => Http::response($imageContent, 200, ['Content-Type' => $contentType]), + ]); + + $response = $this->get('/image-proxy?url=' . urlencode($url)); + + $response->assertStatus(200) + ->assertHeader('Content-Type', $contentType); + + $this->assertEquals($imageContent, $response->getContent()); + } + + /** + * Test proxy serves from cache. + */ + public function test_proxy_serves_from_cache() + { + $url = 'https://example.com/image.jpg'; + $imageContent = 'cached image data'; + $contentType = 'image/png'; + $cacheKey = 'image_proxy_' . md5($url); + + // Manually set cache + Cache::put($cacheKey, ['content' => $imageContent, 'content_type' => $contentType], 3600); + + $response = $this->get('/image-proxy?url=' . urlencode($url)); + + $response->assertStatus(200) + ->assertHeader('Content-Type', $contentType); + + $this->assertEquals($imageContent, $response->getContent()); + } + + /** + * Test proxy with failed external request. + */ + public function test_proxy_with_failed_external_request() + { + $url = 'https://example.com/image.jpg'; + + Http::fake([ + $url => Http::response(null, 404), + ]); + + $response = $this->get('/image-proxy?url=' . urlencode($url)); + + $response->assertStatus(404); + } + + /** + * Test proxy with exception during fetch. + */ + public function test_proxy_with_exception_during_fetch() + { + $url = 'https://example.com/image.jpg'; + + Http::fake([ + $url => function () { + throw new \Exception('Network error'); + }, + ]); + + $response = $this->get('/image-proxy?url=' . urlencode($url)); + + $response->assertStatus(404); + } +} \ No newline at end of file