diff --git a/.gitignore b/.gitignore index 96374c4..5baa208 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,7 @@ $RECYCLE.BIN/ Network Trash Folder Temporary Items .apdisk + +# Local configuration +config.local.php + diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..29febea --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/phpqrcode-git"] + path = lib/phpqrcode-git + url = git://git.code.sf.net/p/phpqrcode/git diff --git a/.htaccess b/.htaccess index 9862c54..bfe19a4 100644 --- a/.htaccess +++ b/.htaccess @@ -1,2 +1,2 @@ Options All -Indexes -DirectoryIndex api.php +DirectoryIndex index.php diff --git a/CONSTANTS.php b/CONSTANTS.php index 32a6cc5..a053d7d 100644 --- a/CONSTANTS.php +++ b/CONSTANTS.php @@ -3,27 +3,29 @@ define('API_SUCCESS_LIST', 1000); define('API_SUCCESS_LIST_EMPTY', 1001); - define('API_SUCCESS_UPDATE', 1002); - define('API_SUCCESS_FAVORITE', 1003); - define('API_SUCCESS_DELETE', 1004); - define('API_SUCCESS_SAVE', 1005); + define('API_SUCCESS_UPDATE', 1002); + define('API_SUCCESS_FAVORITE', 1003); + define('API_SUCCESS_DELETE', 1004); + define('API_SUCCESS_SAVE', 1005); define('API_SUCCESS_CLEAR', 1006); define('API_ERROR_SERVER', 5000); - define('API_ERROR_404', 5001); - define('API_ERROR_403', 5002); - define('API_ERROR_MISSING_FUNCTION', 5003); - define('API_ERROR_NO_DATABASE', 5004); - define('API_ERROR_CONFIG', 5005); - define('API_ERROR_UNKNOWN', 5006); + define('API_ERROR_404', 5001); + define('API_ERROR_403', 5002); + define('API_ERROR_MISSING_FUNCTION', 5003); + define('API_ERROR_NO_DATABASE', 5004); + define('API_ERROR_CONFIG', 5005); + define('API_ERROR_UNKNOWN', 5006); define('API_ERROR_DATABASE_CONNECT', 5012); define('API_ERROR_MISSING_PARAMETER', 5013); - define('API_ERROR_FUNCTION_NOT_SPECIFIED', 5014); + define('API_ERROR_FUNCTION_NOT_SPECIFIED', 5014); define('API_ERROR_NOT_CONFIGURED', 5015); define('API_ERROR_UPDATE_', 6001); - define('API_ERROR_FAVORITE', 6002); - define('API_ERROR_DELETE', 6003); - define('API_ERROR_SAVE', 6004); + define('API_ERROR_FAVORITE', 6002); + define('API_ERROR_DELETE', 6003); + define('API_ERROR_SAVE', 6004); define('API_ERROR_CLEAR', 6005); + define('API_ERROR_LIST', 6006); + define('API_ERROR_QRCODE', 6007); ?> diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8a6b7f3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +FROM alpine + +RUN apk update \ + && apk add \ + nginx \ + php-fpm \ + php-json \ + php-pdo \ + php-pdo_sqlite \ + && rm -rf /var/cache/apk/* + +COPY . /shoppinglist + +VOLUME [ "/shoppinglist/data" ] + +RUN chown -R nginx:www-data /shoppinglist/ \ + && chown -R nginx:www-data /shoppinglist/data/ + +# Copy scripts +COPY docker/nginx.conf /etc/nginx/ +COPY docker/php-fpm.conf /etc/php/ +COPY docker/entrypoint.sh / + + +EXPOSE 80 + +ENV API_KEY="" + + +CMD [ "/entrypoint.sh" ] diff --git a/INSTALL.php b/INSTALL.php index fa05722..f69d732 100644 --- a/INSTALL.php +++ b/INSTALL.php @@ -1,288 +1,301 @@ - - - - - - - Install ShoppingList - - -
-
- =') AND version_compare(phpversion(), '5.5', '<')) - require_once('func.inc.php'); + + + + + + +Install ShoppingList + + +
+
+=') AND version_compare(phpversion(), '5.5', '<')) + require_once('func.inc.php'); - $htaccess = "Options ALL -Indexes\nDirectoryIndex api.php"; - if($dbtype === 'SQLite'){ - $htaccess .= "\n\norder allow,deny\ndeny from all\n\n"; - } - - file_put_contents('.htaccess', $htaccess); - //create config file value +//Start on submit form +if(isset($_POST['createConfig'])) { + function generateRandomPWD($length = 25) { + $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + $charactersLength = strlen($characters); + $randomString = ''; + for ($i = 0; $i < $length; $i++) { + $randomString .= $characters[rand(0, $charactersLength - 1)]; + } + return $randomString; + } - if($createDBUser == "true") { - $config_access = ' - "'.$filename.'", - ); - //only for MySQL - $MySQLConfig = array( - \'host\' => "'.$dbhost.'", - \'db\' => "shopping", - \'user\' => "ShoppingListUser", - \'password\' => "'.$dbrandom_pwd.'", - ); -?>'; + $qr_code = "{ \"url\": \"$api_url\", \"apikey\": \"$_apikey\", \"ssl\": $ssl}"; - //mysql dump for root access - $dbdump_access = "CREATE USER 'ShoppingListUser'@'".$dbhost."' IDENTIFIED BY '".$dbrandom_pwd."';"; - $dbdump_access2 = "CREATE DATABASE shopping;"; - $dbdump_access3 = "GRANT ALL PRIVILEGES ON shopping.ShoppingList TO 'ShoppingListUser'@'".$dbhost."';"; + ob_start(); + QRCode::png($qr_code); + $image_string = base64_encode(ob_get_clean()); + header('Content-Type: text/html'); - } - else - { + return $image_string; + } - $config = ' - "'.$filename.'", - ); - //only for MySQL - $MySQLConfig = array( - \'host\' => "'.$dbhost.'", - \'db\' => "'.$dbname.'", - \'user\' => "'.$dbuser.'", - \'password\' => "'.$dbpassword.'", - ); -?>'; - } + $filename = 'shoppinglist.sqlite'; + //set up .htaccess rules for sqlite database if sqlite is used - //mysql dump - $dbdump = " - CREATE TABLE ShoppingList ( - item VARCHAR(255), - count VARCHAR(255), - RID int(11) NOT NULL auto_increment, - primary KEY (RID)) - ENGINE=InnoDB DEFAULT COLLATE=utf8_general_ci;"; + $htaccess = "Options ALL -Indexes\nDirectoryIndex api.php"; + if($dbtype === 'SQLite') { + $htaccess .= "\n\norder allow,deny\ndeny from all\n\n"; + } - //try to open/create config.php file - //success: write config value - //error: message and get out config.php value - if($handler = fopen('config.php', 'w')) { - if($createDBUser == "true") - fwrite($handler, $config_access); - else - fwrite($handler, $config); - - fclose($handler); - } - else - { - echo <<Config.php - -
- - - + file_put_contents('.htaccess', $htaccess); + //create config file value + + if($createDBUser == "true") { + $config_access = ' + "'.$filename.'", + ); + //only for MySQL + $MySQLConfig = array( + \'host\' => "'.$dbhost.'", + \'db\' => "shopping", + \'user\' => "ShoppingListUser", + \'password\' => "'.$dbrandom_pwd.'", + ); + ?>'; + + //mysql dump for root access + $dbdump_access = "CREATE USER 'ShoppingListUser'@'".$dbhost."' IDENTIFIED BY '".$dbrandom_pwd."';"; + $dbdump_access2 = "CREATE DATABASE shopping;"; + $dbdump_access3 = "GRANT ALL PRIVILEGES ON shopping.ShoppingList TO 'ShoppingListUser'@'".$dbhost."';"; + + } else { + + $config = ' + "'.$filename.'", + ); + //only for MySQL + $MySQLConfig = array( + \'host\' => "'.$dbhost.'", + \'db\' => "'.$dbname.'", + \'user\' => "'.$dbuser.'", + \'password\' => "'.$dbpassword.'", + ); + ?>'; + } + + //mysql dump + $dbdump = " + CREATE TABLE IF NOT EXISTS ShoppingList ( + item VARCHAR(255), + count VARCHAR(255), + RID int(11) NOT NULL auto_increment, + primary KEY (RID)) + ENGINE=InnoDB DEFAULT COLLATE=utf8_general_ci;"; + + //try to open/create config.php file + //success: write config value + //error: message and get out config.php value + if($handler = fopen('config.php', 'w')) { + if($createDBUser == "true") + fwrite($handler, $config_access); + else + fwrite($handler, $config); + + fclose($handler); + } else { + echo <<Config.php + +
+ + '; + } - //execute query and check if successful - if($createDBUser == "true") { - if($stmt1->execute() && $stmt2->execute() && $stmt3->execute()) { - $mysql_error = false; - $handler = new mysqli($dbhost, $dbuser, $dbpassword, 'shopping'); - $stmt4 = $handler->prepare($dbdump); - if($stmt4->execute()) - $mysql_error = false; - else - $mysql_error = true; - } - else { - $mysql_error = true; - } - //close connection - $stmt1->close(); - $stmt2->close(); - $stmt3->close(); - $stmt4->close(); - } - else { - if($stmt->execute()) - $mysql_error = false; - else - $mysql_error = true; - - //close connection - $stmt->close(); - } - if (!$mysql_error) - echo ''; - else - echo ''; - + //when DB Type MySQL create table + if($dbtype == "MySQL") { + if($createDBUser == "true") + $handler = new mysqli($dbhost, $dbuser, $dbpassword); + else + $handler = new mysqli($dbhost, $dbuser, $dbpassword, $dbname); + + //check if connection successful + if ($handler->connect_error) + die(''); + + //prepare query + if($createDBUser == "true") { + $stmt1 = $handler->prepare($dbdump_access); + $stmt2 = $handler->prepare($dbdump_access2); + $stmt3 = $handler->prepare($dbdump_access3); } else - { - echo ''; + $stmt = $handler->prepare($dbdump); + + //execute query and check if successful + if($createDBUser == "true") { + if($stmt1->execute() && $stmt2->execute() && $stmt3->execute()) { + $mysql_error = false; + $handler = new mysqli($dbhost, $dbuser, $dbpassword, 'shopping'); + $stmt4 = $handler->prepare($dbdump); + if($stmt4->execute()) + $mysql_error = false; + else + $mysql_error = true; + } + else { + $mysql_error = true; + } + //close connection + $stmt1->close(); + $stmt2->close(); + $stmt3->close(); + $stmt4->close(); + } else { + if($stmt->execute()) + $mysql_error = false; + else + $mysql_error = true; + + //close connection + $stmt->close(); } + if (!$mysql_error) { + echo ''; + + echo '
+

