From a3656604c00ff26deb908da84a7030c2a9b4d434 Mon Sep 17 00:00:00 2001 From: Erik Thuning <boooink@gmail.com> Date: Tue, 19 Nov 2019 15:06:22 +0100 Subject: [PATCH] Implemented attachments for products --- config.php.example | 7 +++ html/fragments.html | 30 +++++++---- include/Ajax.php | 33 ++++++++++++ include/Attachment.php | 109 ++++++++++++++++++++++++++++++++++++++++ include/Download.php | 25 +++++++++ include/Product.php | 16 ++++-- include/ProductPage.php | 9 ++-- include/Responder.php | 30 +++++++---- include/functions.php | 2 + script.js | 100 +++++++++++++++++++++++++++--------- style.css | 4 ++ 11 files changed, 312 insertions(+), 53 deletions(-) create mode 100644 include/Attachment.php create mode 100644 include/Download.php diff --git a/config.php.example b/config.php.example index e972ecf..982e551 100644 --- a/config.php.example +++ b/config.php.example @@ -18,4 +18,11 @@ $error_address = 'root@example.com'; #$notify_discard = 'inventory-tracking@example.com'; $discard_notify = false; +# Directory to save attachments to +# Interpreted as absolute path if beginning with /, +# otherwise assumed to be relative to the application root. +# Must be writable by the web server. +# Does not need to be within the web root. +$files_dir = 'attachments'; + ?> diff --git a/html/fragments.html b/html/fragments.html index 5da87eb..e167cce 100644 --- a/html/fragments.html +++ b/html/fragments.html @@ -280,12 +280,6 @@ </button> </form> </div> -<div id="product-history" - class="¤hidden¤"> - <h2>Artikelhistorik</h2> - ¤history¤ -</div> -<!-- <div id="product-attachments" class="¤hidden¤"> <h2>Bilagor</h2> @@ -298,17 +292,21 @@ <input id="uploadfile" name="uploadfile" type="file" - onchange="show_file(event)"/> + onchange="showFile(event)"/> <input id="filename" name="filename" type="text" placeholder="Välj en fil..." - onclick="select_file(event)" + onclick="selectFile(event)" readonly /> <button>Ladda upp</button> </form> </div> ---> +<div id="product-history" + class="¤hidden¤"> + <h2>Artikelhistorik</h2> + ¤history¤ +</div> <div id="product-label" class="¤hidden¤"> <h2>Etikett</h2> @@ -316,13 +314,23 @@ </div> ¤¤ attachment_list ¤¤ -<ul> +<ul class="attachment-list"> ¤attachments¤ </ul> ¤¤ attachment ¤¤ <li> - <strong>¤name¤</strong>: <a href="./?page=dl&id=¤id¤">Ladda ner</a> + <strong>¤name¤</strong> (¤date¤): <a href="./?page=dl&id=¤id¤">Ladda ner</a> + <br/> + <form onSubmit="JavaScript:deleteAttachment(event)"> + <input type="hidden" + name="id" + value="¤id¤" /> + <input type="hidden" + name="name" + value="¤name¤" /> + <button>Ta bort</button> + </form> </li> ¤¤ product_label ¤¤ diff --git a/include/Ajax.php b/include/Ajax.php index 841fa35..8ddadd3 100644 --- a/include/Ajax.php +++ b/include/Ajax.php @@ -59,6 +59,13 @@ class Ajax extends Responder { break; case 'toggleservice': $out = $this->toggle_service(); + break; + case 'addattachment': + $out = $this->add_attachment(); + break; + case 'deleteattachment': + $out = $this->delete_attachment(); + break; } print($out->toJson()); } @@ -381,5 +388,31 @@ class Ajax extends Responder { .'på den här artikeln nu.'); } } + + private function add_attachment() { + try { + $product = new Product($_POST['id']); + $uploadfile = $_FILES['uploadfile']; + $attach = Attachment::create($uploadfile, $product->get_id()); + $date = format_date($attach->get_uploadtime()); + $fragment = replace(array('name' => $attach->get_filename(), + 'id' => $attach->get_id(), + 'date' => $date), + $this->fragments['attachment']); + return new Success($fragment); + } catch(Exception $e) { + return new Failure($e->getMessage()); + } + } + + private function delete_attachment() { + $attach = new Attachment($_POST['id']); + try { + $attach->delete(); + return new Success(''); + } catch(Exception $e) { + return new Failure($e->getMessage()); + } + } } ?> diff --git a/include/Attachment.php b/include/Attachment.php new file mode 100644 index 0000000..0450bcc --- /dev/null +++ b/include/Attachment.php @@ -0,0 +1,109 @@ +<?php +class Attachment { + private $id; + private $product; + private $filename; + private $uploadtime; + + public static function create($file, $prodid) { + begin_trans(); + try { + if(!isset($file['error']) || is_array($file['error'])) { + throw new Exception('Ogiltigt anrop.'); + } + switch($file['error']) { + case UPLOAD_ERR_OK: + break; + case UPLOAD_ERR_NO_FILE: + throw new Exception('Ingen fil skickades.'); + case UPLOAD_ERR_INI_SIZE: + case UPLOAD_ERR_FORM_SIZE: + throw new Exception('Filen är för stor.'); + default: + throw new Exception('Ett okänt fel har inträffat.'); + } + $filename = $file['name']; + $insert = prepare('insert into `attachment` + (`product`, `filename`, `uploadtime`) + values (?, ?, ?)'); + bind($insert, 'isi', $prodid, $filename, time()); + execute($insert); + $attachid = $insert->insert_id; + global $files_dir; + $savepath = $files_dir; + if(substr($savepath, -1) !== '/') { + $savepath .= '/'; + } + $savepath .= $attachid; + $tmp_name = $file['tmp_name']; + if(file_exists($savepath)) { + throw new Exception('Filens plats är upptagen. ' + .'Det här borde aldrig inträffa.'); + } + if(!move_uploaded_file($tmp_name, $savepath)) { + throw new Exception('Filen kunde inte sparas.'); + } + commit_trans(); + return new Attachment($attachid); + } catch(Exception $e) { + revert_trans(); + throw $e; + } + } + + public function __construct($id) { + $search = prepare('select * from `attachment` where `id`=?'); + bind($search, 'i', $id); + execute($search); + $result = result_single($search); + if($result === null) { + throw new Exception('Attachment does not exist.'); + } + $this->id = $result['id']; + $this->product = $result['product']; + $this->filename = $result['filename']; + $this->uploadtime = $result['uploadtime']; + } + + public function delete() { + $delete = prepare('update `attachment` set `deletetime`=? + where `id`=?'); + bind($delete, 'ii', time(), $this->get_id()); + execute($delete); + $path = $this->get_filepath(); + if(file_exists($path)) { + unlink($path); + } + return true; + } + + public function get_id() { + return $this->id; + } + + public function get_product() { + return new Product($this->product); + } + + public function get_filename() { + return $this->filename; + } + + public function get_uploadtime() { + return $this->uploadtime; + } + + public function get_filepath() { + global $files_dir; + $path = $files_dir; + if(substr($path, -1) !== '/') { + $path .= '/'; + } + $path .= $this->get_id(); + if(!file_exists($path)) { + throw new Exception('Filen har försvunnit.'); + } + return $path; + } +} +?> diff --git a/include/Download.php b/include/Download.php new file mode 100644 index 0000000..3f99ee6 --- /dev/null +++ b/include/Download.php @@ -0,0 +1,25 @@ +<?php +class Download extends Responder { + private $attachment; + + public function __construct() { + parent::__construct(); + if(isset($_GET['id'])) { + $this->attachment = new Attachment($_GET['id']); + } + } + + public function render() { + $filename = $this->attachment->get_filename(); + $filepath = $this->attachment->get_filepath(); + header('Content-Description: File Transfer'); + header('Content-Type: application/octet-stream'); + header('Content-Disposition: attachment; filename="'.$filename.'"'); + header('Expires: 0'); + header('Cache-Control: no-cache'); + header('Content-length: '.filesize($filepath)); + readfile($filepath); + exit(0); + } +} +?> diff --git a/include/Product.php b/include/Product.php index 8725079..7c0a0e6 100644 --- a/include/Product.php +++ b/include/Product.php @@ -21,8 +21,8 @@ class Product { $now = time(); begin_trans(); try { - $stmt = 'insert into - `product`(`brand`, `name`, `invoice`, `serial`, `createtime`) + $stmt = 'insert into `product` + (`brand`, `name`, `invoice`, `serial`, `createtime`) values (?, ?, ?, ?, ?)'; $ins_prod = prepare($stmt); bind($ins_prod, 'ssssi', $brand, $name, $invoice, $serial, $now); @@ -403,7 +403,17 @@ class Product { } public function get_attachments() { - return array(); + $out = array(); + $find = prepare('select `id` from `attachment` + where `product`=? and `deletetime` is NULL + order by `uploadtime` asc'); + bind($find, 'i', $this->id); + execute($find); + $items = result_list($find); + foreach($items as $item) { + $out[] = new Attachment($item['id']); + } + return $out; } } ?> diff --git a/include/ProductPage.php b/include/ProductPage.php index c5fe86f..78237a1 100644 --- a/include/ProductPage.php +++ b/include/ProductPage.php @@ -89,7 +89,6 @@ class ProductPage extends Page { 'service' => 'Starta service', 'history' => $history, 'attachments' => $attachments); - $attachments = $this->product->get_attachments(); if(class_exists('QRcode')) { $fields['label'] = replace($fields, $this->fragments['product_label']); @@ -144,12 +143,14 @@ class ProductPage extends Page { private function build_attachment_list($attachments) { if(!$attachments) { - return 'Inga bilagor.'; + return '<p>Inga bilagor.</p>'; } $items = ''; foreach($attachments as $attachment) { - $items .= replace(array('name' => $attachment->get_name(), - 'id' => $attachments->get_id()), + $date = format_date($attachment->get_uploadtime()); + $items .= replace(array('name' => $attachment->get_filename(), + 'id' => $attachment->get_id(), + 'date' => $date), $this->fragments['attachment']); } return replace(array('attachments' => $items), diff --git a/include/Responder.php b/include/Responder.php index 80a4953..6d05f51 100644 --- a/include/Responder.php +++ b/include/Responder.php @@ -5,27 +5,35 @@ abstract class Responder { public function __construct() { $this->fragments = get_fragments('./html/fragments.html'); } - + final protected function escape_tags($tags) { foreach($tags as $key => $tag) { - $tags[$key] = str_replace(array("'", - '"'), - array(''', - '"'), - strtolower($tag)); + $tags[$key] = $this->escape_string(strtolower($tag)); } return $tags; } final protected function unescape_tags($tags) { foreach($tags as $key => $tag) { - $tags[$key] = str_replace(array(''', - '"'), - array("'", - '"'), - strtolower($tag)); + $tags[$key] = $this->unescape_string(strtolower($tag)); } return $tags; } + + final protected function escape_string($string) { + return str_replace(array("'", + '"'), + array(''', + '"'), + $string); + } + + final protected function unescape_string($string) { + return str_replace(array(''', + '"'), + array("'", + '"'), + $string); + } } ?> diff --git a/include/functions.php b/include/functions.php index 50520a4..ffb6c39 100644 --- a/include/functions.php +++ b/include/functions.php @@ -97,6 +97,8 @@ function make_page($page) { return new QR(); case 'print': return new Printer(); + case 'dl': + return new Download(); } } diff --git a/script.js b/script.js index cb77bb2..4ac5bfe 100644 --- a/script.js +++ b/script.js @@ -2,18 +2,7 @@ function ajaxRequest(action, datalist, callback) { var request = false request = new XMLHttpRequest() request.open('POST', "./?page=ajax&action=" + action, true) - request.setRequestHeader('Content-Type', - 'application/x-www-form-urlencoded') - var datastring = '' - var first = true - for(let [key, value] of datalist) { - if(!first) { - datastring += '&' - } - datastring += key + '=' + encodeURIComponent(value) - first = false - } - request.send(datastring) + request.send(datalist) request.onreadystatechange = function() { if (request.readyState == 4) { @@ -83,12 +72,18 @@ function fixDuplicateInputNames(fields) { } function dataListFromForm(form, filter = function(field) {return true}) { - var out = [] + var out = new FormData() var fields = form.querySelectorAll('input,textarea') fixDuplicateInputNames(fields) for(var i = 0; i < fields.length; i++) { - if(filter(fields[i])) { - out.push([fields[i].name, fields[i].value]) + var field = fields[i] + if(filter(field)) { + if(field.type == 'file') { + var file = field.files[0] + out.append(field.name, file, file.name) + } else { + out.append(field.name, field.value) + } } } return out @@ -103,9 +98,9 @@ function getFragment(name, callback) { } } - ajaxRequest('getfragment', - [['fragment', name]], - unpack) + var data = new FormData() + data.append('fragment', name) + ajaxRequest('getfragment', data, unpack) } function replace(fragment, replacements) { @@ -158,12 +153,12 @@ function extendLoan(event) { function startInventory(event) { event.preventDefault() - ajaxRequest('startinventory', [], reloadOrError) + ajaxRequest('startinventory', new FormData(), reloadOrError) } function endInventory(event) { event.preventDefault() - ajaxRequest('endinventory', [], reloadOrError) + ajaxRequest('endinventory', new FormData(), reloadOrError) } function inventoryProduct(event) { @@ -220,7 +215,9 @@ function suggest(input, type) { suggestlist.appendChild(next) } } - ajaxRequest('suggest', [['type', type]], render) + data = new FormData() + data.append('type', type) + ajaxRequest('suggest', data, render) } function suggestContent(input) { @@ -236,7 +233,9 @@ function suggestContent(input) { suggestlist.appendChild(next) } } - ajaxRequest('suggestcontent', [['fieldname', input.name]], render) + data = new FormData() + data.append('fieldname', input.name) + ajaxRequest('suggestcontent', data, render) } function addField(event) { @@ -414,6 +413,59 @@ function updateUser(event) { ajaxRequest('updateuser', dataListFromForm(form), reloadOrError) } +function uploadAttachment(event) { + event.preventDefault() + var form = event.currentTarget + var render = function(result) { + if(result.type != 'success') { + showResult(result) + return + } + var classvalue = 'attachment-list' + var list = form.parentNode.querySelector('.'+classvalue) + if(list == null) { + list = document.createElement('ul') + list.classList.add(classvalue) + var p = form.parentNode.querySelector('p') + p.replaceWith(list) + } + var temp = document.createElement('template') + temp.innerHTML = result.message + list.appendChild(temp.content.firstChild) + } + var filter = function(input) { + if(input.name == 'filename') { + return false; + } + return true; + } + ajaxRequest('addattachment', dataListFromForm(form, filter), render) +} + +function deleteAttachment(event) { + event.preventDefault() + var form = event.currentTarget + var node = form.parentNode + var name = form.name.value + if(window.confirm("Är du säker på att du vill ta bort bilagan '" + +name+"'?")) { + var render = function(result) { + if(result.type == 'success') { + var list = node.parentNode + list.removeChild(node) + if(list.childElementCount == 0) { + var p = document.createElement('p') + p.append('Inga bilagor.') + list.replaceWith(p) + } + } else { + showResult(result); + } + } + ajaxRequest('deleteattachment', dataListFromForm(form), render) + } +} + function productDataList(form) { var filter = function(input) { var name = input.name @@ -513,11 +565,11 @@ function removeTerm(event) { } function selectFile(event) { - var fileinput = document.getElementById("uploadfile") + var fileinput = event.currentTarget.parentNode.uploadfile fileinput.click() } function showFile(event) { - var filefield = document.getElementById("filename") + var filefield = event.currentTarget.parentNode.filename filefield.value = event.currentTarget.files[0].name } diff --git a/style.css b/style.css index 2baa6f2..c1ec303 100644 --- a/style.css +++ b/style.css @@ -2,6 +2,10 @@ textarea { height: 80px; } +ul { + padding-left: 15px; +} + #message { position: absolute; top: 5px;