Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions app/Enums/TipeMedia.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

/*
* File ini bagian dari:
*
* OpenDK
*
* Aplikasi dan source code ini dirilis berdasarkan lisensi GPL V3
*
* Hak Cipta 2017 - 2024 Perkumpulan Desa Digital Terbuka (https://opendesa.id)
*
* Dengan ini diberikan izin, secara gratis, kepada siapa pun yang mendapatkan salinan
* dari perangkat lunak ini dan file dokumentasi terkait ("Aplikasi Ini"), untuk diperlakukan
* tanpa batasan, termasuk hak untuk menggunakan, menyalin, mengubah dan/atau mendistribusikan,
* 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
* pemberitahuan ini melanggar ketentuan lisensi Aplikasi Ini.
*
* 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 ATAUPUN PENGGUNAAN ATAU LAINNYA TERKAIT APLIKASI INI.
*
* @package OpenDK
* @author Tim Pengembang OpenDesa
* @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
*/

namespace App\Enums;

use BenSampo\Enum\Enum;

/**
* Tipe media galeri untuk media berbasis link (Google Drive, YouTube, file langsung).
*
* Menentukan bagaimana media ditampilkan pada web maupun admin:
* - image : dirender sebagai <img>
* - video : dirender sebagai <video> atau <iframe> preview
* - youtube: dirender sebagai <iframe> embed YouTube
* - unknown: hanya ditampilkan sebagai tautan biasa
*/
final class TipeMedia extends Enum
{
/** @var string Media foto. */
public const Image = 'image';

/** @var string Media video. */
public const Video = 'video';

/** @var string Video YouTube, ditampilkan melalui iframe embed. */
public const Youtube = 'youtube';

/** @var string Link tidak dikenali sebagai media. */
public const Unknown = 'unknown';

/**
* Tipe media yang boleh dipilih manual oleh pengguna pada form galeri.
*
* @return array<string, string>
*/
public static function selectable(): array
{
return [
self::Image => 'Foto',
self::Video => 'Video',
self::Youtube => 'YouTube',
];
}

/**
* Daftar seluruh nilai tipe media yang valid.
*
* @return array<int, string>
*/
public static function values(): array
{
return [self::Image, self::Video, self::Youtube, self::Unknown];
}

/**
* Cek apakah nilai yang diberikan merupakan tipe media yang valid.
*/
public static function isValid(?string $value): bool
{
return $value !== null && in_array($value, self::values(), true);
}
}
32 changes: 27 additions & 5 deletions app/Http/Controllers/Publikasi/GaleriController.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
use App\Http\Requests\GaleriRequest;
use App\Models\Album;
use App\Models\Galeri;
use App\Services\MediaLinkService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Session;
Expand All @@ -43,7 +44,6 @@

class GaleriController extends Controller
{

public function index(Album $album)
{
$page_title = 'Album';
Expand All @@ -61,7 +61,7 @@ public function getDataGaleri(Request $request, Album $album)
->addIndexColumn()
->addColumn('aksi', function ($row) {
// $data['show_web'] = route('berita.detail', $row->slug);

if (!auth()->guest()) {
$data['edit_url'] = auth()->user()->can('access.publikasi.galeri.edit') ? route('publikasi.galeri.edit', $row->id) : null;
$data['delete_url'] = auth()->user()->can('access.publikasi.galeri.delete') ? route('publikasi.galeri.destroy', $row->id) : null;
Expand All @@ -74,17 +74,22 @@ public function getDataGaleri(Request $request, Album $album)

return view('forms.aksi', $data);
})
->addColumn('media', function (Galeri $row) {
return view('publikasi.galeri.parts.media-cell', [
'galeri' => $row,
]);
})
->editColumn('status', function ($row) {
if ($row->status == 0) {
return '<span class="label label-danger">Tidak Aktif</span>';
} else {
return '<span class="label label-success">Aktif</span>';
}
return '<span class="label label-success">Aktif</span>';

})
->editColumn('dibuat', function ($row) {
return format_datetime($row->created_at);
})
->rawColumns(['aksi', 'status', 'dibuat'])
->rawColumns(['aksi', 'media', 'status', 'dibuat'])
->make(true);
}
}
Expand Down Expand Up @@ -115,6 +120,7 @@ public function store(GaleriRequest $request)
}