Open your app and scan code for automatically configuration

'; + + } else { + echo ''; } - else - { - ?> + } else { + echo ''; + + echo '
+

Open your app and scan code for automatically configuration

'; + } +} else { +?>

Install ShoppingList Database

API Key

-
-
-
- -
-
- -

Database Setup

-
-
-
- -
-

-
-

 Do you want to create a user and database for that App?

- -
-
-
- +
+
+
+ +
-


-
-
-
- + +

Database Setup

+
+
+
+ +
+

+
+

 Do you want to create a user and database for that App?

+ +
+
+
+ +
+
+

+
+
+
+ +
+

+
+
+
+
+ +
+
+

+
+
+
+ +
+


-
-
-
-
- -
-


-
-
-
- -
-
-


- Click Create to write the necessary configuration to config.php and in case of MySQL to create a table for the list storage in the given database.

-
+ Click Create to write the necessary configuration to config.php and in case of MySQL to create a table for the list storage in the given database.

+
-
- -
+
+
- + diff --git a/README.md b/README.md index 3bd2e45..65e2523 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,43 @@ # ShoppingList Backend +##Docker Deployment +For a fast deployment, use Docker! +For now the container is only compatible with an sqlite database. + +###Build from source +You can build the container from sources: + +> git clone https://github.com/GroundApps/ShoppingList_Backend/tree/devel +> docker build -t shoppinglist . + +###Pull an already built container +Or, if you are feeling lazy, just pull an already built container from the +[Docker hub](https://hub.docker.com/r/lertsenem/shoppinglist): + +> docker pull lertsenem/shoppinglist + +###Run the container +Finally, run the container with a command like this: + +> $ docker run \ +> -d \ +> --name shoppinglist \ +> -p 8000:80 \ +> -e "API_KEY=mysecretpassword" \ +> -v /tmp/sl_data:/shoppinglist/data \ +> lertsenem/shoppinglist + +* use the env variable 'API_KEY' to set the app password ; +* mount the volume '/shoppinglist/data' to persist the sqlite database. + +Note that in regard to this last point you can (and should) use a volume-only +container for portability reasons. + ##Installation ###Requirements * PHP >= 5.3.7 +* php-gd * Apache Websever (we recommend a TLS Connection to the Server) * MySQL or SQLite (you can select in the Install Script) @@ -14,8 +48,14 @@ You can either use MySQL or SQLite. SQLite is easier to set up. If you use Ubuntu you can use the [PPA](https://launchpad.net/~jklmnn/+archive/ubuntu/groundapps) by executing `add-apt-repository ppa:jklmnn/groundapps`. To install the backend manually , go to http://your.path/. Fill up the form, click on create! +======= +To install the backend manually, go to http://your.path/ +Fill up the form, click on create! That's all. +https is currently not supported for self-signed certificates. +See the [wiki](https://github.com/GroundApps/ShoppingList/wiki) for more informations about installation and roadmap. + ## Feedback Please do never hesitate to open an issue!
I know there a some bugs, most likely because I had no idea how to do it otherwise and therefore had to use a workaround. diff --git a/UPDATE.php b/UPDATE.php new file mode 100644 index 0000000..0a11b4e --- /dev/null +++ b/UPDATE.php @@ -0,0 +1,361 @@ +DEBUG"; +} +if(ISSET($_POST['update'])){ + update($_POST['zipURL'], $_POST['newVersion']); +} else { + status(); +} + +function echoHead(){ +echo << + + + + +HEAD; +} +function echoStyle(){ +<< +STYLE; +echo << + + + +BOOTSTRAP; +} + +function echoPageStart(){ +echo << +PAGE_START; +} + +function echoPageEnd(){ +echo << +PAGE_END; +} + +function echoUpdateAvailable($curVersion, $newVersion, $updateText, $zipURL, $disabled){ + echo << +
+
+

Update Available - ShoppingList Backend

+
+
+ Release Note: $updateText

