fixed sanitizeFilename for backend\API,
added new API call attachmentDownload, fixed attachment URL to call attachmentDownload, refactoring Report.vue for <script setup>
This commit is contained in:
16
backend/composer.lock
generated
16
backend/composer.lock
generated
@ -8,11 +8,11 @@
|
|||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "tpsoft/apilite",
|
"name": "tpsoft/apilite",
|
||||||
"version": "v1.0.2",
|
"version": "v1.0.3",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://gitea.tpsoft.org/TPsoft.org/APIlite.git",
|
"url": "https://gitea.tpsoft.org/TPsoft.org/APIlite.git",
|
||||||
"reference": "2d3f8bfdd46d0d304bac44057ff8444bfb2a4a17"
|
"reference": "c0fd7b3fe5270ee44a84a92e9255ada2438812b7"
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=8.2"
|
"php": ">=8.2"
|
||||||
@ -49,15 +49,15 @@
|
|||||||
"type": "other"
|
"type": "other"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-06-12T05:54:37+00:00"
|
"time": "2025-10-13T21:36:34+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "tpsoft/dbmodel",
|
"name": "tpsoft/dbmodel",
|
||||||
"version": "v1.0.4",
|
"version": "v1.0.5",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://gitea.tpsoft.org/TPsoft.org/DBmodel.git",
|
"url": "https://gitea.tpsoft.org/TPsoft.org/DBmodel.git",
|
||||||
"reference": "ae59ffaa97094854bcd1d863c6648a5b4dada671"
|
"reference": "80e889946bc4e38e987f46a13f95ee177ea934dc"
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"ext-pdo": "*",
|
"ext-pdo": "*",
|
||||||
@ -85,7 +85,9 @@
|
|||||||
"keywords": [
|
"keywords": [
|
||||||
"db",
|
"db",
|
||||||
"model",
|
"model",
|
||||||
"pdo"
|
"mysql",
|
||||||
|
"pdo",
|
||||||
|
"sqlite"
|
||||||
],
|
],
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@ -93,7 +95,7 @@
|
|||||||
"type": "other"
|
"type": "other"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-06-15T17:07:42+00:00"
|
"time": "2025-10-13T21:26:19+00:00"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"packages-dev": [],
|
"packages-dev": [],
|
||||||
|
|||||||
@ -187,7 +187,7 @@ class API extends APIlite
|
|||||||
if (!is_array($data)) return false;
|
if (!is_array($data)) return false;
|
||||||
$base64 = preg_replace('/^data:.*?;base64,/', '', $data['base64']);
|
$base64 = preg_replace('/^data:.*?;base64,/', '', $data['base64']);
|
||||||
$base64_data = base64_decode($base64);
|
$base64_data = base64_decode($base64);
|
||||||
$filename = 'report_' . $report_id . '_' . time() . '_' . sanitizeFilename($data['filename']);
|
$filename = 'report_' . $report_id . '_' . time() . '_' . $this->sanitizeFilename($data['filename']);
|
||||||
file_put_contents(UPLOAD_DIR_ATTACHMENTS . $filename, $base64_data);
|
file_put_contents(UPLOAD_DIR_ATTACHMENTS . $filename, $base64_data);
|
||||||
$attachment_content = $filename;
|
$attachment_content = $filename;
|
||||||
}
|
}
|
||||||
@ -201,6 +201,26 @@ class API extends APIlite
|
|||||||
return $suc !== false;
|
return $suc !== false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function sanitizeFilename($filename, $allowedExtensions = [])
|
||||||
|
{
|
||||||
|
// Rozdelenie názvu a prípony
|
||||||
|
$pathInfo = pathinfo($filename);
|
||||||
|
$name = $pathInfo['filename'] ?? 'file';
|
||||||
|
$extension = strtolower($pathInfo['extension'] ?? '');
|
||||||
|
// Odstránenie nebezpečných znakov z názvu
|
||||||
|
$name = preg_replace('/[^a-zA-Z0-9_-]/', '_', $name);
|
||||||
|
$name = substr($name, 0, 100); // voliteľné obmedzenie dĺžky
|
||||||
|
// Validácia prípony, ak je zoznam povolený
|
||||||
|
if (
|
||||||
|
$allowedExtensions
|
||||||
|
&& count($allowedExtensions) > 0
|
||||||
|
&& !in_array($extension, $allowedExtensions)
|
||||||
|
) {
|
||||||
|
$extension = 'bin'; // fallback ak prípona nie je povolená
|
||||||
|
}
|
||||||
|
return $name . '.' . $extension;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update report attachment
|
* Update report attachment
|
||||||
*
|
*
|
||||||
@ -236,7 +256,7 @@ class API extends APIlite
|
|||||||
->toArray();
|
->toArray();
|
||||||
if (is_array($all)) foreach ($all as $key => $row) {
|
if (is_array($all)) foreach ($all as $key => $row) {
|
||||||
if ($all[$key]['attachment_type'] == 'file') {
|
if ($all[$key]['attachment_type'] == 'file') {
|
||||||
$all[$key]['attachment_content'] = UPLOAD_URL_ATTACHMENTS . $all[$key]['attachment_content'];
|
$all[$key]['attachment_content'] = '?action=attachmentDownload&filename=' . $all[$key]['attachment_content'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $all;
|
return $all;
|
||||||
@ -254,4 +274,28 @@ class API extends APIlite
|
|||||||
$suc = $attachments->attachment($attachment_id, null);
|
$suc = $attachments->attachment($attachment_id, null);
|
||||||
return $suc !== false;
|
return $suc !== false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download report attachment
|
||||||
|
*
|
||||||
|
* @param string $filename
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function attachmentDownload(string $filename): void {
|
||||||
|
$filename = $this->sanitizeFilename($filename);
|
||||||
|
$filename = UPLOAD_DIR_ATTACHMENTS . $filename;
|
||||||
|
if (file_exists($filename)) {
|
||||||
|
header('Content-Description: File Transfer');
|
||||||
|
header('Content-Type: application/octet-stream');
|
||||||
|
header('Content-Disposition: attachment; filename="' . basename($filename) . '"');
|
||||||
|
header('Expires: 0');
|
||||||
|
header('Cache-Control: must-revalidate');
|
||||||
|
header('Pragma: public');
|
||||||
|
header('Content-Length: ' . filesize($filename));
|
||||||
|
readfile($filename);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,10 +2,10 @@
|
|||||||
* Generated by APIlite
|
* Generated by APIlite
|
||||||
* https://gitea.tpsoft.org/TPsoft.org/APIlite
|
* https://gitea.tpsoft.org/TPsoft.org/APIlite
|
||||||
*
|
*
|
||||||
* 2025-10-01 00:23:37 */
|
* 2025-10-16 01:38:51 */
|
||||||
|
|
||||||
class backend {
|
class backend {
|
||||||
endpont = import.meta.env.VITE_BACKENDAPI_URL;
|
endpoint = import.meta.env.VITE_BACKENDAPI_URL;
|
||||||
|
|
||||||
/* ----------------------------------------------------
|
/* ----------------------------------------------------
|
||||||
* General API call
|
* General API call
|
||||||
@ -28,7 +28,7 @@ class backend {
|
|||||||
if (typeof val == 'object') val = JSON.stringify(val);
|
if (typeof val == 'object') val = JSON.stringify(val);
|
||||||
form_data.append(key, val);
|
form_data.append(key, val);
|
||||||
});
|
});
|
||||||
xhttp.open('POST', this.endpont + '?action=' + method);
|
xhttp.open('POST', this.endpoint + '?action=' + method);
|
||||||
xhttp.send(form_data);
|
xhttp.send(form_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,6 +107,10 @@ class backend {
|
|||||||
return this.callPromise('attachmentDelete', {attachment_id: attachment_id});
|
return this.callPromise('attachmentDelete', {attachment_id: attachment_id});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
attachmentDownload(filename) {
|
||||||
|
return this.callPromise('attachmentDownload', {filename: filename});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -89,9 +89,9 @@
|
|||||||
v-else-if="attachment.attachment_type == 'file'"
|
v-else-if="attachment.attachment_type == 'file'"
|
||||||
class="attachment-file"
|
class="attachment-file"
|
||||||
>
|
>
|
||||||
<a :href="attachment.attachment_content" target="_blank">Stiahnut {{ attachment.attachment_content.split('/').pop().split('?')[0].split('#')[0] }}</a>
|
<a :href="backend.endpoint + attachment.attachment_content" target="_blank">Stiahnut {{ attachment.attachment_content.split('/').pop().split('?')[0].split('#')[0] }}</a>
|
||||||
<br>
|
<br>
|
||||||
<img :src="attachment.attachment_content" v-if="isImageUrl(attachment.attachment_content)" />
|
<img :src="backend.endpoint + attachment.attachment_content" v-if="isImageUrl(attachment.attachment_content)" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="attachment-content">
|
<div v-else class="attachment-content">
|
||||||
Neznamy typ prilohy: <strong>{{ attachment.attachment_type }}</strong>
|
Neznamy typ prilohy: <strong>{{ attachment.attachment_type }}</strong>
|
||||||
@ -144,7 +144,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { onMounted, ref } from "vue";
|
||||||
|
import { router } from "../router";
|
||||||
import backend from "../backend";
|
import backend from "../backend";
|
||||||
import FullScreenLoader from "../components/FullScreenLoader.vue";
|
import FullScreenLoader from "../components/FullScreenLoader.vue";
|
||||||
import JSConfetti from 'js-confetti'
|
import JSConfetti from 'js-confetti'
|
||||||
@ -152,164 +154,168 @@ import JSConfetti from 'js-confetti'
|
|||||||
let tadas = ['/sounds/tada.mp3', '/sounds/tada2.mp3', '/sounds/crazy-phrog-short.mp3'];
|
let tadas = ['/sounds/tada.mp3', '/sounds/tada2.mp3', '/sounds/crazy-phrog-short.mp3'];
|
||||||
const jungle = new Audio(tadas[Math.floor(Math.random() * tadas.length)]);
|
const jungle = new Audio(tadas[Math.floor(Math.random() * tadas.length)]);
|
||||||
|
|
||||||
export default {
|
const loading = ref(false);
|
||||||
name: "Report",
|
const report_id = router.currentRoute.value.params.id;
|
||||||
components: { FullScreenLoader },
|
const report = ref({
|
||||||
data() {
|
report_id: 0,
|
||||||
return {
|
report_title: "Nacitavam report",
|
||||||
loading: false,
|
report_description: "...",
|
||||||
report_id: this.$route.params.id,
|
report_status: 4,
|
||||||
report: {
|
report_group: "--",
|
||||||
report_id: 0,
|
report_priority: 1,
|
||||||
report_title: "Nacitavam report",
|
created_dt: "--",
|
||||||
report_description: "...",
|
ordnum: 0,
|
||||||
report_status: 4,
|
});
|
||||||
report_group: "--",
|
const editable = ref(true);
|
||||||
report_priority: 1,
|
const attachments = ref([
|
||||||
created_dt: "--",
|
{
|
||||||
ordnum: 0,
|
attachment_id: 0,
|
||||||
},
|
attachment_type: "comment",
|
||||||
editable: true,
|
attachment_content: "Nacitavam report",
|
||||||
attachments: [
|
created_dt: "--",
|
||||||
{
|
|
||||||
attachment_id: 0,
|
|
||||||
attachment_type: "comment",
|
|
||||||
attachment_content: "Nacitavam report",
|
|
||||||
created_dt: "--",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
attachmentNewFiles: null,
|
|
||||||
selectedFiles: [],
|
|
||||||
selectedFilesContent: [],
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
mounted() {
|
]);
|
||||||
// console.log(this.report_id);
|
const attachmentNewFiles = ref(null);
|
||||||
this.loadReportData();
|
let selectedFiles = [];
|
||||||
},
|
let selectedFilesContent = [];
|
||||||
methods: {
|
|
||||||
loadReportData() {
|
onMounted(() => {
|
||||||
backend.get(this.report_id).then((response) => {
|
// console.log(report_id);
|
||||||
this.report = response.data;
|
loadReportData();
|
||||||
// console.log(this.report);
|
});
|
||||||
this.editable = response.data.report_status < 4;
|
|
||||||
|
function loadReportData() {
|
||||||
|
backend.get(report_id).then((response) => {
|
||||||
|
report.value = response.data;
|
||||||
|
// console.log(report);
|
||||||
|
editable.value = response.data.report_status < 4;
|
||||||
|
});
|
||||||
|
backend.attachmentGetAll(report_id).then((response) => {
|
||||||
|
attachments.value = response.data;
|
||||||
|
// console.log(attachments);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTitleChange(event) {
|
||||||
|
backend.update(report_id, { report_title: event.target.innerText });
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDescriptionChange(event) {
|
||||||
|
backend.update(report_id, {
|
||||||
|
report_description: event.target.innerText,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function reportDelete() {
|
||||||
|
if (!confirm("Naozaj chcete report zmazať?")) return;
|
||||||
|
loading = true;
|
||||||
|
backend.delete(report_id).then(() => {
|
||||||
|
router.push("/");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function reportDone() {
|
||||||
|
backend.update(report_id, {
|
||||||
|
report_status: 4,
|
||||||
|
}).then(() => {
|
||||||
|
jungle.play();
|
||||||
|
const confetti = new JSConfetti();
|
||||||
|
confetti.addConfetti();
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push("/");
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachmentAdd() {
|
||||||
|
let comment = $refs.attachmentNewContent.value;
|
||||||
|
if (comment.trim().length > 0) {
|
||||||
|
loading = true;
|
||||||
|
backend
|
||||||
|
.attachmentAdd(
|
||||||
|
report_id,
|
||||||
|
"comment",
|
||||||
|
attachmentNewContent.value
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
$refs.attachmentNewContent.value = "";
|
||||||
|
loadReportData();
|
||||||
|
loading = false;
|
||||||
});
|
});
|
||||||
backend.attachmentGetAll(this.report_id).then((response) => {
|
}
|
||||||
this.attachments = response.data;
|
if (selectedFiles.length > 0) {
|
||||||
// console.log(this.attachments);
|
let for_upload = selectedFiles.length;
|
||||||
});
|
loading = true;
|
||||||
},
|
for (let i = 0; i < selectedFiles.length; i++) {
|
||||||
onTitleChange(event) {
|
|
||||||
backend.update(this.report_id, { report_title: event.target.innerText });
|
|
||||||
},
|
|
||||||
onDescriptionChange(event) {
|
|
||||||
backend.update(this.report_id, {
|
|
||||||
report_description: event.target.innerText,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
reportDelete() {
|
|
||||||
if (!confirm("Naozaj chcete report zmazať?")) return;
|
|
||||||
this.loading = true;
|
|
||||||
backend.delete(this.report_id).then(() => {
|
|
||||||
this.$router.push("/");
|
|
||||||
});
|
|
||||||
},
|
|
||||||
reportDone() {
|
|
||||||
backend.update(this.report_id, {
|
|
||||||
report_status: 4,
|
|
||||||
}).then(() => {
|
|
||||||
jungle.play();
|
|
||||||
const confetti = new JSConfetti();
|
|
||||||
confetti.addConfetti();
|
|
||||||
setTimeout(() => {
|
|
||||||
this.$router.push("/");
|
|
||||||
}, 3000);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
attachmentAdd() {
|
|
||||||
let comment = this.$refs.attachmentNewContent.value;
|
|
||||||
if (comment.trim().length > 0) {
|
|
||||||
this.loading = true;
|
|
||||||
backend
|
|
||||||
.attachmentAdd(
|
|
||||||
this.report_id,
|
|
||||||
"comment",
|
|
||||||
this.$refs.attachmentNewContent.value
|
|
||||||
)
|
|
||||||
.then(() => {
|
|
||||||
this.$refs.attachmentNewContent.value = "";
|
|
||||||
this.loadReportData();
|
|
||||||
this.loading = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (this.selectedFiles.length > 0) {
|
|
||||||
let for_upload = this.selectedFiles.length;
|
|
||||||
this.loading = true;
|
|
||||||
for (let i = 0; i < this.selectedFiles.length; i++) {
|
|
||||||
backend
|
|
||||||
.attachmentAdd(this.report_id, "file", {
|
|
||||||
'filename': this.selectedFiles[i].name,
|
|
||||||
'base64': this.selectedFilesContent[i]
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
for_upload--;
|
|
||||||
if (for_upload == 0) {
|
|
||||||
this.selectedFiles = [];
|
|
||||||
this.selectedFilesContent = [];
|
|
||||||
this.$refs.attachmentNewFiles.value = null;
|
|
||||||
this.loadReportData();
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
attachmentDelete(attachment) {
|
|
||||||
if (!confirm("Naozaj chcete zmazať prilohu?")) return;
|
|
||||||
this.loading = true;
|
|
||||||
backend.attachmentDelete(attachment.attachment_id).then(() => {
|
|
||||||
this.loadReportData();
|
|
||||||
this.loading = false;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
updateAttachmentContent(event, attachment) {
|
|
||||||
this.loading = true;
|
|
||||||
backend
|
backend
|
||||||
.attachmentUpdate(attachment.attachment_id, event.target.innerText)
|
.attachmentAdd(report_id, "file", {
|
||||||
|
'filename': selectedFiles[i].name,
|
||||||
|
'base64': selectedFilesContent[i]
|
||||||
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.loadReportData();
|
for_upload--;
|
||||||
this.loading = false;
|
if (for_upload == 0) {
|
||||||
|
selectedFiles = [];
|
||||||
|
selectedFilesContent = [];
|
||||||
|
attachmentNewFiles.value = null;
|
||||||
|
loadReportData();
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
|
||||||
handleFileUpload(event) {
|
|
||||||
const files = event.target.files;
|
|
||||||
if (files) {
|
|
||||||
for (let i = 0; i < files.length; i++) {
|
|
||||||
this.selectedFiles.push(files[i]);
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = () => {
|
|
||||||
this.selectedFilesContent[i] = reader.result;
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(files[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
removeFile(index) {
|
|
||||||
this.selectedFiles.splice(index, 1);
|
|
||||||
this.selectedFilesContent.splice(index, 1);
|
|
||||||
},
|
|
||||||
formatFileSize(size) {
|
|
||||||
if (size < 1024) {
|
|
||||||
return size + " B";
|
|
||||||
} else if (size < 1024 * 1024) {
|
|
||||||
return (size / 1024).toFixed(2) + " KB";
|
|
||||||
} else {
|
|
||||||
return (size / (1024 * 1024)).toFixed(2) + " MB";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isImageUrl(url) {
|
|
||||||
return /\.(jpg|jpeg|png|gif|svg|webp)$/.test(url);
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
function attachmentDelete(attachment) {
|
||||||
|
if (!confirm("Naozaj chcete zmazať prilohu?")) return;
|
||||||
|
loading = true;
|
||||||
|
backend.attachmentDelete(attachment.attachment_id).then(() => {
|
||||||
|
loadReportData();
|
||||||
|
loading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateAttachmentContent(event, attachment) {
|
||||||
|
loading = true;
|
||||||
|
backend
|
||||||
|
.attachmentUpdate(attachment.attachment_id, event.target.innerText)
|
||||||
|
.then(() => {
|
||||||
|
loadReportData();
|
||||||
|
loading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFileUpload(event) {
|
||||||
|
const files = event.target.files;
|
||||||
|
if (files) {
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
selectedFiles.push(files[i]);
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
selectedFilesContent[i] = reader.result;
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(files[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeFile(index) {
|
||||||
|
selectedFiles.splice(index, 1);
|
||||||
|
selectedFilesContent.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatFileSize(size) {
|
||||||
|
if (size < 1024) {
|
||||||
|
return size + " B";
|
||||||
|
} else if (size < 1024 * 1024) {
|
||||||
|
return (size / 1024).toFixed(2) + " KB";
|
||||||
|
} else {
|
||||||
|
return (size / (1024 * 1024)).toFixed(2) + " MB";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isImageUrl(url) {
|
||||||
|
return /\.(jpg|jpeg|png|gif|svg|webp)$/.test(url);
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user