$input['link'] = $input['jenis'] == 'file' ? null : ($input['link'] ?? null);
$input['media_type'] = $input['jenis'] == 'file' ? null : $this->detectMediaType($request);
$input['album_id'] = Session::get('album_id');

Galeri::create($input);
Expand Down Expand Up @@ -181,6 +187,7 @@ public function update(GaleriRequest $request, Galeri $galeri)
}

$input['link'] = ($input['jenis'] ?? null) == 'file' ? null : ($input['link'] ?? null);
$input['media_type'] = ($input['jenis'] ?? null) == 'file' ? null : $this->detectMediaType($request);

$galeri->update($input);
} catch (\Exception $e) {
Expand Down Expand Up @@ -212,4 +219,19 @@ public function destroy(Galeri $galeri)

return redirect()->route('publikasi.galeri.index', Session::get('album_id'))->with('success', 'Galeri sukses dihapus!');
}

/**
* Tentukan tipe media untuk galeri berbasis link.
*
* Pilihan manual pengguna selalu dipakai apa adanya. Bila tidak ada pilihan,
* tipe dideteksi dari link, dan hanya melakukan permintaan jaringan bila
* link-nya memang tidak memberi petunjuk (misalnya Google Drive).
*/
protected function detectMediaType(GaleriRequest $request): string
{
return app(MediaLinkService::class)->detectType(
$request->input('link'),
$request->selectedMediaType()
);
}
}
5 changes: 2 additions & 3 deletions app/Http/Middleware/SecurityHeaders.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

namespace App\Http\Middleware;

use App\Support\CspPolicy;
use Closure;

class SecurityHeaders
Expand Down Expand Up @@ -59,9 +60,7 @@ public function handle($request, Closure $next)
$response->headers->set('X-Content-Type-Options', 'nosniff');

if (app()->environment('production')) {
$localDomain = env('APP_URL', 'http://localhost');
$urlDatabaseGabungan = config('setting.api_server_database_gabungan');
$response->headers->set('Content-Security-Policy', "default-src 'self';script-src 'self' https://pantau.opensid.my.id/ https://cdnjs.cloudflare.com/ajax/libs/tinymce/ https://cdn.jsdelivr.net/npm/ https://cdnjs.cloudflare.com/ajax/libs/numeral.js/2.0.6/numeral.min.js https://website-widgets.pages.dev/dist/sienna.min.js platform.twitter.com unpkg.com 'unsafe-inline' 'unsafe-eval';style-src 'self' https://www.tiny.cloud/ http://www.tinymce.com/css/codepen.min.css https://cdnjs.cloudflare.com/ajax/libs/tinymce/ https://cdn.jsdelivr.net/npm/ fonts.googleapis.com unpkg.com 'unsafe-inline';img-src 'self' * data: blob:;font-src 'self' https://cdnjs.cloudflare.com/ajax/libs/tinymce/ https://cdn.jsdelivr.net/npm/ fonts.gstatic.com data:;connect-src 'self' ".$urlDatabaseGabungan." https://pantau.opensid.my.id/ ;media-src 'self';frame-src 'self' blob: data: platform.twitter.com github.com *.youtube.com *.vimeo.com *.opensid.my.id;object-src 'self' blob:;frame-ancestors 'self';base-uri 'self';");
$response->headers->set('Content-Security-Policy', (new CspPolicy())->toHeader());
}

return $response;
Expand Down
31 changes: 30 additions & 1 deletion app/Http/Requests/GaleriRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

namespace App\Http\Requests;

use App\Enums\TipeMedia;
use App\Services\FileUploadService;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class GaleriRequest extends FormRequest
{
Expand All @@ -30,10 +32,37 @@ public function rules()
return [
'judul' => 'required|string|max:191',
'jenis' => 'required|in:file,url',
'link' => $isUrl ? 'required|string|max:255' : 'nullable',
'link' => $isUrl ? 'required|string|max:255|url:http,https' : 'nullable',
'media_type' => [
'nullable',
Rule::in(array_keys(TipeMedia::selectable())),
],
'gambar' => (! $isUrl && $this->isMethod('post')) ? 'required|array' : 'nullable|array',
'gambar.*' => $isUrl ? 'nullable' : 'required|image|mimes:jpg,jpeg,png' . $maxRule . '|valid_file',
'status' => 'required',
];
}