+ Download URL: $zipURL +
+
+ + + +
+
+ +STATUS; +} + +function echoAllGood(){ + echo << +
+
+

ShoppingList Backend

+
+
+ Congratulations!
+ Everything is up to date. +
+
+ +ALLGOOD; +} + +function echoChecklist($newVersion, $zipURL){ +echo << +
+
+

Updating backend to version: $newVersion

+
+
+ Downloading from: $zipURL

+ +
+ +
+ +
+ +
+ +
+
+
+
+ +CHECKLIST; +} + +function echoJavascript(){ +echo << + var progressDownloadElement = document.getElementById('progressDownload') + var progressElement = document.getElementById('progressDownload') + var downloadElement = document.getElementById('cb_download') + var extractElement = document.getElementById('cb_extract') + var backupElement = document.getElementById('cb_backup') + var filemoveElement = document.getElementById('cb_filemove') + var databaseElement = document.getElementById('cb_databaseupdate') + var databaseLabel = document.getElementById('databaseupdateLabel') + + function updateProgress(percentage) { + progressElement.innerHTML = 'Download (' + percentage + '%' + ')'; + } + function updateDownload(status) { + downloadElement.checked = status; + } + function updateExtract(status) { + extractElement.checked = status; + } + function updateBackup(status) { + backupElement.checked = status; + } + function updateFilemove(status) { + filemoveElement.checked = status; + } + function updateDatabase(status) { + databaseElement.checked = status; + } + function updateDatabaseNotNecessary() { + databaseLabel.style.setProperty("text-decoration", "line-through"); + } + + +JS; +} + +function status(){ + $githubData = getGithubData(); + echoHead(); + echoPageStart(); + echoStyle(); + echoJavascript(); + if($githubData['version'] == 0){ + echoError('Could not connect to GitHub API.'); + die(); + } + if($githubData['version'] == BACKEND_VERSION){ + $updateDisabled = permissionCheck(); + echoUpdateAvailable(BACKEND_VERSION, $githubData['version'], $githubData['releasenote'], $githubData['zipurl'], $updateDisabled); + } else { + echoAllGood(); + } + echoPageEnd(); +} + +function update($zipURL, $newVersion){ + echoPageStart(); + echoHead(); + echoStyle(); + echoChecklist($newVersion, $zipURL); + echoJavascript(); + $zipFILE = downloadUpdate($zipURL); + unzipDownloadedUpdate($zipFILE); + backup(); + copyNewFiles(); + updateDatabase(); + echoPageEnd(); +} + +function echoSuccess($message){ +echo << +
+ +
+
+SUCCESS; +} + +function echoError($errorMessage){ +echo << +
+ +
+ +ERROR; + +} + +function permissionCheck(){ + if (!is_writable(__DIR__)){ + $user = get_current_user(); + $dir = __DIR__; + echoError('No write permission for user "'.$user.'" for directory "'. $dir.'"'); + return "disabled"; + } +} + +function downloadUpdate($zipurl){ + $ch = curl_init() or die('Sorry cURL is not installed!'); + $zipfile = __DIR__.'/update_'.$tag.'.zip'; + $fp = fopen ($zipfile, 'w+'); + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $zipurl); + curl_setopt($ch, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT']); + curl_setopt($ch, CURLOPT_FILE, $fp); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch,CURLOPT_NOPROGRESS,false); + curl_setopt($ch,CURLOPT_PROGRESSFUNCTION,'progress'); + curl_exec($ch); + curl_close($ch); + echo ''; + return $zipfile; + if(!file_exists($zipfile)){ + echoError('Could not download file from GitHub!'); + } +} + +function unzipDownloadedUpdate($zipfile){ + $zip = new ZipArchive; + $res = $zip->open($zipfile); + if ($res === TRUE) { + $zip->extractTo(__DIR__); + $zip->close(); + echo ''; + unlink($zipfile); + } else { + die ('Could not extract file!'); + } +} + +function backup(){ + $backupFolder = __DIR__.'/backup/'; + delDir($backupFolder); + if(!mkdir($backupFolder, 0770)){ + echoError('Could not create backup folder!'); + exit; + } + $iteratorBackup = new DirectoryIterator(__DIR__); + foreach ($iteratorBackup as $backupFile) { + $backupFileName = $backupFile->getFilename(); + //REMOVE CHECK FOR update.sql FOR RELEASE, CURRENTLY USED BECAUSE ZIP FROM GITHUB HAS NONE AND IT WOULD BE DELETED + if ($backupFileName != '.' && $backupFileName != '..' && $backupFileName != 'config.php' && $backupFileName != 'update.sql' && $backupFileName != '.htaccess' && !$backupFile->isDir() && $backupFileName[0] != '.'){ + if(!rename($backupFile->getPathname(), $backupFolder.$backupFile->getFilename())){ + die ('Could not backup files!'); + } + } + } + echo ''; +} + +function copyNewFiles(){ + $iterator = new DirectoryIterator(__DIR__); + foreach ($iterator as $fileinfo) { + if ($fileinfo->isDir() && strlen(strstr($fileinfo->getFilename(),"GroundApps-ShoppingList_Backend"))>0) { + $iteratorUpdateDir = new DirectoryIterator($fileinfo->getPathname()); + foreach ($iteratorUpdateDir as $updateFile) { + $updateFileName = $updateFile->getFilename(); + if ($updateFileName != '.' && $updateFileName != '..' && $updateFileName != 'config.php' && $updateFileName != '.htaccess'){ + rename($updateFile->getPathname(), __DIR__ .'/'.$updateFileName); + echo ''; + } + } + delDir($fileinfo->getPathname()); + } + } + echo ''; +} + +function getGithubData(){ + $ch = curl_init() or die('Sorry cURL is not installed!'); + $url = 'https://api.github.com/repos/GroundApps/ShoppingList_Backend/releases/latest'; + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_URL,$url); + $agent = 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.0'; + curl_setopt($ch,CURLOPT_HTTPHEADER,array('User-Agent: '.$agent)); + $result = curl_exec($ch); + curl_close($ch); + $obj = json_decode($result); + $tag = $obj->tag_name; + $zipurl = $obj->zipball_url; + $releasenote = $obj->body; + //$tag = "v1.1"; + $zipurl = 'https://api.github.com/repos/GroundApps/ShoppingList_Backend/zipball/v1'; + return array( + "version" => substr($tag,1), + "zipurl" => $zipurl, + "releasenote" => $releasenote, + ); +} + +function updateDatabase(){ + include('config.php'); + //IMPLEMENT DATABASE ALTERATION + // WITH PDO + $updateFILE = __DIR__."/update.sql"; + if(!file_exists($updateFILE)){ + echo ''; + } else { + $lines = file($updateFILE); + foreach ($lines as $line){ + echo $line.'
'; + //SQL FOR EACH LINE IN update.sql + } + echo ''; + } + echoSuccess('Update done! Please check if everything works and then delete the backup folder.'); +} + +function progress($clientp,$dltotal,$dlnow,$ultotal,$ulnow){ + if ($dltotal > 0){ + echo ''; + } + return(0); + } + +function delDir($dir) { + if (is_dir($dir)) { + $objects = scandir($dir); + foreach ($objects as $object) { + if ($object != "." && $object != "..") { + if (filetype($dir."/".$object) == "dir") delDir($dir."/".$object); else unlink($dir."/".$object); + } + } + reset($objects); + rmdir($dir); + } +} + +?> diff --git a/api.php b/api.php index 4dfc75c..7e5c8f8 100644 --- a/api.php +++ b/api.php @@ -1,20 +1,21 @@ - init(); //TODO: put this to INSTALL.php + +session_start(); +if (isset($_SESSION['user_logged']) && $_SESSION['user_logged'] == 0 && $_SESSION['user_read'] == 1) { + echo $db->listall(); + exit(); +} else if (! isset($_SESSION['user_logged']) || $_SESSION['user_logged'] != 1) { if (!hash_equals($authKey, crypt($auth, $authKey))){ die (json_encode(array('type' => API_ERROR_403, 'content' => 'Authentication failed.'))); } - - $db = NEW DataBase($dbConfig); - - switch ($function){ - case 'listall': +} + + +switch ($function){ + + case 'listall': echo $db->listall(); break; - case 'save': - if($db->exists($itemName)){ - echo $db->update($itemName, $itemCount); - } else { - echo $db->save($itemName, $itemCount); - } + + case 'save': + if($db->exists($itemName)){ + echo $db->update($itemName, $itemCount, $itemChecked); + } else { + echo $db->save($itemName, $itemCount, $itemChecked); + } break; - case 'saveMultiple': + + case 'saveMultiple': echo $db->saveMultiple($jsonData); break; - case 'deleteMultiple': + + case 'deleteMultiple': echo $db->deleteMultiple($jsonData); break; - case 'update': - echo $db->update($itemName, $itemCount); + + case 'update': + echo $db->update($itemName, $itemCount, $itemChecked); break; - case 'delete': + + case 'delete': echo $db->delete($itemName); break; - case 'clear': + + case 'clear': echo $db->clear(); break; - default: - die (json_encode(array('type' => API_ERROR_FUNCTION_NOT_SPECIFIED, 'content' => 'function not specified'))); - - } + case 'addQRcodeItem': + /* OUTPAN + $outpanApiKey='1a74a95c40a331e50d4b2c7fe311328c'; // taken from https://github.com/johncipponeri/outpan-api-java + $response = file_get_contents('https://api.outpan.com/v2/products/' . $itemName . '/?apikey=' . $outpanApiKey); + if($response!==false) { + $name = json_decode($response)->{'name'}; + if ( $name != '' ) { + $itemName = $name; + $itemCount = 1; + $itemChecked = 'false'; + if( $db->exists($itemName) ) { + echo $db->update($itemName, $itemCount, $itemChecked); + } else { + echo $db->save($itemName, $itemCount, $itemChecked); + } + } else { + die (json_encode(array('type' => API_ERROR_QRCODE, 'content' => 'Code not found: ' . $itemName))); + } + } else { + die (json_encode(array('type' => API_ERROR_QRCODE, 'content' => 'Error querying outpan.com'))); + } + //*/ + $opengtindbApiKey='400000000'; // taken from https://opengtindb.org/api.php + $response = file_get_contents('https://opengtindb.org/?ean=' . $itemName . '&cmd=query&queryid=' . $opengtindbApiKey); + if($response!==false) { + if(strpos($response,'error=0')!==false) { + $values = parse_ini_string(utf8_encode($response)); + //file_put_contents('/tmp/sholi',print_r($values, true)); + $name = trim($values['name']); + if ($name == '') $name = trim($values['detailname']); + if ($name == '') $name = trim($values['name_en']); + if ($name == '') $name = trim($values['detailname_en']); + if ($name == '') $name = trim($values['descr']); -?> + if ( $name != '' ) { + $itemName = $name; + $itemCount = 1; + $itemChecked = 'false'; + if( $db->exists($itemName) ) { + echo $db->update($itemName, $itemCount, $itemChecked); + } else { + echo $db->save($itemName, $itemCount, $itemChecked); + } + } else { + die (json_encode(array('type' => API_ERROR_QRCODE, 'content' => 'Code not found: ' . $itemName))); + } + } else { + die (json_encode(array('type' => API_ERROR_QRCODE, 'content' => 'Code not found: ' . $itemName . ', '.$response))); + } + } else { + die (json_encode(array('type' => API_ERROR_QRCODE, 'content' => 'Error querying opengtindb.org'))); + } + break; + + default: + die (json_encode(array('type' => API_ERROR_FUNCTION_NOT_SPECIFIED, 'content' => 'function not specified'))); + +} +?> diff --git a/config.php b/config.php index 7278a42..e6243ec 100644 --- a/config.php +++ b/config.php @@ -5,24 +5,35 @@ ############################################################*/ /*########################################################## - # Auth Key / Password, to get acces to the database # + # Auth Key / Password, to get acces to the database # ############################################################*/ - $authKey = ''; + $authKey = ''; /*########################################################## - # Branch: stable, testing, experimental # - ############################################################*/ + # Branch: stable, testing, experimental # + ############################################################*/ $branch = "stable"; /*########################################################## - # Database: MySQL, SQLite # - ############################################################*/ + # Database: MySQL, SQLite # + ############################################################*/ $dataBase = "SQLite"; /*########################################################## - # SQLite Config # - ############################################################*/ + # SQLite Config # + ############################################################*/ $SQLiteConfig = array('file' => "shoppinglist.sqlite"); /*########################################################## - # MySQL Config # + # MySQL Config # ############################################################*/ - $MySQLConfig = array('host' => "host",'db' => "db",'user' => "user",'password' => "password",); -?> + $MySQLConfig = [ + 'host' => "localhost", + 'db' => "your_database_name", + 'user' => "your_mysql_user", + 'password' => "your_mysql_password", + ]; + /*########################################################## + # if you put your config into a new file config.local.php # + # instead of changing the values above then in case of an # + # update of config.php you will not lose all your settings # + ############################################################*/ + @include('config.local.php'); +?> diff --git a/css/main.css b/css/main.css new file mode 100644 index 0000000..ab2dd15 --- /dev/null +++ b/css/main.css @@ -0,0 +1,166 @@ +/****************** + Shopping list css for web frontend + Reference : https://github.com/GroundApps/ShoppingList_Backend + Licence : http://www.gnu.org/licenses/agpl-3.0.fr.html +*******************/ +#shopItems { + list-style-type: none; + margin: 0; + padding: 0; + zoom: 1; +} + +#shopItems li { + margin: 2px 0px 12px 8px; + border-style: solid; + border-width: 0px; +} + +.itemCheck { width: 40px ; } +.itemName { } +.itemQty { width: 36px;} +.itemQty input { width: 30px;} +.itemDelete { } + +.itemCheckedTD { font-family: FontAwesome; color: #a94442; } +.itemCheckedName{ text-decoration:line-through; } +.itemUncheckedTD { font-family: FontAwesome; color: #3c763d; } +.itemCheckedTD i::before { content: '\f273'; } +.itemUncheckedTD i::before { content: '\f274'; } + +body.dragging, body.dragging * { + cursor: move !important; +} + +.dragged { + position: absolute; + opacity: 0.5; + z-index: 2000; +} + +#shopItems li.placeholder { + position: relative; + /** More li styles **/ +} +#shopItems li.placeholder:before { + position: absolute; + /** Define arrowhead **/ +} +.icon::before { + font-family: FontAwesome; + font-style: normal; + font-weight: normal; + text-transform: none !important; +} +button::before, .button::before { + left: -0.5em; + padding: 0 0 0 0.75em; + color: #aaaaaa; + position: relative; +} +.row { + margin-left: unset; + margin-right: unset; +} +.row { + border-bottom: 1px solid transparent; + box-sizing: border-box; +} +.row > * { + box-sizing: border-box; + float: left; +} +.row::after, .row::before { + clear: both; + content: ""; + display: block; + height: 0; +} +.row.uniform > * > *:first-child { + margin-top: 0; +} +.row.uniform > * > *:last-child { + margin-bottom: 0; +} +.row > * { + padding: 0 0 0 1em; +} +.row { + margin: 0 0 -1px -1em; +} +.row.uniform > * { + padding: 1em 0 0 1em; +} +.row.uniform { + margin: -1em 0 -1px -1em; +} +@media screen and (max-width: 1680px) { +.row > * { + padding: 0 0 0 1em; +} +.row { + margin: 0 0 -1px -1em; +} +.row.uniform > * { + padding: 1em 0 0 1em; +} +.row.uniform { + margin: -1em 0 -1px -1em; +} +} +@media screen and (max-width: 1280px) { +.row > * { + padding: 0 0 0 1em; +} +.row { + margin: 0 0 -1px -1em; +} +.row.uniform > * { + padding: 1em 0 0 1em; +} +.row.uniform { + margin: -1em 0 -1px -1em; +} +} +@media screen and (max-width: 980px) { +.row > * { + padding: 0 0 0 1em; +} +.row { + margin: 0 0 -1px -1em; +} +.row.uniform > * { + padding: 1em 0 0 1em; +} +.row.uniform { + margin: -1em 0 -1px -1em; +} +} +@media screen and (max-width: 736px) { +.row > * { + padding: 0 0 0 1em; +} +.row { + margin: 0 0 -1px -1em; +} +.row.uniform > * { + padding: 1em 0 0 1em; +} +.row.uniform { + margin: -1em 0 -1px -1em; +} +} +@media screen and (max-width: 480px) { +.row > * { + padding: 0 0 0 1em; +} +.row { + margin: 0 0 -1px -1em; +} +.row.uniform > * { + padding: 1em 0 0 1em; +} +.row.uniform { + margin: -1em 0 -1px -1em; +} +} diff --git a/db_connector.php b/db_connector.php new file mode 100644 index 0000000..f0584a8 --- /dev/null +++ b/db_connector.php @@ -0,0 +1,178 @@ +type = $dbtype; + switch($dbtype){ + case 'SQLite': + $db_pdo='sqlite:'.$dbargs['file']; + try{ + $this->db = new PDO($db_pdo); + }catch(PDOException $e){ + die(json_encode(array('type' => API_ERROR_DATABASE_CONNECT, 'content' => $e->getMessage()))); + } + break; + case 'MySQL': + $db_pdo='mysql:host='.$dbargs['host'].';dbname='.$dbargs['db'].';charset=utf8mb4'; + try{ + $this->db = new PDO($db_pdo, $dbargs['user'], $dbargs['password'], array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'utf8mb4' COLLATE 'utf8mb4_bin'")); + }catch(PDOException $e){ + die(json_encode(array('type' => API_ERROR_DATABASE_CONNECT, 'content' => $e->getMessage()))); + } + break; + default: + die(json_encode(array('type' => API_ERROR_MISSING_PARAMETER, 'content' => 'Missing database parameters.'))); + } + $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + //SQLITE $this->db->query('PRAGMA journal_mode=OFF;'); + } + + function init(){ + $sql = "CREATE table $this->table( + item STRING PRIMARY KEY, + count INT NOT NULL, + checked INT NOT NULL, + category STRING);"; + try{ + $this->db->exec($sql); + }catch(PDOException $e){ + //die(json_encode(array('type' => API_ERROR_UNKNOWN, 'content' => $e->getMessage()))); //uncomment after init() has been put to INSTALL.php + } + } + + function listall(){ + try{ + $sql = "SELECT * FROM $this->table ORDER BY item ASC"; + $val = $this->db->query($sql); + $stack = array(); + foreach($val as $row){ + array_push($stack, array( + 'itemTitle' => $row['item'], + 'itemCount' => $row['count'], + 'checked' => (bool)$row['checked'], + 'itemCategory' => $row['category'])); + } + if(count($stack) == 0){ + return json_encode(array('type' => API_SUCCESS_LIST_EMPTY)); + }else{ + return json_encode(array('type' => API_SUCCESS_LIST, 'items' => $stack)); + } + }catch(PDOException $e){ + die(json_encode(array('type' => API_ERROR_LIST, 'content' => $e->getMessage()))); + } + } + + function exists($item){ + $stmt = $this->db->prepare("SELECT * from $this->table WHERE item=:item;"); + $stmt->bindParam(':item', $item, PDO::PARAM_STR); + $stmt->execute(); + return (bool)count($stmt->fetchAll()); + } + + function save($item, $count, $checked = false){ + try{ + $checked = ($checked=='true' ? 1:0); + $stmt = $this->db->prepare("INSERT INTO $this->table (item, count, checked) VALUES (:item, :count, :checked);"); + $stmt->bindParam(':item', $item, PDO::PARAM_STR); + $stmt->bindParam(':count', $count, PDO::PARAM_INT); + $stmt->bindParam(':checked', $checked, PDO::PARAM_INT); + $stmt->execute(); + return json_encode(array('type' => API_SUCCESS_SAVE, 'content' => $item.' saved.')); + }catch(PDOException $e){ + return json_encode(array('type' => API_ERROR_SAVE, 'content' => $e->getMessage())); + } + } + + function saveMultiple($jsonData){ + if(empty($jsonData)){ + die(json_encode(array('type' => API_ERROR_MISSING_PARAMETER, 'content' => 'parameter missing for saveMultiple'))); + } + $itemList = json_decode($jsonData, true); + $success = true; + $errors = array(); + foreach($itemList as $item){ + $output = json_decode($this->exists($item['itemTitle']) ? $this->update($item['itemTitle'], $item['itemCount']) : $this->save($item['itemTitle'], $item['itemCount']),true); + if($output['type']!=API_SUCCESS_SAVE && $output['type']!=API_SUCCESS_UPDATE) { + $success = false; + array_push($errors, array('itemTitle' => $item['itemTitle'], 'error' => $output['content'])); + } + } + if($success) { + return json_encode(array('type' => API_SUCCESS_SAVE, 'content' => count($itemList)>1 ? 'Multiple items saved.':$itemList[0]['itemTitle'].' saved.')); + } else { + return json_encode(array('type' => API_ERROR_SAVE, 'content' => $errors)); + } + } + + function update($item, $count, $checked = false){ + try{ + $stmt = $this->db->prepare("SELECT * from $this->table WHERE item=:item;"); + $stmt->bindParam(':item', $item, PDO::PARAM_STR); + $stmt->execute(); + $rows = $stmt->fetchAll(); + $prev = $rows[0]; + + $checked = ($checked=='true' ? 1:0); + $stmt = $this->db->prepare("UPDATE $this->table SET count=:count, checked=:checked WHERE item=:item;"); + $stmt->bindParam(':item', $item, PDO::PARAM_STR); + $stmt->bindParam(':count', $count, PDO::PARAM_INT); + $stmt->bindParam(':checked', $checked, PDO::PARAM_INT); + $stmt->execute(); + + if ($prev['count'] != $count) + return json_encode(array('type' => API_SUCCESS_UPDATE, 'content' => 'Update successful.')); + + return json_encode(array('type' => API_SUCCESS_UPDATE, 'content' => ($checked ? '✔':'♻'))); + }catch(PDOException $e){ + return json_encode(array('type' => API_ERROR_UPDATE_, 'content' => $e->getMessage())); + } + } + + function deleteMultiple($jsonData){ + if(empty($jsonData)) { + die(json_encode(array('type' => API_ERROR_MISSING_PARAMETER, 'content' => 'Parameter missing for deleteMultiple.'))); + } + $itemList = json_decode($jsonData, true); + $success = true; + $errors = array(); + foreach($itemList as $item){ + $output = json_decode($this->delete($item['itemTitle']),true); + if($output['type']!=API_SUCCESS_DELETE) { + $success = false; + array_push($errors, array('itemTitle' => $item['itemTitle'], 'error' => $output['content'])); + } + } + if($success) { + return json_encode(array('type' => API_SUCCESS_DELETE, 'content' => count($itemList)>1 ? 'Multiple items deleted.':$itemList[0]['itemTitle'].' deleted.')); + } else { + return json_encode(array('type' => API_ERROR_DELETE, 'content' => $errors)); + } + } + + function delete($item){ + try{ + $stmt = $this->db->prepare("DELETE FROM $this->table WHERE item=:item"); + $stmt->bindParam(':item', $item, PDO::PARAM_STR); + $stmt->execute(); + return json_encode(array('type' => API_SUCCESS_DELETE, 'content' => 'Item deleted.')); + }catch(PDOException $e){ + return json_encode(array('type' => API_ERROR_DELETE, 'content' => $e->getMessage())); + } + } + + function clear(){ + try{ + $stmt = $this->db->exec("TRUNCATE TABLE $this->table;"); + return json_encode(array('type' => API_SUCCESS_CLEAR, 'content' => 'Database cleared.')); + }catch(PDOException $e){ + return json_encode(array('type' => API_ERROR_CLEAR, 'content' => $e->getMessage())); + } + } + + } diff --git a/docker/config_sqlite.php b/docker/config_sqlite.php new file mode 100644 index 0000000..3aa0fce --- /dev/null +++ b/docker/config_sqlite.php @@ -0,0 +1,9 @@ + "data/shoppinglist.sqlite" + ); +?> diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100755 index 0000000..68161ba --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,31 @@ +#!/bin/sh + +SL_ROOT="/shoppinglist" + +if [ -z "$API_KEY" ]; then + echo "You must define an API_KEY when running this container" >&2 + echo "Use for example: -e \"API_KEY=mysecretpassword\"" >&2 + exit 1 +fi + +# Check initialisation state +if [ -e "$SL_ROOT/INSTALL.php" ]; then + + # Hash the key and add it to config.php + hashed_apikey="$( php "$SL_ROOT/docker/hash_apikey.php" "$API_KEY" )" + + sed "s%authKey *= *.*;%authKey = '$hashed_apikey';%" "$SL_ROOT/docker/config_sqlite.php" \ + > "$SL_ROOT/config.php" + + # No need for web install + rm "$SL_ROOT/INSTALL.php" +fi + +# Reset permissions +chown -R nginx:www-data "$SL_ROOT/data" + +# Run PHP-FPM +php-fpm + +# Run nginx +nginx diff --git a/docker/hash_apikey.php b/docker/hash_apikey.php new file mode 100644 index 0000000..9c8e5c6 --- /dev/null +++ b/docker/hash_apikey.php @@ -0,0 +1,6 @@ + diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000..eccf80c --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,63 @@ +# run nginx in foreground +daemon off; + +error_log /dev/stderr; + +pid /var/run/nginx.pid; +worker_processes 5; + +events { + worker_connections 4096; +} + +http { + include /etc/nginx/mime.types; + include /etc/nginx/fastcgi.conf; + + default_type application/octet-stream; + + #tcp_nopush on; + #client_body_temp_path /tmp/nginx/body 1 2; + #fastcgi_temp_path /tmp/nginx/fastcgi_temp 1 2; + + server { + + listen 80; + + access_log /dev/stdout; + + root /shoppinglist; + index index.php index.html index.htm; + + location = /robots.txt { + allow all; + log_not_found off; + access_log off; + } + + location / { + try_files $uri $uri/ /index.html; + } + + # Deny data/ + location /data { + deny all; + } + + # pass the PHP scripts to FastCGI server listening on /tmp/phpfpm.sock + location ~ [^/]\.php(/|$) { + + fastcgi_split_path_info ^(.+?\.php)(/.*)$; + + if (!-f $document_root$fastcgi_script_name) { + return 404; + } + + fastcgi_pass unix:/tmp/phpfpm.sock; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + include fastcgi_params; + } + } + +} diff --git a/docker/php-fpm.conf b/docker/php-fpm.conf new file mode 100644 index 0000000..a82ffed --- /dev/null +++ b/docker/php-fpm.conf @@ -0,0 +1,24 @@ +error_log = /dev/stderr + +[www] +user = nginx +group = www-data +listen = /tmp/phpfpm.sock +listen.owner = nginx +listen.group = www-data + +pm = ondemand +pm.max_children = 75 +pm.process_idle_timeout = 10s +pm.max_requests = 500 + +chdir = /shoppinglist + +php_flag[display_errors] = on +php_admin_value[memory_limit] = 128M +php_admin_value[upload_max_filesize] = 2G +php_admin_value[post_max_size] = 2G +php_admin_value[always_populate_raw_post_data] = -1 +php_admin_value[output_buffering] = 0 +php_admin_value[php_value max_input_time] = 3600 +php_admin_value[php_value max_execution_time] = 3600 diff --git a/index.php b/index.php new file mode 100644 index 0000000..4b97905 --- /dev/null +++ b/index.php @@ -0,0 +1,94 @@ +Invalid API Key

"; + } + } +?> + + + + + Shopping List + + + + + + + + + + +
+
+
+

Shopping List

+ +
" method="post"> + +

API Key

+
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+ + + */ ?> + + +
+
+ + +
+
+
+ + diff --git a/js/main.js b/js/main.js new file mode 100644 index 0000000..47d7b96 --- /dev/null +++ b/js/main.js @@ -0,0 +1,419 @@ +/****************** + Shopping list javascript for web frontend + Reference : https://github.com/GroundApps/ShoppingList_Backend + Licence : http://www.gnu.org/licenses/agpl-3.0.fr.html + +*******************/ + +/*******************/ + +var apiurl="api.php"; + +/*******************/ +var BACKEND_VERSION=1.0; + +var API_SUCCESS_LIST=1000; +var API_SUCCESS_LIST_EMPTY=1001; +var API_SUCCESS_UPDATE=1002; +var API_SUCCESS_FAVORITE=1003; +var API_SUCCESS_DELETE=1004; +var API_SUCCESS_SAVE=1005; +var API_SUCCESS_CLEAR=1006; + +var API_ERROR_SERVER=5000; +var API_ERROR_404=5001; +var API_ERROR_403=5002; +var API_ERROR_MISSING_FUNCTION=5003; +var API_ERROR_NO_DATABASE=5004; +var API_ERROR_CONFIG=5005; +var API_ERROR_UNKNOWN=5006; +var API_ERROR_DATABASE_CONNECT=5012; +var API_ERROR_MISSING_PARAMETER=5013; +var API_ERROR_FUNCTION_NOT_SPECIFIED=5014; +var API_ERROR_NOT_CONFIGURED=5015; + +var API_ERROR_UPDATE_=6001; +var API_ERROR_FAVORITE=6002; +var API_ERROR_DELETE=6003; +var API_ERROR_SAVE=6004; +var API_ERROR_CLEAR=6005; +var API_ERROR_LIST=6006; +/*******************/ + +/* web page structure + + + + + +
# accordeon +

+
+

(Item add inputs and buttons)

+
    +
  • + + + + + + + +
    + +
    +
  • + +
+
+ +
+ + + + */ + + var categoryList = new Array ("Uncategorized"); // TODO : make an object here + + $.ajaxSetup({ + url: apiurl, + async: true, + dataType: "json", + type: "POST" + }); + + // add a category ( id: category ref in categoryList, name: display name ) + function addCategory(id,name) { + var read = $("#shopcategory").attr("data"); + var content= '

'+name+'


'; + if (read!=null && read.length) { + content= '

'+name+'

'; + } + $("#shopcategory").append(content); + var addButtonOBJ=$('#cat_'+id); + addButtonOBJ.find( "#addItemButton" ).button().click(addItemClick); + addButtonOBJ.find( "#shopItems" ).sortable({ + items: "li" + }); + addButtonOBJ.find( "#addItemName" ).keypress(function( event ) { + if ( event.which == 13 ) { + event.preventDefault(); + addButtonOBJ.find( "#addItemButton" ).click(); + } + }); + //XXX $("#title_"+id).droppable({drop: function (event,ui) { categoryMove(event, ui); } }); + //XXX $("#title_"+id).droppable({drop: function (event,ui) { categoryMove(event, ui, $("#title_"+id)); } }); + + return addButtonOBJ; + } + // Returns category ref in categoryList, or -1 if not found + function categoryID(name) { + for (index = 0; index < categoryList.length; index++) { + if (name == categoryList[index]) { + return index; + } + } + return -1; + } + /* Returns jquery object of a category identified by display name. + Create categoryList entry and/or object if not found.*/ + function GetCategoryOBJ(name) { + var index=categoryID(name); + if (index ==-1) { + index=categoryList.length; + categoryList[index]=name; + } + var catObject=$('#cat_'+index); + if (catObject.length ) { + return catObject + } + return addCategory(index,name) + } + // delete item in list. Only used by the delete button + function deleteItem(){ + var itemNameOBJ=$(this).parent().find(".itemName"); + var itemName=itemNameOBJ.html(); + //$(this).closest("#shopItemEntry").remove(); + var itemOBJ = $(this).closest("#shopItemEntry"); + + $.ajax({ + data: { + auth: "none", + function: "delete", + item: itemName, + }, + success: function (data) { + if (data.type == API_SUCCESS_DELETE) { + itemOBJ.remove(); + } + } + }); + } + // Add an item in the current category. Only used by "add" button of category + function addItemClick(){ + // Get the input values + var addedItemObj=$(this).parent().parent().find("#addItemName"); + var addedQtyObj=$(this).parent().parent().find("#addItemQty"); + var addedItem=addedItemObj.val(); + var addedQty=addedQtyObj.val(); + // Get category pointer and add item + var category=$(this).closest("div[id*='cat_']"); + + if(addedQty>0) { + } + else { + addedQty = 1; + addedQtyObj.val(addedQty); + } + + if(addedItem.length>0) { + + $.ajax({ + data: { + auth: "none", + function: "save", + item: addedItem, + count: addedQty + }, + success: function (data) { + if (data.type == API_SUCCESS_SAVE) { + addItem(category,addedItem,addedQty,false); + // reset values + addedItemObj.val(""); + addedQtyObj.val("1"); + } + else if (data.type == API_SUCCESS_UPDATE) { + // reset values + addedItemObj.val(""); + addedQtyObj.val("1"); + // easier than finding out on which item to update the QTY + refresh(); + } + } + }); + + $(this).blur(); + } + else addedItemObj.focus(); + } + /* Add an item (name : display name, amount : amount of items, checked : bool ) + to the category object categoryOBJ */ + function addItem(categoryOBJ, name, amount, checked) { + var isChecked,isCheckedName; + var read = $("#shopcategory").attr("data"); + if (checked) { isChecked="itemCheckedTD"; isCheckedName=" itemCheckedName";} + else { isChecked="itemUncheckedTD";isCheckedName=""; } + var itemVal=$('
  • ' + + ' '+ + ' '+ + '
    ' + + ' ' + + ' '+name+'
  • '); + if (read!=null && read.length) { + itemVal=$('
  • ' + + ' '+ + '
    ' + + ' '+amount+'' + + ' '+name+'
  • '); + } + $(itemVal).appendTo(categoryOBJ.find("#shopItems")); + // set events + if (read==null || read.length==0) { + $(itemVal).find( ".itemDelete" ).click(deleteItem); + $(itemVal).find( ".itemCheck" ).click(checkItemToggle); + $(itemVal).find( "#itemQtyValue" ).change(updateItem); + } + } + + // Toggle check/uncheck of an item. Only used by "add" button of category + function checkItemToggle() { + var itemNameOBJ=$(this).parent().find(".itemName"); + var itemUnchecked=$(this).hasClass("itemUncheckedTD"); + if (itemUnchecked) + { + $(this).removeClass("itemUncheckedTD"); + $(this).addClass("itemCheckedTD"); + itemNameOBJ.addClass("itemCheckedName"); + } else + { + $(this).removeClass("itemCheckedTD"); + $(this).addClass("itemUncheckedTD"); + itemNameOBJ.removeClass("itemCheckedName"); + } + + var itemOBJ = $(this).closest("#shopItemEntry"); + var itemName=itemOBJ.find(".itemName").html(); + var itemQty=itemOBJ.find("#itemQtyValue").val(); + + $.ajax({ + data: { + auth: "none", + function: "update", + item: itemName, + count: itemQty, + checked: itemUnchecked ? "true" : "false" + }, + success: function (data) { + if (data.type != API_SUCCESS_UPDATE) { + //refresh(); not required for toggling + } + } + }); + + } + + // Update item linked by itemObject + function updateItem() + { + var itemOBJ = $(this).closest("#shopItemEntry"); + var itemName=itemOBJ.find(".itemName").html(); + var itemQty=itemOBJ.find("#itemQtyValue").val(); + + if(itemQty>0) { + } + else { + itemQty = 1; + itemOBJ.find("#itemQtyValue").val(itemQty); + } + + $.ajax({ + data: { + auth: "none", + function: "save", + item: itemName, + count: itemQty + }, + success: function (data) { + if (data.type != API_SUCCESS_UPDATE) { + refresh(); + } + } + }); + + } + + // Delete all and refresh from DB + function refresh(){ + var addedItem="Error"; + var addedQty="Error"; + var addedchecked="Error"; + var addedcategory="Error"; + + //Remove all + $("#shopcategory").empty(); + categoryList=[]; + var defcategory=GetCategoryOBJ("Uncategorized"); + + $.ajax({ + data: { + auth: "none", + function: "listall" + }, + success: function (data) { + + //alert(data.type); + if (data.type == API_SUCCESS_LIST) { + for (var x = 0; x < data.items.length; x++) { + addedItem = data.items[x].itemTitle; + addedQty = data.items[x].itemCount; + checked = data.items[x].checked; + addedcategory = data.items[x].itemCategory; + if (addedcategory == null) { + addItem(defcategory, addedItem, addedQty, checked); + } else { + var categoryOBJ=GetCategoryOBJ(addedcategory); + addItem(categoryOBJ, addedItem, addedQty, checked); + } + } + //XXX $( "#shopcategory" ).accordion("refresh"); + } else + if (data.type == API_SUCCESS_LIST_EMPTY) { + alert ("Empty list"); + //TODO + } else { + alert ("Error : "+data.content); + } + } //success + + }); //ajax + + if($(this).blur) $(this).blur(); + } + + function removechecked(){ + var checked = $(".itemCheckedTD"); + if (checked.length>0) { + + var data = '['; + for (index = 0; index < checked.length; index++) { + data = data + '{"itemTitle":"'+$(checked[index]).parent().find(".itemName").html()+'"},'; + } + data = data.substr(0,data.length-1) + ']'; + + $.ajax({ + data: { + auth: "none", + function: "deleteMultiple", + jsonArray: data + }, + success: function (data) { + if (data.type == API_SUCCESS_DELETE) { + refresh(); + } + } + }); + + } + $(this).blur(); + } + + function share(){ + alert('Share the following URL: '+"\n"+$(this).attr("data")); + } + + function getItemValues(itemOBJ) { + var retVal = new Array(); + retVal['checked']=itemOBJ.find(".itemCheck").hasClass("itemCheckedTD"); + retVal['amount']=itemOBJ.find("#itemQtyValue").val(); + retVal['name']=itemOBJ.find(".itemName").html(); + retVal['category']=itemOBJ.closest("div[id*='cat_']").attr("id"); + retVal['category'] = retVal['category'].substr(4); + return retVal; + } + + // Item is dropped in another category.Only used by drop event of category + function categoryMove(event, ui, category) { + //alert("Dropped"); + var itemValues = new Array (); + itemValues = getItemValues(ui.draggable); + var catID = category.attr("id").substr(6); // Remove this + var newCategory = $("#cat_"+catID); + ui.draggable.remove(); + addItem(newCategory,itemValues['name'],itemValues['amount'],itemValues['checked']); + } + + // Doc ready functions + $(function() { + + // for test cat + $( ".itemDelete" ).click(deleteItem); + $( "#shopItems" ).sortable({ items: "li" }); + $( "#addItemButton" ).button().click(addItemClick); + $( ".itemCheck" ).click(checkItemToggle); + + // Main page + $( "#refresh" ).button().click(refresh); + $( "#checked" ).button().click(removechecked); + $( "#share" ).button().click(share); + $( "#itemQtyValue" ).change(updateItem); + + /* XXX + // Categories accordeon + $( "#shopcategory" ).accordion({ + heightStyle: content,collapsible: true + }); + */ + + if ($("#shopcategory").length) refresh(); + }); + diff --git a/lib/phpqrcode-git b/lib/phpqrcode-git new file mode 160000 index 0000000..863ffff --- /dev/null +++ b/lib/phpqrcode-git @@ -0,0 +1 @@ +Subproject commit 863ffffac4c9d22e522464e325cbcbadfbb26470 diff --git a/mysql_connector.php b/mysql_connector.php deleted file mode 100644 index 7894908..0000000 --- a/mysql_connector.php +++ /dev/null @@ -1,276 +0,0 @@ - server = $mysql_config['host']; - $this->database = $mysql_config['db']; - $this->username = $mysql_config['user']; - $this->password = $mysql_config['password']; - } - - function save($itemName, $itemCount) - { - if(empty($itemName)||empty($itemCount)) { - die(json_encode(array('type' => API_ERROR_MISSING_PARAMETER, 'content' => 'parameter missing'))); - } - //connect to db - $handler = new mysqli($this->server, $this->username, $this->password, $this->database); - - //check if connection successful - if ($handler->connect_error) { - die(json_encode(array('type' => API_ERROR_DATABASE_CONNECT, 'content' => $handler->connect_error))); - } - - //prepare query - $stmt = $handler->prepare("INSERT into ShoppingList(item,count) VALUES(?,?)"); - $stmt->bind_param('ss', $itemName, $itemCount); - - //execute query and check if successful - if ($stmt->execute()){ - $result = json_encode(array('type' => API_SUCCESS_SAVE, 'content' => $itemName.' saved')); - } else { - $result = json_encode(array('type' => API_ERROR_SAVE, 'content' => $stmt->error)); - } - - //close connection - $stmt->close(); - - //return result - return $result; - } - - function saveMultiple($jsonData) - { - if(empty($jsonData)) { - die(json_encode(array('type' => API_ERROR_MISSING_PARAMETER, 'content' => 'parameter missing for saveMultiple'))); - } - //connect to db - $handler = new mysqli($this->server, $this->username, $this->password, $this->database); - - //check if connection successful - if ($handler->connect_error) { - die(json_encode(array('type' => API_ERROR_DATABASE_CONNECT, 'content' => $handler->connect_error))); - } - //iterate over all items in json array - $array = json_decode( $jsonData, true ); - foreach($array as $item) - { - //prepare query - $stmt = $handler->prepare("INSERT into ShoppingList(item,count) VALUES(?,?)"); - $stmt->bind_param('ss', $item['itemTitle'], $item['itemCount']); - - //execute query and check if successful - if ($stmt->execute()){ - $result = json_encode(array('type' => API_SUCCESS_SAVE, 'content' => ' Multiple items saved')); - } else { - $result = json_encode(array('type' => API_ERROR_SAVE, 'content' => $stmt->error)); - } - } - - //close connection - $stmt->close(); - - //return result - return $result; - } - - function deleteMultiple($jsonData) - { - if(empty($jsonData)) { - die(json_encode(array('type' => API_ERROR_MISSING_PARAMETER, 'content' => 'parameter missing for deleteMultiple'))); - } - //connect to db - $handler = new mysqli($this->server, $this->username, $this->password, $this->database); - - //check if connection successful - if ($handler->connect_error) { - die(json_encode(array('type' => API_ERROR_DATABASE_CONNECT, 'content' => $handler->connect_error))); - } - //iterate over all items in json array - $array = json_decode( $jsonData, true ); - foreach($array as $item) - { - //prepare query - $stmt = $handler->prepare("DELETE from ShoppingList WHERE item = ?"); - $stmt->bind_param('s', $item['itemTitle']); - - //execute query and check if successful - if (!$stmt->execute()){ - $result = json_encode(array('type' => API_SUCCESS_DELETE, 'content' => ' Multiple items deleted')); - } else { - $result = json_encode(array('type' => API_ERROR_DELETE, 'content' => $stmt->error)); - } - } - - //close connection - $stmt->close(); - - //return result - return $result; - } - - function update($itemName, $itemCount) - { - if(empty($itemName)||empty($itemCount)){ - die(json_encode(array('type' => API_ERROR_MISSING_PARAMETER, 'content' => 'parameter missing'))); - } - //connect to db - $handler = new mysqli($this->server, $this->username, $this->password, $this->database); - - //check if connection successful - if ($handler->connect_error) { - die(json_encode(array('type' => API_ERROR_DATABASE_CONNECT, 'content' => $handler->connect_error))); - } - - //prepare query - $stmt = $handler->prepare("UPDATE ShoppingList SET count = ? WHERE item = ?"); - $stmt->bind_param('ss', $itemCount, $itemName); - - //execute query and check if successful - if ($stmt->execute()){ - $result = json_encode(array('type' => API_SUCCESS_UPDATE, 'content' => $itemName.' updated')); - } else { - $result = json_encode(array('type' => API_ERROR_UPDATE_, 'content' => $stmt->error)); - } - - //close connection - $stmt->close(); - - //return result - return $result; - } - - function delete($itemName) - { - if(empty($itemName)){ - die(json_encode(array('type' => API_ERROR_MISSING_PARAMETER, 'content' => 'parameter missing'))); - } - //connect to db - $handler = new mysqli($this->server, $this->username, $this->password, $this->database); - - //check if connection successful - if ($handler->connect_error) { - die(json_encode(array('type' => API_ERROR_DATABASE_CONNECT, 'content' => $handler->connect_error))); - } - - //prepare query - $stmt = $handler->prepare("DELETE FROM ShoppingList WHERE item = ?"); - $stmt->bind_param('s', $itemName); - - //execute query and check if successful - if ($stmt->execute()){ - $result = json_encode(array('type' => API_SUCCESS_DELETE, 'content' => $itemName.' deleted')); - } else { - $result = json_encode(array('type' => API_ERROR_DELETE, 'content' => $stmt->error)); - } - - //close connection - $stmt->close(); - - //return result - return $result; - } - - function exists($itemName) - { - if(empty($itemName)){ - die(json_encode(array('type' => API_ERROR_MISSING_PARAMETER, 'content' => 'parameter missing'))); - } - //connect to db - $handler = new mysqli($this->server, $this->username, $this->password, $this->database); - - //check if connection successful - if ($handler->connect_error) { - die(json_encode(array('type' => API_ERROR_DATABASE_CONNECT, 'content' => $handler->connect_error))); - } - - //prepare query - $stmt = $handler->prepare("SELECT item FROM ShoppingList WHERE item = ?"); - $stmt->bind_param('s', $itemName); - //execute query - $stmt->execute(); - - //bind the result - $stmt->store_result(); - if ($stmt->num_rows > 0){ - $itemExists = true; - } else { - $itemExists = false; - } - - //close connection - $stmt->close(); - return $itemExists; - - } - - function listall() - { - //connect to db - $handler = new mysqli($this->server, $this->username, $this->password, $this->database); - - //check if connection successful - if ($handler->connect_error) { - die(json_encode(array('type' => API_ERROR_DATABASE_CONNECT, 'content' => $handler->connect_error))); - } - - //prepare query - $stmt = $handler->prepare("SELECT item, count FROM ShoppingList ORDER BY item ASC"); - //execute query - $stmt->execute(); - $stmt->store_result(); - //bind the result - $stmt->bind_result($item_name, $item_count); - - //create array - $stack = array(); - - if($stmt->num_rows > 0) { - //put all rows into array - while ($stmt->fetch()) { - $listdata = array('itemTitle' => $item_name, 'itemCount' => $item_count, 'checked' => false); - array_push($stack, $listdata); - } - return json_encode(array('type' => API_SUCCESS_LIST, 'items' => $stack)); - } else { - return json_encode(array('type' => API_SUCCESS_LIST_EMPTY)); - } - - //close connection - $stmt->close(); - } - - function clear() - { - //connect to db - $handler = new mysqli($this->server, $this->username, $this->password, $this->database); - - //check if connection successful - if ($handler->connect_error) { - die(json_encode(array('type' => API_ERROR_DATABASE_CONNECT, 'content' => $handler->connect_error))); - } - - //prepare query - $stmt = $handler->prepare("TRUNCATE ShoppingList"); - - //execute query and check if successful - if ($stmt->execute()){ - $result = json_encode(array('type' => API_SUCCESS_CLEAR, 'content' => 'list cleared')); - } else { - $result = json_encode(array('type' => API_ERROR_CLEAR, 'content' => $stmt->error)); - } - - //close connection - $stmt->close(); - - //return result - return $result; - } - - } - -?> diff --git a/postinst.sh b/postinst.sh new file mode 100644 index 0000000..c7b386e --- /dev/null +++ b/postinst.sh @@ -0,0 +1,18 @@ +#!/bin/bash +echo -n "Press Enter to update the ShoppingList database" +read +BASE=`dirname $BASH_SOURCE` +HOST=`cat $BASE/config.php | grep host | cut -d\" -f2` +USER=`cat $BASE/config.php | grep user | cut -d\" -f2` +PASS=`cat $BASE/config.php | grep password | cut -d\" -f2` +DB=`cat $BASE/config.php | grep db | cut -d\" -f2` +echo "Starting the update. Please wait..." +SQLSTATUS=`mysql -h $HOST -u $USER -p$PASS $DB < $BASE/update.sql 2>&1` + +if [ $? -eq 0 ] +then + echo "The database has been updated successfully!" +else + echo "Error updating the database:" + echo "$SQLSTATUS" +fi diff --git a/sqlite_connector.php b/sqlite_connector.php deleted file mode 100644 index 6ba6cce..0000000 --- a/sqlite_connector.php +++ /dev/null @@ -1,136 +0,0 @@ -db = new SQLite3($dbfile, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE); - }catch(Exception $e){ - die(json_encode(array('type' => API_ERROR_DATABASE_CONNECT, 'content' => $e->getMessage()))); - } - $resultQuery = $this->db->query("SELECT COUNT(*) as count FROM sqlite_master WHERE type='table' AND name='itemlist'"); - $row = $resultQuery->fetchArray(); - if($row['count'] == 0){ - $this->db->exec("CREATE TABLE itemlist(ITEM TEXT PRIMARY KEY NOT NULL, COUNT INT NOT NULL);"); - } - } - - function __destructor(){ - $this->db->close(); - } - - function listall(){ - $resultQuery = $this->db->query("SELECT ITEM, COUNT FROM itemlist ORDER BY ITEM ASC;"); - $stack = array(); - if(!$resultQuery){ - return json_encode(array('type' => API_SUCCESS_LIST_EMPTY)); - } - while($item = $resultQuery->fetchArray()){ - $itemData = array( - 'itemTitle' => $item['ITEM'], - 'itemCount' => $item['COUNT'], - 'checked' => false - ); - array_push($stack, $itemData); - } - if(count($stack) == 0){ - return json_encode(array('type' => API_SUCCESS_LIST_EMPTY)); - }else{ - return json_encode(array('type' => API_SUCCESS_LIST, 'items' => $stack)); - } - } - - function exists($item){ - $resultQuery = $this->db->query("SELECT COUNT(*) as count FROM itemlist WHERE ITEM = '".$item."';"); - $row = $resultQuery->fetchArray(); - if($row['count'] > 0){ - return True; - }else{ - return False; - } - } - - function save($item, $count){ - $resultQuery = $this->db->query("INSERT INTO itemlist (ITEM, COUNT) VALUES('".$item."', ".$count.");"); - if($resultQuery){ - $result = json_encode(array('type' => API_SUCCESS_SAVE, 'content' => $item.' saved.')); - }else{ - $result = json_encode(array('type' => API_ERROR_SAVE, 'content' => 'Saving failed')); - } - return $result; - } - - function saveMultiple($jsonData){ - if(empty($jsonData)) { - die(json_encode(array('type' => API_ERROR_MISSING_PARAMETER, 'content' => 'parameter missing for saveMultiple'))); - } - //iterate over all items in json array - $array = json_decode( $jsonData, true ); - foreach($array as $item) - { - $resultQuery = $this->db->query("INSERT INTO itemlist (ITEM, COUNT) VALUES('".$item['itemTitle']."', ".$item['itemCount'].");"); - } - if($resultQuery){ - $result = json_encode(array('type' => API_SUCCESS_SAVE, 'content' => 'Multiple items saved')); - }else{ - $result = json_encode(array('type' => API_ERROR_SAVE, 'content' => 'Saving failed')); - } - return $result; - } - - function update($item, $count){ - $resultQuery = $this->db->query("UPDATE itemlist SET COUNT = ".$count." WHERE ITEM = '".$item."';"); - if($resultQuery){ - $result = json_encode(array('type' => API_SUCCESS_UPDATE, 'content' => $item.' updated.')); - }else{ - $result = json_encode(array('type' => API_ERROR_UPDATE_, 'content' => 'Updating failed')); - } - return $result; - } - - function deleteMultiple($jsonData){ - if(empty($jsonData)) { - die(json_encode(array('type' => API_ERROR_MISSING_PARAMETER, 'content' => 'parameter missing for deleteMultiple'))); - } - //iterate over all items in json array - $array = json_decode( $jsonData, true ); - foreach($array as $item) - { - $resultQuery = $this->db->query("DELETE FROM itemlist WHERE ITEM = '".$item['itemTitle']."';"); - } - if($resultQuery){ - $result = json_encode(array('type' => API_SUCCESS_DELETE, 'content' => 'Multiple items deleted')); - }else{ - $result = json_encode(array('type' => API_ERROR_DELETE, 'content' => 'Deleting failed')); - } - return $result; - } - - function delete($item){ - $resultQuery = $this->db->query("DELETE FROM itemlist WHERE ITEM = '".$item."';"); - if($resultQuery){ - $result = json_encode(array('type' => API_SUCCESS_DELETE, 'content' => $item.' deleted.')); - }else{ - $result = json_encode(array('type' => API_ERROR_DELETE, 'content' => 'Deleting failed')); - } - return $result; - } - - function clear(){ - $resultQuery = $this->db->query("DELETE FROM itemlist;"); - $this->db->exec("VACUUM;"); - if($resultQuery){ - $result = json_encode(array('type' => API_SUCCESS_CLEAR, 'content' => 'List cleared')); - }else{ - $result = json_encode(array('type' => API_ERROR_CLEAR, 'content' => 'Clearing failed')); - } - return $result; - } - - - } - -?> diff --git a/update.sql b/update.sql new file mode 100644 index 0000000..c3a1a7a --- /dev/null +++ b/update.sql @@ -0,0 +1 @@ +ALTER TABLE ShoppingList ADD COLUMN checked INT NOT NULL; diff --git a/view.php b/view.php new file mode 100644 index 0000000..8645f1c --- /dev/null +++ b/view.php @@ -0,0 +1,52 @@ + + + + + + Shopping List View + + + + + + + + + + +
    +
    +
    +

    Shopping List View

    + + +
    +
    + + +
    +
    +
    + +