media security_type updates
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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,14 +367,15 @@ 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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<Media>) => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<SMForm :model-value="form" @submit="handleSubmit">
|
||||
<SMFormCard :loading="dialogLoading">
|
||||
<SMCard :loading="dialogLoading">
|
||||
<template #header>
|
||||
<h3>Change Password</h3>
|
||||
<p>Enter your new password below</p>
|
||||
@@ -16,7 +16,7 @@
|
||||
<button type="button" @click="handleClickCancel">Cancel</button>
|
||||
<input role="button" type="submit" value="Update" />
|
||||
</template>
|
||||
</SMFormCard>
|
||||
</SMCard>
|
||||
</SMForm>
|
||||
</template>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -145,7 +145,8 @@
|
||||
)}')`,
|
||||
}">
|
||||
<div
|
||||
v-if="item.security_type != ''">
|
||||
v-if="item.security_type != ''"
|
||||
class="absolute right--1 top--1 h-4 w-4">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24">
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 => {
|
||||
if (string1 !== undefined && string2 !== undefined) {
|
||||
return string1.toLowerCase() === string2.toLowerCase();
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -4,25 +4,49 @@
|
||||
:status="pageStatus" />
|
||||
<SMLoading v-else-if="pageLoading == true"></SMLoading>
|
||||
<SMForm
|
||||
v-else-if="showPasswordForm == true"
|
||||
v-else-if="showForm == 'password'"
|
||||
:model-value="form"
|
||||
@submit="handleSubmit">
|
||||
<SMFormCard>
|
||||
<template #header>
|
||||
<h3>Password Required</h3>
|
||||
<p>This file requires a password before it can be viewed</p>
|
||||
</template>
|
||||
<template #body>
|
||||
<div
|
||||
class="max-w-2xl mx-auto border-1 bg-white rounded-xl mt-7xl text-gray-5 px-12 py-8">
|
||||
<h3 class="mb-4">Password Required</h3>
|
||||
<p class="mb-2">
|
||||
This file requires a password before it can be viewed
|
||||
</p>
|
||||
<SMInput
|
||||
class="mb-4"
|
||||
control="password"
|
||||
type="password"
|
||||
label="File Password"
|
||||
autofocus />
|
||||
</template>
|
||||
<template #footer-space-between>
|
||||
<input role="button" type="submit" value="OK" />
|
||||
</template>
|
||||
</SMFormCard>
|
||||
<div class="flex flex-justify-end">
|
||||
<input
|
||||
type="submit"
|
||||
class="font-medium px-6 py-3.1 rounded-2 hover:shadow-md text-lg transition bg-sky-600 hover:bg-sky-500 text-white cursor-pointer"
|
||||
value="Submit" />
|
||||
</div>
|
||||
</div>
|
||||
</SMForm>
|
||||
<SMForm
|
||||
v-else-if="showForm == 'complete'"
|
||||
:model-value="form"
|
||||
@submit="handleSubmit">
|
||||
<div
|
||||
class="max-w-2xl mx-auto border-1 bg-white rounded-xl mt-7xl text-gray-5 px-12 py-8">
|
||||
<h3 class="mb-4">Download Complete</h3>
|
||||
<p class="mb-2">
|
||||
If you have permission to view this document, your download
|
||||
should now begin.
|
||||
</p>
|
||||
<div class="flex flex-justify-end">
|
||||
<button
|
||||
role="button"
|
||||
class="font-medium px-6 py-3.1 rounded-2 hover:shadow-md text-lg transition bg-sky-600 hover:bg-sky-500 text-white cursor-pointer"
|
||||
@click="handleClose()">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</SMForm>
|
||||
</template>
|
||||
|
||||
@@ -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,13 +130,15 @@ const handleLoad = async () => {
|
||||
id: route.params.id,
|
||||
};
|
||||
|
||||
try {
|
||||
let result = await api.get({
|
||||
url: "/media/:id",
|
||||
url: "/media/{id}",
|
||||
params: params,
|
||||
});
|
||||
|
||||
if (result.status === 200) {
|
||||
const medium = result.data as Media;
|
||||
const data = result.data as MediaResponse;
|
||||
const medium = data.medium as Media;
|
||||
fileUrl.value = medium.url;
|
||||
|
||||
if (medium.security_type === "") {
|
||||
@@ -116,14 +152,23 @@ const handleLoad = async () => {
|
||||
};
|
||||
|
||||
downloadFile(params);
|
||||
} else if (strCaseCmp("password", medium.security_type) === true) {
|
||||
showPasswordForm.value = true;
|
||||
} else if (
|
||||
strCaseCmp("password", medium.security_type) === true
|
||||
) {
|
||||
showForm.value = "password";
|
||||
} else {
|
||||
/* unknown security type */
|
||||
pageStatus.value = 403;
|
||||
}
|
||||
|
||||
pageLoading.value = false;
|
||||
} else {
|
||||
pageStatus.value = result.status;
|
||||
pageLoading.value = false;
|
||||
}
|
||||
} catch (error) {
|
||||
pageStatus.value = error.status;
|
||||
pageLoading.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -74,12 +74,24 @@
|
||||
<template #item-title="item">
|
||||
<div class="flex gap-2">
|
||||
<div
|
||||
class="w-100 h-100 max-h-15 max-w-20 mr-2 bg-contain bg-no-repeat bg-center"
|
||||
class="w-100 h-100 max-h-15 max-w-20 mr-2 bg-contain bg-no-repeat bg-center relative"
|
||||
:style="{
|
||||
backgroundImage: `url('${mediaGetThumbnail(
|
||||
item,
|
||||
)}')`,
|
||||
}"></div>
|
||||
}">
|
||||
<div
|
||||
v-if="item.security_type != ''"
|
||||
class="absolute right--1 top--1 h-4 w-4">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24">
|
||||
<title>locked</title>
|
||||
<path
|
||||
d="M12,17A2,2 0 0,0 14,15C14,13.89 13.1,13 12,13A2,2 0 0,0 10,15A2,2 0 0,0 12,17M18,8A2,2 0 0,1 20,10V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V10C4,8.89 4.9,8 6,8H7V6A5,5 0 0,1 12,1A5,5 0 0,1 17,6V8H18M12,3A3,3 0 0,0 9,6V8H15V6A3,3 0 0,0 12,3Z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col flex-justify-center">
|
||||
<span>{{ item.title }}</span>
|
||||
<span class="small">({{ item.name }})</span>
|
||||
@@ -101,11 +113,11 @@
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
<a
|
||||
:href="mediaGetWebURL(item)"
|
||||
class="bg-transparent cursor-pointer hover:text-sky-5"
|
||||
title="Download"
|
||||
@click="handleDownload(item)">
|
||||
target="_blank">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
@@ -114,7 +126,7 @@
|
||||
d="M220-160q-24 0-42-18t-18-42v-143h60v143h520v-143h60v143q0 24-18 42t-42 18H220Zm260-153L287-506l43-43 120 120v-371h60v371l120-120 43 43-193 193Z"
|
||||
fill="currrentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
class="bg-transparent cursor-pointer hover:text-red-7"
|
||||
@@ -170,11 +182,11 @@ import SMMastHead from "../../components/SMMastHead.vue";
|
||||
import SMTable from "../../components/SMTable.vue";
|
||||
import SMPagination from "../../components/SMPagination.vue";
|
||||
import SMLoading from "../../components/SMLoading.vue";
|
||||
import { updateRouterParams } from "../../helpers/url";
|
||||
import { addQueryParam, updateRouterParams } from "../../helpers/url";
|
||||
import { userHasPermission } from "../../helpers/utils";
|
||||
import SMPageStatus from "../../components/SMPageStatus.vue";
|
||||
import SMCheckbox from "../../components/SMCheckbox.vue";
|
||||
import { mediaGetThumbnail } from "../../helpers/media";
|
||||
import { mediaGetThumbnail, mediaGetWebURL } from "../../helpers/media";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -448,7 +460,9 @@ const handleEditSelected = async () => {
|
||||
* @param {Media} item The media item.
|
||||
*/
|
||||
const handleDownload = (item: Media) => {
|
||||
window.open(`${item.url}?download=1`, "_blank");
|
||||
// window.open(`${item.url}?download=1`, "_blank");
|
||||
// window.open(addQueryParam(mediaGetWebURL(item), "download", "1"), "_blank");
|
||||
window.open(mediaGetWebURL(item), "_blank");
|
||||
};
|
||||
|
||||
const computedSelectedCount = computed(() => {
|
||||
|
||||
@@ -44,7 +44,7 @@ Route::get('/users/{user}/events', [UserController::class, 'eventList']);
|
||||
Route::get('media/jobs', [MediaJobController::class, 'index']);
|
||||
Route::get('media/jobs/{mediaJob}', [MediaJobController::class, 'show']);
|
||||
Route::apiResource('media', MediaController::class);
|
||||
Route::get('media/{medium}/download', [MediaController::class, 'download']);
|
||||
Route::get('media/{media}/download', [MediaController::class, 'download']);
|
||||
|
||||
Route::apiResource('articles', ArticleController::class);
|
||||
// Route::apiAddendumResource('attachments', 'articles', ArticleController::class);
|
||||
|
||||
Reference in New Issue
Block a user