From edeb893e3d11ab31df44829bba170ac709f958fd Mon Sep 17 00:00:00 2001 From: Ahmad Afandi Date: Thu, 7 May 2026 14:20:06 +0700 Subject: [PATCH 1/5] fix: gambar logo desa pada website desa aktif --- app/Http/Controllers/ImageProxyController.php | 67 +++++++++ app/Policies/CustomCSPPolicy.php | 2 +- .../views/web/partials/property.blade.php | 5 +- routes/web.php | 4 + .../Controllers/ImageProxyControllerTest.php | 135 ++++++++++++++++++ 5 files changed, 211 insertions(+), 2 deletions(-) create mode 100644 app/Http/Controllers/ImageProxyController.php create mode 100644 tests/Feature/Http/Controllers/ImageProxyControllerTest.php diff --git a/app/Http/Controllers/ImageProxyController.php b/app/Http/Controllers/ImageProxyController.php new file mode 100644 index 00000000..2e5b5da1 --- /dev/null +++ b/app/Http/Controllers/ImageProxyController.php @@ -0,0 +1,67 @@ +get('url'); + + if (!$url || !filter_var($url, FILTER_VALIDATE_URL)) { + Log::warning('ImageProxyController: Invalid URL provided', ['url' => $url]); + abort(400, 'Invalid URL'); + } + + // Cache key berdasarkan URL + $cacheKey = 'image_proxy_' . md5($url); + + // Cek cache + $cachedImage = Cache::get($cacheKey); + if ($cachedImage) { + Log::info('ImageProxyController: Serving cached image', ['url' => $url]); + return response($cachedImage['content'])->header('Content-Type', $cachedImage['content_type']); + } + + Log::info('ImageProxyController: Fetching image from external URL', ['url' => $url]); + $response = Http::timeout(10)->get($url); + + 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') ?? ''; + + // Pastikan ini gambar + 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'); + } + + // Cache selama 1 jam + Cache::put($cacheKey, ['content' => $content, 'content_type' => $contentType], 3600); + Log::info('ImageProxyController: Image cached and served', ['url' => $url, 'content_type' => $contentType]); + + 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/resources/views/web/partials/property.blade.php b/resources/views/web/partials/property.blade.php index 56d450ef..9bda34bf 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 diff --git a/routes/web.php b/routes/web.php index 0b78f135..a18ab675 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 +Route::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..21e0879c --- /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(500); + } +} \ No newline at end of file From b00f14bb8d4913445355d1121235d83bc25cabc5 Mon Sep 17 00:00:00 2001 From: Ahmad Afandi Date: Thu, 7 May 2026 14:20:06 +0700 Subject: [PATCH 2/5] fix: gambar logo desa pada website desa aktif --- app/Http/Controllers/ImageProxyController.php | 67 +++++++++ app/Policies/CustomCSPPolicy.php | 2 +- .../views/web/partials/property.blade.php | 18 ++- routes/web.php | 4 + .../Controllers/ImageProxyControllerTest.php | 135 ++++++++++++++++++ 5 files changed, 220 insertions(+), 6 deletions(-) create mode 100644 app/Http/Controllers/ImageProxyController.php create mode 100644 tests/Feature/Http/Controllers/ImageProxyControllerTest.php diff --git a/app/Http/Controllers/ImageProxyController.php b/app/Http/Controllers/ImageProxyController.php new file mode 100644 index 00000000..2e5b5da1 --- /dev/null +++ b/app/Http/Controllers/ImageProxyController.php @@ -0,0 +1,67 @@ +get('url'); + + if (!$url || !filter_var($url, FILTER_VALIDATE_URL)) { + Log::warning('ImageProxyController: Invalid URL provided', ['url' => $url]); + abort(400, 'Invalid URL'); + } + + // Cache key berdasarkan URL + $cacheKey = 'image_proxy_' . md5($url); + + // Cek cache + $cachedImage = Cache::get($cacheKey); + if ($cachedImage) { + Log::info('ImageProxyController: Serving cached image', ['url' => $url]); + return response($cachedImage['content'])->header('Content-Type', $cachedImage['content_type']); + } + + Log::info('ImageProxyController: Fetching image from external URL', ['url' => $url]); + $response = Http::timeout(10)->get($url); + + 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') ?? ''; + + // Pastikan ini gambar + 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'); + } + + // Cache selama 1 jam + Cache::put($cacheKey, ['content' => $content, 'content_type' => $contentType], 3600); + Log::info('ImageProxyController: Image cached and served', ['url' => $url, 'content_type' => $contentType]); + + 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/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 0b78f135..a18ab675 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 +Route::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..21e0879c --- /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(500); + } +} \ No newline at end of file From 04c844e713a31298426868f64f6ca42df02bfde0 Mon Sep 17 00:00:00 2001 From: Ahmad Afandi Date: Thu, 7 May 2026 16:09:38 +0700 Subject: [PATCH 3/5] ikuti rekomendasi AI review --- app/Http/Controllers/ImageProxyController.php | 43 +++++++++++++------ config/image_proxy.php | 12 ++++++ routes/web.php | 4 +- 3 files changed, 43 insertions(+), 16 deletions(-) create mode 100644 config/image_proxy.php diff --git a/app/Http/Controllers/ImageProxyController.php b/app/Http/Controllers/ImageProxyController.php index 2e5b5da1..a6f881bb 100644 --- a/app/Http/Controllers/ImageProxyController.php +++ b/app/Http/Controllers/ImageProxyController.php @@ -10,12 +10,8 @@ class ImageProxyController extends Controller { - /** - * Proxy an image from an external URL with caching and validation. - * - * @param Request $request - * @return Response - */ + private const MAX_CACHE_BYTES = 1048576; // 1MB default, configurable + public function proxy(Request $request): Response { $url = $request->get('url'); @@ -25,18 +21,32 @@ public function proxy(Request $request): Response abort(400, 'Invalid URL'); } - // Cache key berdasarkan 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); - // Cek cache $cachedImage = Cache::get($cacheKey); if ($cachedImage) { Log::info('ImageProxyController: Serving cached image', ['url' => $url]); return response($cachedImage['content'])->header('Content-Type', $cachedImage['content_type']); } - Log::info('ImageProxyController: Fetching image from external URL', ['url' => $url]); - $response = Http::timeout(10)->get($url); + $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', [ @@ -49,7 +59,6 @@ public function proxy(Request $request): Response $content = $response->body(); $contentType = $response->header('Content-Type') ?? ''; - // Pastikan ini gambar if (!str_starts_with($contentType, 'image/')) { Log::warning('ImageProxyController: URL does not point to an image', [ 'url' => $url, @@ -58,9 +67,15 @@ public function proxy(Request $request): Response abort(400, 'URL does not point to an image'); } - // Cache selama 1 jam - Cache::put($cacheKey, ['content' => $content, 'content_type' => $contentType], 3600); - Log::info('ImageProxyController: Image cached and served', ['url' => $url, 'content_type' => $contentType]); + $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); } 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/routes/web.php b/routes/web.php index a18ab675..6712d1f4 100644 --- a/routes/web.php +++ b/routes/web.php @@ -457,5 +457,5 @@ Route::get('/module/kesehatan/{id}', [PresisiController::class, 'kesehatan'])->name('presisi.kesehatan'); Route::get('/statistik-kesehatan', [PresisiController::class, 'kesehatan'])->name('presisi.kesehatan'); -// Image proxy route -Route::get('/image-proxy', [ImageProxyController::class, 'proxy'])->name('image.proxy'); +// Image proxy route with rate limiting +Route::middleware('throttle:30,1')->get('/image-proxy', [ImageProxyController::class, 'proxy'])->name('image.proxy'); From 1ff1505e0bcfd1dd79a573cf860fc7eeb7664eed Mon Sep 17 00:00:00 2001 From: Ahmad Afandi Date: Thu, 7 May 2026 16:18:50 +0700 Subject: [PATCH 4/5] perbaiki test --- tests/Feature/Http/Controllers/ImageProxyControllerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Feature/Http/Controllers/ImageProxyControllerTest.php b/tests/Feature/Http/Controllers/ImageProxyControllerTest.php index 21e0879c..a4b7c552 100644 --- a/tests/Feature/Http/Controllers/ImageProxyControllerTest.php +++ b/tests/Feature/Http/Controllers/ImageProxyControllerTest.php @@ -130,6 +130,6 @@ public function test_proxy_with_exception_during_fetch() $response = $this->get('/image-proxy?url=' . urlencode($url)); - $response->assertStatus(500); + $response->assertStatus(404); } } \ No newline at end of file From c55e531bdd46eefd36afdab55cb46a29981db12d Mon Sep 17 00:00:00 2001 From: Ahmad Affandi Date: Wed, 20 May 2026 09:59:23 +0700 Subject: [PATCH 5/5] [ci skip] memutahirkan catatan rilis --- catatan_rilis.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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