diff --git a/app/Conductors/MediaConductor.php b/app/Conductors/MediaConductor.php index 1aaefb8..7c1e8c7 100644 --- a/app/Conductors/MediaConductor.php +++ b/app/Conductors/MediaConductor.php @@ -69,10 +69,12 @@ class MediaConductor extends Conductor { $user = auth()->user(); if ($user === null) { - $builder->where('security_type', ''); + $builder->where('security_type', '') + ->orWhere('security_type', 'password'); } else { $builder->where(function ($query) use ($user) { $query->where('security_type', '') + ->orWhere('security_type', 'password') ->orWhere(function ($subquery) use ($user) { $subquery->where('security_type', 'permission') ->whereIn('security_data', $user->permissions); @@ -89,12 +91,14 @@ class MediaConductor extends Conductor */ public static function viewable(Model $model): bool { - if ($model->permission !== '') { + if (strcasecmp('permission', $model->security_type) === 0) { /** @var \App\Models\User */ $user = auth()->user(); - if ($user === null || $user->hasPermission($model->permission) === false) { + if ($user === null || $user->hasPermission($model->security_data) === false) { return false; } + } else if($model->security_type !== '' && strcasecmp('password', $model->security_type) !== 0) { + return false; } return true; diff --git a/app/Http/Controllers/Api/MediaController.php b/app/Http/Controllers/Api/MediaController.php index 658c2b3..cb5e16e 100644 --- a/app/Http/Controllers/Api/MediaController.php +++ b/app/Http/Controllers/Api/MediaController.php @@ -170,6 +170,13 @@ class MediaController extends ApiController if($data['security']['type'] === '') { $data['security']['data'] = ''; } + + if(strcasecmp($data['security']['type'], $medium->security_type) !== 0) { + if($request->has('storage') === false) { + $mime_type = $request->get('mime_type', $medium->mime_type); + $data['storage'] = Media::recommendedStorage($mime_type, $data['security']['type']); + } + } } if(array_key_exists('storage', $data) === true && @@ -288,16 +295,16 @@ class MediaController extends ApiController * @param \App\Models\Media $medium Specified media. * @return \Illuminate\Http\Response */ - public function download(Request $request, Media $medium) + public function download(Request $request, Media $media) { $headers = []; /* Check file exists */ - if(Storage::disk($medium->storage)->exists($medium->name) === true) { + if(Storage::disk($media->storage)->exists($media->name) === false) { return $this->respondNotFound(); } - $updated_at = Carbon::parse(Storage::disk($medium->storage)->lastModified($medium->name)); + $updated_at = Carbon::parse(Storage::disk($media->storage)->lastModified($media->name)); $headerPragma = 'no-cache'; $headerCacheControl = 'max-age=0, must-revalidate'; @@ -316,21 +323,21 @@ class MediaController extends ApiController } } - if ($medium->security_type === '') { + if ($media->security_type === '') { /* no security */ $headerPragma = 'public'; $headerExpires = $updated_at->addMonth()->toRfc2822String(); - } else if (strcasecmp('password', $medium->security_type) === 0) { + } else if (strcasecmp('password', $media->security_type) === 0) { /* password */ if( ($user === null || $user->hasPermission('admin/media') === false) && - ($request->has('password') === false || $request->get('password') !== $medium->security_data)) { + ($request->has('password') === false || $request->get('password') !== $media->security_data)) { return $this->respondForbidden(); } - } else if (strcasecmp('permission', $medium->security_type) === 0) { + } else if (strcasecmp('permission', $media->security_type) === 0) { /* permission */ if( - $user === null || ($user->hasPermission('admin/media') === false && $user->hasPermission($medium->security_data) === false)) { + $user === null || ($user->hasPermission('admin/media') === false && $user->hasPermission($media->security_data) === false)) { return $this->respondForbidden(); } }//end if @@ -341,7 +348,7 @@ class MediaController extends ApiController $headers = [ 'Cache-Control' => $headerCacheControl, - 'Content-Disposition' => sprintf('inline; filename="%s"', basename($medium->name)), + 'Content-Disposition' => sprintf('inline; filename="%s"', basename($media->name)), 'Etag' => $headerEtag, 'Expires' => $headerExpires, 'Last-Modified' => $headerLastModified, @@ -360,15 +367,16 @@ class MediaController extends ApiController return response()->make('', 304, $headers); } - $headers['Content-Type'] = Storage::disk($medium->storage)->mimeType($medium->name); - $headers['Content-Length'] = Storage::disk($medium->storage)->size($medium->name); - $headers['Content-Disposition'] = 'inline; filename="' . basename($medium->name) . '"'; + $headers['Content-Type'] = Storage::disk($media->storage)->mimeType($media->name); + $headers['Content-Length'] = Storage::disk($media->storage)->size($media->name); + $headers['Content-Disposition'] = 'attachment; filename="' . basename($media->name) . '"'; - $stream = Storage::disk($medium->storage)->readStream($medium->name); + $stream = Storage::disk($media->storage)->readStream($media->name); return response()->stream( - function () use ($stream) { - fclose($stream); - }, + function() use($stream) { + while(ob_get_level() > 0) ob_end_flush(); + fpassthru($stream); + }, 200, $headers ); diff --git a/app/Models/Media.php b/app/Models/Media.php index 94ee035..2a07e6c 100644 --- a/app/Models/Media.php +++ b/app/Models/Media.php @@ -327,7 +327,7 @@ class Media extends Model public function getUrlPath(): string { $url = config("filesystems.disks.$this->storage.url"); - return "$url/"; + return "$url"; } /** @@ -985,11 +985,28 @@ class Media extends Model return $this->hasMany(MediaJob::class, 'media_id'); } + public static function recommendedStorage(string $mime_type, string $security_type): string { + if($mime_type === '') { + return 'cdn'; + } + + if($security_type === '') { + if (strpos($mime_type, 'image/') === 0) { + return('local'); + } else { + return('cdn'); + } + } + + return('private'); + } + public static function verifyStorage($mime_type, $security_type, &$storage): int { if($mime_type === '') { return Media::STORAGE_MIME_MISSING; } + Log::info('verify: ' . $storage); if($storage === '') { if($security_type === '') { if (strpos($mime_type, 'image/') === 0) { diff --git a/resources/js/components/SMAttachments.vue b/resources/js/components/SMAttachments.vue index 1e1204f..6a91e5a 100644 --- a/resources/js/components/SMAttachments.vue +++ b/resources/js/components/SMAttachments.vue @@ -85,6 +85,7 @@ import { Media } from "../helpers/api.types"; import { onMounted, ref, watch } from "vue"; import { ImportMetaExtras } from "../../../import-meta"; import { strCaseCmp } from "../helpers/string"; +import { mediaGetWebURL } from "../helpers/media"; const emits = defineEmits(["update:modelValue"]); const props = defineProps({ @@ -157,24 +158,8 @@ const updateFileList = (newFileList: Array) => { fileList.value = []; for (const mediaItem of newFileList) { - const webUrl = (import.meta as ImportMetaExtras).env.APP_URL; - const apiUrl = (import.meta as ImportMetaExtras).env.APP_URL_API; - - // Is the URL a API request? - if (mediaItem.url.startsWith(apiUrl)) { - const fileUrlPath = mediaItem.url.substring(apiUrl.length); - const fileUrlParts = fileUrlPath.split("/"); - - if ( - fileUrlParts.length === 4 && - fileUrlParts[0].length === 0 && - strCaseCmp("media", fileUrlParts[1]) === true && - strCaseCmp("download", fileUrlParts[3]) === true - ) { - mediaItem.url = webUrl + "/file/" + fileUrlParts[2]; - fileList.value.push(mediaItem); - } - } else { + mediaItem.url = mediaGetWebURL(mediaItem); + if (mediaItem.url != "") { fileList.value.push(mediaItem); } } diff --git a/resources/js/components/dialogs/SMDialogChangePassword.vue b/resources/js/components/dialogs/SMDialogChangePassword.vue index d023866..cb260b0 100644 --- a/resources/js/components/dialogs/SMDialogChangePassword.vue +++ b/resources/js/components/dialogs/SMDialogChangePassword.vue @@ -1,6 +1,6 @@ @@ -31,11 +31,12 @@ import { useToastStore } from "../../store/ToastStore"; import { useUserStore } from "../../store/UserStore"; import SMForm from "../SMForm.vue"; import SMInput from "../SMInput.vue"; +import SMCard from "../SMCard.vue"; const form: FormObject = reactive( Form({ password: FormControl("", And([Required(), Password()])), - }) + }), ); const applicationStore = useApplicationStore(); diff --git a/resources/js/components/dialogs/SMDialogMedia.vue b/resources/js/components/dialogs/SMDialogMedia.vue index 56a3e12..679a610 100644 --- a/resources/js/components/dialogs/SMDialogMedia.vue +++ b/resources/js/components/dialogs/SMDialogMedia.vue @@ -145,7 +145,8 @@ )}')`, }">
+ v-if="item.security_type != ''" + class="absolute right--1 top--1 h-4 w-4"> diff --git a/resources/js/helpers/media.ts b/resources/js/helpers/media.ts index 6dd99b1..f96a920 100644 --- a/resources/js/helpers/media.ts +++ b/resources/js/helpers/media.ts @@ -1,5 +1,6 @@ +import { ImportMetaExtras } from "../../../import-meta"; import { Media, MediaJob } from "./api.types"; -import { toTitleCase } from "./string"; +import { strCaseCmp, toTitleCase } from "./string"; export const mediaGetVariantUrl = ( media: Media, @@ -25,6 +26,30 @@ export const mediaGetVariantUrl = ( : media.url; }; +export const mediaGetWebURL = (media: Media): string => { + const webUrl = (import.meta as ImportMetaExtras).env.APP_URL; + const apiUrl = (import.meta as ImportMetaExtras).env.APP_URL_API; + + let url = media.url; + + // Is the URL a API request? + if (media.url.startsWith(apiUrl)) { + const fileUrlPath = media.url.substring(apiUrl.length); + const fileUrlParts = fileUrlPath.split("/"); + + if ( + fileUrlParts.length >= 4 && + fileUrlParts[0].length === 0 && + strCaseCmp("media", fileUrlParts[1]) === true && + strCaseCmp("download", fileUrlParts[3]) === true + ) { + url = webUrl + "/file/" + fileUrlParts[2]; + } + } + + return url; +}; + /** * Check if a mime matches. * @param {string} mimeExpected The mime expected. diff --git a/resources/js/helpers/string.ts b/resources/js/helpers/string.ts index f6ae44a..282c9c4 100644 --- a/resources/js/helpers/string.ts +++ b/resources/js/helpers/string.ts @@ -121,5 +121,9 @@ export const toPrice = (numOrString: number | string): string => { * @returns {boolean} If the strings match. */ export const strCaseCmp = (string1: string, string2: string): boolean => { - return string1.toLowerCase() === string2.toLowerCase(); + if (string1 !== undefined && string2 !== undefined) { + return string1.toLowerCase() === string2.toLowerCase(); + } + + return false; }; diff --git a/resources/js/helpers/url.ts b/resources/js/helpers/url.ts index a1bf6a8..2941407 100644 --- a/resources/js/helpers/url.ts +++ b/resources/js/helpers/url.ts @@ -126,3 +126,22 @@ export const extractFileNameFromUrl = (url: string): string => { const fileName = matches[1]; return fileName; }; + +export const addQueryParam = ( + url: string, + name: string, + value: string, +): string => { + const urlObject = new URL(url); + const queryParams = new URLSearchParams(urlObject.search); + + if (queryParams.has(name)) { + queryParams.set(name, value); + } else { + // Add the new query parameter + queryParams.append(name, value); + } + + urlObject.search = queryParams.toString(); + return urlObject.toString(); +}; diff --git a/resources/js/views/File.vue b/resources/js/views/File.vue index 1c9d121..27b3b98 100644 --- a/resources/js/views/File.vue +++ b/resources/js/views/File.vue @@ -4,25 +4,49 @@ :status="pageStatus" /> - -

Password Required

-

This file requires a password before it can be viewed

- - - - +
+

Password Required

+

+ This file requires a password before it can be viewed +

+ +
+ +
+
+ + +
+

Download Complete

+

+ If you have permission to view this document, your download + should now begin. +

+
+ +
+
@@ -30,8 +54,11 @@ import { reactive, ref } from "vue"; import { api } from "../helpers/api"; import { useRoute } from "vue-router"; -import { Media } from "../helpers/api.types"; +import { Media, MediaResponse } from "../helpers/api.types"; +import SMForm from "../components/SMForm.vue"; +import SMInput from "../components/SMInput.vue"; import SMLoading from "../components/SMLoading.vue"; +import SMPageStatus from "../components/SMPageStatus.vue"; import { strCaseCmp } from "../helpers/string"; import { useUserStore } from "../store/UserStore"; import { Form, FormControl, FormObject } from "../helpers/form"; @@ -39,7 +66,7 @@ import { Required } from "../helpers/validate"; const pageStatus = ref(200); const pageLoading = ref(true); -const showPasswordForm = ref(false); +const showForm = ref(""); const fileUrl = ref(""); const userStore = useUserStore(); @@ -71,6 +98,9 @@ const downloadFile = (params = {}) => { } window.location.href = url; + window.setTimeout(() => { + showForm.value = "complete"; + }, 1500); }; /* @@ -84,6 +114,10 @@ const handleSubmit = () => { downloadFile(params); }; +const handleClose = () => { + window.close(); +}; + /** * Handle page loading */ @@ -96,34 +130,45 @@ const handleLoad = async () => { id: route.params.id, }; - let result = await api.get({ - url: "/media/:id", - params: params, - }); + try { + let result = await api.get({ + url: "/media/{id}", + params: params, + }); - if (result.status === 200) { - const medium = result.data as Media; - fileUrl.value = medium.url; + if (result.status === 200) { + const data = result.data as MediaResponse; + const medium = data.medium as Media; + fileUrl.value = medium.url; - if (medium.security_type === "") { - downloadFile(); - } else if ( - strCaseCmp("permission", medium.security_type) === true && - userStore.id - ) { - const params = { - token: userStore.token, - }; + if (medium.security_type === "") { + downloadFile(); + } else if ( + strCaseCmp("permission", medium.security_type) === true && + userStore.id + ) { + const params = { + token: userStore.token, + }; - downloadFile(params); - } else if (strCaseCmp("password", medium.security_type) === true) { - showPasswordForm.value = true; + downloadFile(params); + } else if ( + strCaseCmp("password", medium.security_type) === true + ) { + showForm.value = "password"; + } else { + /* unknown security type */ + pageStatus.value = 403; + } + + pageLoading.value = false; } else { - /* unknown security type */ - pageStatus.value = 403; + pageStatus.value = result.status; + pageLoading.value = false; } - } else { - pageStatus.value = result.status; + } catch (error) { + pageStatus.value = error.status; + pageLoading.value = false; } } }; diff --git a/resources/js/views/dashboard/MediaList.vue b/resources/js/views/dashboard/MediaList.vue index ccb8adb..036992c 100644 --- a/resources/js/views/dashboard/MediaList.vue +++ b/resources/js/views/dashboard/MediaList.vue @@ -74,12 +74,24 @@