diff --git a/.gitignore b/.gitignore index c583050..3836c57 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ settings.php nbproject/private database.mwb.bak *.sync-conflict* +images/ +!images/.htaccess \ No newline at end of file diff --git a/action.php b/action.php index 76636bd..72f6b23 100644 --- a/action.php +++ b/action.php @@ -229,6 +229,129 @@ switch ($VARS['action']) { exit("[]"); } break; + case "imageupload": + $destpath = FILE_UPLOAD_PATH; + if (!is_writable($destpath)) { + returnToSender("unwritable_folder", "&id=$VARS[itemid]"); + } + + if (empty($VARS['itemid']) || !$database->has('items', ['itemid' => $VARS['itemid']])) { + returnToSender("invalid_itemid", "&id=$VARS[itemid]"); + } + + $files = []; + foreach ($_FILES['files'] as $key => $all) { + foreach ($all as $i => $val) { + $files[$i][$key] = $val; + } + } + + $errors = []; + foreach ($files as $f) { + if ($f['error'] !== UPLOAD_ERR_OK) { + $err = "could not be uploaded."; + switch ($f['error']) { + case UPLOAD_ERR_INI_SIZE: + case UPLOAD_ERR_FORM_SIZE: + $err = "is too big."; + break; + case UPLOAD_ERR_CANT_WRITE: + $err = "could not be saved to disk."; + break; + case UPLOAD_ERR_NO_FILE: + $err = "was not actually sent."; + break; + case UPLOAD_ERR_PARTIAL: + $err = "was only partially sent."; + break; + default: + $err = "could not be uploaded."; + } + $errors[] = htmlspecialchars($f['name']) . " $err"; + continue; + } + + if (filesize($f['tmp_name']) > 11) { + $imagetype = exif_imagetype($f['tmp_name']); + } else { + $imagetype = false; + } + + switch ($imagetype) { + case IMAGETYPE_JPEG: + case IMAGETYPE_GIF: + case IMAGETYPE_PNG: + case IMAGETYPE_WEBP: + $imagevalid = true; + break; + default: + $imagevalid = false; + } + + if (!$imagevalid) { + $errors[] = htmlspecialchars($f['name']) . " is not a supported image type (JPEG, GIF, PNG, WEBP)."; + continue; + } + + $filename = basename($f['name']); + $filename = preg_replace("/[^a-z0-9\._\-]/", "_", strtolower($filename)); + $n = 1; + if (file_exists($destpath . "/" . $filename)) { + while (file_exists($destpath . '/' . $n . '_' . $filename)) { + $n++; + } + $filename = $n . '_' . $filename; + } + + $finalpath = $destpath . "/" . $filename; + + if (move_uploaded_file($f['tmp_name'], $finalpath)) { + $primary = false; + if (!$database->has('images', ['AND' => ['itemid' => $VARS['itemid'], 'primary' => true]])) { + $primary = true; + } + $database->insert('images', ['itemid' => $VARS['itemid'], 'imagename' => $filename, 'primary' => $primary]); + } else { + $errors[] = htmlspecialchars($f['name']) . " could not be uploaded."; + } + } + + if (count($errors) > 0) { + returnToSender("upload_warning", implode("
", $errors) . "&id=$VARS[itemid]"); + } + returnToSender("upload_success", "&id=$VARS[itemid]"); + break; + case "promoteimage": + if (empty($VARS['itemid']) || !$database->has('items', ['itemid' => $VARS['itemid']])) { + returnToSender("invalid_itemid", "&id=$VARS[itemid]"); + } + if (empty($VARS['imageid']) || !$database->has('images', ['AND' => ['itemid' => $VARS['itemid'], 'imageid' => $VARS['imageid']]])) { + returnToSender("invalid_imageid", "&id=$VARS[itemid]"); + } + + $database->update('images', ['primary' => false], ['itemid' => $VARS['itemid']]); + $database->update('images', ['primary' => true], ['AND' => ['itemid' => $VARS['itemid'], 'imageid' => $VARS['imageid']]]); + returnToSender("image_promoted", "&id=$VARS[itemid]"); + break; + case "deleteimage": + if (empty($VARS['itemid']) || !$database->has('items', ['itemid' => $VARS['itemid']])) { + returnToSender("invalid_itemid", "&id=$VARS[itemid]"); + } + if (empty($VARS['imageid']) || !$database->has('images', ['AND' => ['itemid' => $VARS['itemid'], 'imageid' => $VARS['imageid']]])) { + returnToSender("invalid_imageid", "&id=$VARS[itemid]"); + } + + $imagename = $database->get('images', 'imagename', ['imageid' => $VARS['imageid']]); + if ($database->count('images', ['imagename' => $imagename]) <= 1) { + unlink(FILE_UPLOAD_PATH . "/" . $imagename); + } + $database->delete('images', ['AND' => ['itemid' => $VARS['itemid'], 'imageid' => $VARS['imageid']]]); + + if (!$database->has('images', ['AND' => ['itemid' => $VARS['itemid'], 'primary' => true]])) { + $database->update('images', ['primary' => true], ['itemid' => $VARS['itemid'], 'LIMIT' => 1]); + } + + returnToSender("image_deleted", "&id=$VARS[itemid]"); case "signout": session_destroy(); header('Location: index.php'); diff --git a/composer.lock b/composer.lock index d07df16..cb6b6d1 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,6 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "831ae7560f5a64a68c52c69b772a8fc5", "content-hash": "a7d5f4f0d86fc84b983b1a3095ba3288", "packages": [ { @@ -64,7 +63,7 @@ "sql", "sqlite" ], - "time": "2018-06-14 18:59:08" + "time": "2018-06-14T18:59:08+00:00" }, { "name": "guzzlehttp/guzzle", @@ -129,7 +128,7 @@ "rest", "web service" ], - "time": "2018-04-22 15:46:56" + "time": "2018-04-22T15:46:56+00:00" }, { "name": "guzzlehttp/promises", @@ -180,7 +179,7 @@ "keywords": [ "promise" ], - "time": "2016-12-20 10:07:11" + "time": "2016-12-20T10:07:11+00:00" }, { "name": "guzzlehttp/psr7", @@ -245,7 +244,7 @@ "uri", "url" ], - "time": "2017-03-20 17:10:46" + "time": "2017-03-20T17:10:46+00:00" }, { "name": "lapinator/ods-php-generator", @@ -288,7 +287,7 @@ "LibreOffice", "ods" ], - "time": "2016-04-14 21:51:27" + "time": "2016-04-14T21:51:27+00:00" }, { "name": "league/csv", @@ -355,7 +354,7 @@ "read", "write" ], - "time": "2018-05-01 18:32:48" + "time": "2018-05-01T18:32:48+00:00" }, { "name": "psr/http-message", @@ -405,7 +404,7 @@ "request", "response" ], - "time": "2016-08-06 14:39:51" + "time": "2016-08-06T14:39:51+00:00" } ], "packages-dev": [], diff --git a/database.mwb b/database.mwb index de28faa..5808d6d 100644 Binary files a/database.mwb and b/database.mwb differ diff --git a/database.sql b/database.sql index b4bf013..5cac0af 100644 --- a/database.sql +++ b/database.sql @@ -1,5 +1,5 @@ -- MySQL Script generated by MySQL Workbench --- Tue 26 Dec 2017 04:44:44 PM MST +-- Sat 22 Sep 2018 02:40:11 AM MDT -- Model: New Model Version: 1.0 -- MySQL Workbench Forward Engineering @@ -12,15 +12,9 @@ SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='TRADITIONAL,ALLOW_INVALID_DATES'; -- ----------------------------------------------------- -- ----------------------------------------------------- --- Schema inventory --- ----------------------------------------------------- -CREATE SCHEMA IF NOT EXISTS `inventory` DEFAULT CHARACTER SET utf8 ; -USE `inventory` ; - +-- Table `categories` -- ----------------------------------------------------- --- Table `inventory`.`categories` --- ----------------------------------------------------- -CREATE TABLE IF NOT EXISTS `inventory`.`categories` ( +CREATE TABLE IF NOT EXISTS `categories` ( `catid` INT NOT NULL AUTO_INCREMENT, `catname` VARCHAR(45) NOT NULL, PRIMARY KEY (`catid`), @@ -29,9 +23,9 @@ ENGINE = InnoDB; -- ----------------------------------------------------- --- Table `inventory`.`locations` +-- Table `locations` -- ----------------------------------------------------- -CREATE TABLE IF NOT EXISTS `inventory`.`locations` ( +CREATE TABLE IF NOT EXISTS `locations` ( `locid` INT NOT NULL AUTO_INCREMENT, `locname` VARCHAR(100) NOT NULL, `loccode` VARCHAR(45) NOT NULL, @@ -42,9 +36,9 @@ ENGINE = InnoDB; -- ----------------------------------------------------- --- Table `inventory`.`items` +-- Table `items` -- ----------------------------------------------------- -CREATE TABLE IF NOT EXISTS `inventory`.`items` ( +CREATE TABLE IF NOT EXISTS `items` ( `itemid` INT NOT NULL AUTO_INCREMENT, `catid` INT NOT NULL, `locid` INT NOT NULL, @@ -57,27 +51,29 @@ CREATE TABLE IF NOT EXISTS `inventory`.`items` ( `qty` INT NOT NULL DEFAULT 1, `want` INT NOT NULL DEFAULT 0, `userid` INT NULL, - PRIMARY KEY (`itemid`, `catid`, `locid`), + `cost` DECIMAL(10,2) NULL, + `price` DECIMAL(10,2) NULL, + PRIMARY KEY (`itemid`), INDEX `fk_items_categories_idx` (`catid` ASC), INDEX `fk_items_locations1_idx` (`locid` ASC), UNIQUE INDEX `itemid_UNIQUE` (`itemid` ASC), CONSTRAINT `fk_items_categories` FOREIGN KEY (`catid`) - REFERENCES `inventory`.`categories` (`catid`) + REFERENCES `categories` (`catid`) ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT `fk_items_locations1` FOREIGN KEY (`locid`) - REFERENCES `inventory`.`locations` (`locid`) + REFERENCES `locations` (`locid`) ON DELETE NO ACTION ON UPDATE NO ACTION) ENGINE = InnoDB; -- ----------------------------------------------------- --- Table `inventory`.`labels` +-- Table `labels` -- ----------------------------------------------------- -CREATE TABLE IF NOT EXISTS `inventory`.`labels` ( +CREATE TABLE IF NOT EXISTS `labels` ( `rowid` INT NOT NULL AUTO_INCREMENT, `name` VARCHAR(50) NOT NULL, `value` VARCHAR(100) NOT NULL, @@ -86,9 +82,9 @@ ENGINE = InnoDB; -- ----------------------------------------------------- --- Table `inventory`.`permissions` +-- Table `permissions` -- ----------------------------------------------------- -CREATE TABLE IF NOT EXISTS `inventory`.`permissions` ( +CREATE TABLE IF NOT EXISTS `permissions` ( `userid` INT NOT NULL, `itemid` INT NOT NULL, `canedit` TINYINT(1) NOT NULL DEFAULT 0, @@ -96,16 +92,16 @@ CREATE TABLE IF NOT EXISTS `inventory`.`permissions` ( INDEX `fk_permissions_items1_idx` (`itemid` ASC), CONSTRAINT `fk_permissions_items1` FOREIGN KEY (`itemid`) - REFERENCES `inventory`.`items` (`itemid`) + REFERENCES `items` (`itemid`) ON DELETE NO ACTION ON UPDATE NO ACTION) ENGINE = InnoDB; -- ----------------------------------------------------- --- Table `inventory`.`report_access_codes` +-- Table `report_access_codes` -- ----------------------------------------------------- -CREATE TABLE IF NOT EXISTS `inventory`.`report_access_codes` ( +CREATE TABLE IF NOT EXISTS `report_access_codes` ( `id` INT NOT NULL AUTO_INCREMENT, `code` VARCHAR(45) NULL, `expires` DATETIME NULL, @@ -114,6 +110,42 @@ CREATE TABLE IF NOT EXISTS `inventory`.`report_access_codes` ( ENGINE = InnoDB; +-- ----------------------------------------------------- +-- Table `images` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `images` ( + `imageid` INT NOT NULL AUTO_INCREMENT, + `itemid` INT NOT NULL, + `imagename` VARCHAR(255) NOT NULL, + `primary` TINYINT(1) NOT NULL DEFAULT 0, + PRIMARY KEY (`imageid`, `itemid`), + UNIQUE INDEX `imageid_UNIQUE` (`imageid` ASC), + INDEX `fk_images_items1_idx` (`itemid` ASC), + CONSTRAINT `fk_images_items1` + FOREIGN KEY (`itemid`) + REFERENCES `items` (`itemid`) + ON DELETE NO ACTION + ON UPDATE NO ACTION) +ENGINE = InnoDB; + + SET SQL_MODE=@OLD_SQL_MODE; SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS; SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS; + +-- ----------------------------------------------------- +-- Data for table `labels` +-- ----------------------------------------------------- +START TRANSACTION; +INSERT INTO `labels` (`rowid`, `name`, `value`) VALUES (1, 'itemid', 'Item ID'); +INSERT INTO `labels` (`rowid`, `name`, `value`) VALUES (2, 'catid', 'Category ID'); +INSERT INTO `labels` (`rowid`, `name`, `value`) VALUES (3, 'itemname', 'Item Name'); +INSERT INTO `labels` (`rowid`, `name`, `value`) VALUES (4, 'itemnumber1', 'Number Value 1'); +INSERT INTO `labels` (`rowid`, `name`, `value`) VALUES (5, 'itemnumber2', 'Number Value 2'); +INSERT INTO `labels` (`rowid`, `name`, `value`) VALUES (6, 'itemtext1', 'Text Value 1'); +INSERT INTO `labels` (`rowid`, `name`, `value`) VALUES (7, 'itemtext2', 'Text Value 2'); +INSERT INTO `labels` (`rowid`, `name`, `value`) VALUES (8, 'itemtext3', 'Text Value 3'); +INSERT INTO `labels` (`rowid`, `name`, `value`) VALUES (9, 'catname', 'Category Name'); + +COMMIT; + diff --git a/database_upgrade/v1.1_1.2.sql b/database_upgrade/v1.1_1.2.sql index dddaad0..6e9f631 100644 --- a/database_upgrade/v1.1_1.2.sql +++ b/database_upgrade/v1.1_1.2.sql @@ -5,12 +5,18 @@ */ ALTER TABLE `items` ADD COLUMN `cost` DECIMAL(10,2) NULL DEFAULT NULL AFTER `userid`, -ADD COLUMN `price` DECIMAL(10,2) NULL DEFAULT NULL AFTER `cost`; +ADD COLUMN `price` DECIMAL(10,2) NULL DEFAULT NULL AFTER `cost`;\ + +ALTER TABLE `items` +DROP PRIMARY KEY, +ADD PRIMARY KEY (`itemid`); + CREATE TABLE IF NOT EXISTS `images` ( `imageid` INT(11) NOT NULL AUTO_INCREMENT, `itemid` INT(11) NOT NULL, `imagename` VARCHAR(255) NOT NULL, + `primary` TINYINT(1) NOT NULL DEFAULT 0, PRIMARY KEY (`imageid`, `itemid`), UNIQUE INDEX `imageid_UNIQUE` (`imageid` ASC), INDEX `fk_images_items1_idx` (`itemid` ASC), @@ -20,4 +26,4 @@ CREATE TABLE IF NOT EXISTS `images` ( ON DELETE NO ACTION ON UPDATE NO ACTION) ENGINE = InnoDB -DEFAULT CHARACTER SET = utf8 +DEFAULT CHARACTER SET = utf8 \ No newline at end of file diff --git a/image.php b/image.php new file mode 100644 index 0000000..dc2341d --- /dev/null +++ b/image.php @@ -0,0 +1,62 @@ + 11) { + $imagetype = exif_imagetype($filepath); +} else { + $imagetype = false; +} + +switch ($imagetype) { + case IMAGETYPE_JPEG: + $mimetype = "image/jpeg"; + break; + case IMAGETYPE_GIF: + $mimetype = "image/gif"; + break; + case IMAGETYPE_PNG: + $mimetype = "image/png"; + break; + case IMAGETYPE_WEBP: + $mimetype = "image/webp"; + break; + default: + $mimetype = "application/octet-stream"; +} + +header("Content-Type: $mimetype"); +header('Content-Length: ' . filesize($filepath)); +header("X-Content-Type-Options: nosniff"); +$seconds_to_cache = 60 * 60 * 12; // 12 hours +$ts = gmdate("D, d M Y H:i:s", time() + $seconds_to_cache) . " GMT"; +header("Expires: $ts"); +header("Pragma: cache"); +header("Cache-Control: max-age=$seconds_to_cache"); + +ob_end_flush(); + +readfile($filepath); diff --git a/images/.htaccess b/images/.htaccess new file mode 100755 index 0000000..14249c5 --- /dev/null +++ b/images/.htaccess @@ -0,0 +1 @@ +Deny from all \ No newline at end of file diff --git a/langs/en/images.json b/langs/en/images.json new file mode 100644 index 0000000..bb9a405 --- /dev/null +++ b/langs/en/images.json @@ -0,0 +1,10 @@ +{ + "Editing images for item": "Editing images for {item}", + "Edit Images": "Edit Images", + "Browse": "Browse", + "Upload": "Upload", + "Promoted": "Promoted", + "Promote": "Promote", + "Delete": "Delete", + "Back": "Back" +} diff --git a/pages.php b/pages.php index 2d50c5f..c5b92e5 100644 --- a/pages.php +++ b/pages.php @@ -61,6 +61,16 @@ define("PAGES", [ "static/js/edititem.js" ], ], + "editimages" => [ + "title" => "Edit item images", + "navbar" => false, + "styles" => [ + "static/css/files.css" + ], + "scripts" => [ + "static/js/files.js" + ] + ], "editcat" => [ "title" => "Edit category", "navbar" => false, diff --git a/pages/editimages.php b/pages/editimages.php new file mode 100644 index 0000000..0e98ddf --- /dev/null +++ b/pages/editimages.php @@ -0,0 +1,87 @@ +has('items', ['itemid' => $VARS['id']])) { + $images = $database->select('images', ['imageid', 'imagename', 'primary'], ['itemid' => $VARS['id']]); +} else { + header('Location: app.php?page=items&msg=invalid_itemid'); + die(); +} +?> + +
+

+ build("Editing images for item", ['item' => "" . htmlspecialchars($database->get('items', 'name', ['itemid' => $VARS['id']])) . ""]); ?> +

+
+
+
+
+ +
+ + get("Browse"); ?> + + +
+
+ + + +
+
+
+ +
+
+ +
+ + get("Promoted"); ?> + +
+ + + + + +
+ +
+ + + + + +
+
+
+
+ +
+
+ + +
\ No newline at end of file diff --git a/pages/edititem.php b/pages/edititem.php index 636557a..825db81 100644 --- a/pages/edititem.php +++ b/pages/edititem.php @@ -214,14 +214,26 @@ if (!empty($VARS['id'])) { ?>" /> + + + diff --git a/settings.template.php b/settings.template.php index 26da672..0a00a4d 100644 --- a/settings.template.php +++ b/settings.template.php @@ -34,6 +34,11 @@ define("TIMEZONE", "America/Denver"); // Base URL for site links. define('URL', '.'); +// Folder for item images +// If in the webroot, verify that the contents of the folder are not accessible +// from a client (web browser). +define('FILE_UPLOAD_PATH', __DIR__ . '/images'); + // Use Captcheck on login screen // https://captcheck.netsyms.com define("CAPTCHA_ENABLED", FALSE); diff --git a/static/css/files.css b/static/css/files.css new file mode 100644 index 0000000..99485dc --- /dev/null +++ b/static/css/files.css @@ -0,0 +1,24 @@ +/* +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ +.btn-file { + position: relative; + overflow: hidden; +} +.btn-file input[type=file] { + position: absolute; + top: 0; + right: 0; + min-width: 100%; + min-height: 100%; + font-size: 100px; + text-align: right; + filter: alpha(opacity=0); + opacity: 0; + outline: none; + background: white; + cursor: inherit; + display: block; +} diff --git a/static/js/files.js b/static/js/files.js new file mode 100644 index 0000000..23557f7 --- /dev/null +++ b/static/js/files.js @@ -0,0 +1,19 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +$(document).on('change', ':file', function () { + var input = $(this), + numFiles = input.get(0).files ? input.get(0).files.length : 1, + label = input.val().replace(/\\/g, '/').replace(/.*\//, ''); + input.trigger('fileselect', [numFiles, label]); +}); + +$(':file').on('fileselect', function (event, numFiles, label) { + var message = numFiles > 1 ? numFiles + ' files selected' : label; + $("#uploadstatus").val(message); +}); + +$(function () { + $('[data-toggle="tooltip"]').tooltip(); +}) \ No newline at end of file