Compare commits

...

2 Commits

Author SHA1 Message Date
e50c53e50a release 0.0.2 2025-10-16 01:41:52 +02:00
3e9056f4a6 fixed sanitizeFilename for backend\API,
added new API call attachmentDownload,
fixed attachment URL to call attachmentDownload,
refactoring Report.vue for <script setup>
2025-10-16 01:40:44 +02:00
5 changed files with 226 additions and 170 deletions

16
backend/composer.lock generated
View File

@ -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": [],

View File

@ -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;
}
}
} }

View File

@ -1,7 +1,7 @@
{ {
"name": "bugreport", "name": "bugreport",
"private": true, "private": true,
"version": "0.0.1", "version": "0.0.2",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@ -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});
}
}; };

View File

@ -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,14 +154,9 @@ 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() {
return {
loading: false,
report_id: this.$route.params.id,
report: {
report_id: 0, report_id: 0,
report_title: "Nacitavam report", report_title: "Nacitavam report",
report_description: "...", report_description: "...",
@ -168,137 +165,146 @@ export default {
report_priority: 1, report_priority: 1,
created_dt: "--", created_dt: "--",
ordnum: 0, ordnum: 0,
}, });
editable: true, const editable = ref(true);
attachments: [ const attachments = ref([
{ {
attachment_id: 0, attachment_id: 0,
attachment_type: "comment", attachment_type: "comment",
attachment_content: "Nacitavam report", attachment_content: "Nacitavam report",
created_dt: "--", created_dt: "--",
}, },
], ]);
attachmentNewFiles: null, const attachmentNewFiles = ref(null);
selectedFiles: [], let selectedFiles = [];
selectedFilesContent: [], let selectedFilesContent = [];
};
}, onMounted(() => {
mounted() { // console.log(report_id);
// console.log(this.report_id); loadReportData();
this.loadReportData(); });
},
methods: { function loadReportData() {
loadReportData() { backend.get(report_id).then((response) => {
backend.get(this.report_id).then((response) => { report.value = response.data;
this.report = response.data; // console.log(report);
// console.log(this.report); editable.value = response.data.report_status < 4;
this.editable = response.data.report_status < 4;
}); });
backend.attachmentGetAll(this.report_id).then((response) => { backend.attachmentGetAll(report_id).then((response) => {
this.attachments = response.data; attachments.value = response.data;
// console.log(this.attachments); // console.log(attachments);
}); });
}, }
onTitleChange(event) {
backend.update(this.report_id, { report_title: event.target.innerText }); function onTitleChange(event) {
}, backend.update(report_id, { report_title: event.target.innerText });
onDescriptionChange(event) { }
backend.update(this.report_id, {
function onDescriptionChange(event) {
backend.update(report_id, {
report_description: event.target.innerText, report_description: event.target.innerText,
}); });
}, }
reportDelete() {
function reportDelete() {
if (!confirm("Naozaj chcete report zmazať?")) return; if (!confirm("Naozaj chcete report zmazať?")) return;
this.loading = true; loading = true;
backend.delete(this.report_id).then(() => { backend.delete(report_id).then(() => {
this.$router.push("/"); router.push("/");
}); });
}, }
reportDone() {
backend.update(this.report_id, { function reportDone() {
backend.update(report_id, {
report_status: 4, report_status: 4,
}).then(() => { }).then(() => {
jungle.play(); jungle.play();
const confetti = new JSConfetti(); const confetti = new JSConfetti();
confetti.addConfetti(); confetti.addConfetti();
setTimeout(() => { setTimeout(() => {
this.$router.push("/"); router.push("/");
}, 3000); }, 3000);
}); });
}, }
attachmentAdd() {
let comment = this.$refs.attachmentNewContent.value; function attachmentAdd() {
let comment = $refs.attachmentNewContent.value;
if (comment.trim().length > 0) { if (comment.trim().length > 0) {
this.loading = true; loading = true;
backend backend
.attachmentAdd( .attachmentAdd(
this.report_id, report_id,
"comment", "comment",
this.$refs.attachmentNewContent.value attachmentNewContent.value
) )
.then(() => { .then(() => {
this.$refs.attachmentNewContent.value = ""; $refs.attachmentNewContent.value = "";
this.loadReportData(); loadReportData();
this.loading = false; loading = false;
}); });
} }
if (this.selectedFiles.length > 0) { if (selectedFiles.length > 0) {
let for_upload = this.selectedFiles.length; let for_upload = selectedFiles.length;
this.loading = true; loading = true;
for (let i = 0; i < this.selectedFiles.length; i++) { for (let i = 0; i < selectedFiles.length; i++) {
backend backend
.attachmentAdd(this.report_id, "file", { .attachmentAdd(report_id, "file", {
'filename': this.selectedFiles[i].name, 'filename': selectedFiles[i].name,
'base64': this.selectedFilesContent[i] 'base64': selectedFilesContent[i]
}) })
.then(() => { .then(() => {
for_upload--; for_upload--;
if (for_upload == 0) { if (for_upload == 0) {
this.selectedFiles = []; selectedFiles = [];
this.selectedFilesContent = []; selectedFilesContent = [];
this.$refs.attachmentNewFiles.value = null; attachmentNewFiles.value = null;
this.loadReportData(); loadReportData();
this.loading = false; loading = false;
} }
}); });
} }
} }
}, }
attachmentDelete(attachment) {
function attachmentDelete(attachment) {
if (!confirm("Naozaj chcete zmazať prilohu?")) return; if (!confirm("Naozaj chcete zmazať prilohu?")) return;
this.loading = true; loading = true;
backend.attachmentDelete(attachment.attachment_id).then(() => { backend.attachmentDelete(attachment.attachment_id).then(() => {
this.loadReportData(); loadReportData();
this.loading = false; loading = false;
}); });
}, }
updateAttachmentContent(event, attachment) {
this.loading = true; function updateAttachmentContent(event, attachment) {
loading = true;
backend backend
.attachmentUpdate(attachment.attachment_id, event.target.innerText) .attachmentUpdate(attachment.attachment_id, event.target.innerText)
.then(() => { .then(() => {
this.loadReportData(); loadReportData();
this.loading = false; loading = false;
}); });
}, }
handleFileUpload(event) {
function handleFileUpload(event) {
const files = event.target.files; const files = event.target.files;
if (files) { if (files) {
for (let i = 0; i < files.length; i++) { for (let i = 0; i < files.length; i++) {
this.selectedFiles.push(files[i]); selectedFiles.push(files[i]);
const reader = new FileReader(); const reader = new FileReader();
reader.onload = () => { reader.onload = () => {
this.selectedFilesContent[i] = reader.result; selectedFilesContent[i] = reader.result;
}; };
reader.readAsDataURL(files[i]); reader.readAsDataURL(files[i]);
} }
} }
}, }
removeFile(index) {
this.selectedFiles.splice(index, 1); function removeFile(index) {
this.selectedFilesContent.splice(index, 1); selectedFiles.splice(index, 1);
}, selectedFilesContent.splice(index, 1);
formatFileSize(size) { }
function formatFileSize(size) {
if (size < 1024) { if (size < 1024) {
return size + " B"; return size + " B";
} else if (size < 1024 * 1024) { } else if (size < 1024 * 1024) {
@ -306,10 +312,10 @@ export default {
} else { } else {
return (size / (1024 * 1024)).toFixed(2) + " MB"; return (size / (1024 * 1024)).toFixed(2) + " MB";
} }
}, }
isImageUrl(url) {
function isImageUrl(url) {
return /\.(jpg|jpeg|png|gif|svg|webp)$/.test(url); return /\.(jpg|jpeg|png|gif|svg|webp)$/.test(url);
} }
},
};
</script> </script>