/**
* Get custom messages for validator errors.
*
* @return array<string, string>
*/
public function messages()
{
return [
'link.url' => 'Link media harus berupa URL yang valid (diawali http:// atau https://).',
'media_type.in' => 'Tipe media harus salah satu dari: Foto, Video, atau YouTube.',
];
}

/**
* Tipe media yang dipilih manual, atau null bila biarkan dideteksi otomatis.
*/
public function selectedMediaType(): ?string
{
$mediaType = $this->input('media_type');

return is_string($mediaType) && $mediaType !== '' ? $mediaType : null;
}
}
85 changes: 70 additions & 15 deletions app/Models/Galeri.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,26 @@

namespace App\Models;

use App\Enums\TipeMedia;
use App\Observers\GaleriObserver;
use App\Services\MediaLinkService;
use App\ValueObjects\MediaLink;
use Cviebrock\EloquentSluggable\Sluggable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use App\Observers\GaleriObserver;

class Galeri extends Model
{
use HasFactory, Sluggable;

/**
* Register model lifecycle hooks.
*/
protected static function booted(): void
{
static::observe(GaleriObserver::class);
}

protected $fillable = [
'album_id',
'judul',
'gambar',
'link',
'jenis',
'media_type',
'status',
];

Expand All @@ -34,7 +30,7 @@ protected static function booted(): void
'status' => 'boolean',
];

protected $appends = ['gambar_path'];
protected $appends = ['gambar_path', 'media_type', 'media_url', 'media_embed_url'];

/**
* Return the sluggable configuration array for this model.
Expand Down Expand Up @@ -65,19 +61,78 @@ public function Album(): BelongsTo

public function getGambarPathAttribute(): string
{
// Media lokal selalu berupa foto, jadi cukup pakai berkas pertama.
if (($this->attributes['jenis'] ?? null) === 'file' && ! empty($this->gambar)) {
$gambar = is_array($this->gambar) ? ($this->gambar[0] ?? null) : $this->gambar;
if ($gambar) {
return isThumbnail('publikasi/galeri/' . $gambar);
}
}

if (($this->attributes['jenis'] ?? null) === 'url' && ! empty($this->link)) {
if (preg_match('/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/ ]{11})/', $this->link, $matches)) {
return 'https://img.youtube.com/vi/' . $matches[1] . '/hqdefault.jpg';
}
return $this->media()->thumbnail ?? asset('/img/no-image.png');
}

/**
* Tipe media galeri: image, video, youtube, atau unknown.
*
* Untuk media berbasis link, kolom media_type hasil deteksi saat penyimpanan
* lebih dipercaya daripada menebak ulang dari URL, karena link Google Drive
* tidak memiliki ekstensi berkas sehingga jenis media tidak bisa dibaca
* langsung dari URL.
*/
public function getMediaTypeAttribute(): string
{
if (($this->attributes['jenis'] ?? null) === 'file') {
return empty($this->gambar) ? TipeMedia::Unknown : TipeMedia::Image;
}

$stored = $this->attributes['media_type'] ?? null;

if (is_string($stored) && in_array($stored, [TipeMedia::Image, TipeMedia::Video, TipeMedia::Youtube], true)) {
return $stored;
}

return asset('/img/no-image.png');
return $this->media()->type;
}

/**
* URL media yang bisa dimuat langsung oleh browser (src).
*/
public function getMediaUrlAttribute(): ?string
{
if (($this->attributes['jenis'] ?? null) === 'file' && ! empty($this->gambar)) {
$gambar = is_array($this->gambar) ? ($this->gambar[0] ?? null) : $this->gambar;

return $gambar ? isThumbnail('publikasi/galeri/' . $gambar) : null;
}

return $this->media()->url;
}

/**
* URL untuk <iframe>, dipakai bila <video> tidak mendukung media tersebut.
*/
public function getMediaEmbedUrlAttribute(): ?string
{
return $this->media()->embedUrl;
}

/**
* Register model lifecycle hooks.
*/
protected static function booted(): void
{
static::observe(GaleriObserver::class);
}

/**
* Resolusi link galeri menjadi objek media siap tampil.
*/
protected function media(): MediaLink
{
return app(MediaLinkService::class)->resolve(
$this->attributes['link'] ?? null,
$this->attributes['media_type'] ?? null
);
}
}
Loading