diff --git a/ReactNativeClient/android/app/build.gradle b/ReactNativeClient/android/app/build.gradle index ead16d8a9e..3ef0f8e1af 100644 --- a/ReactNativeClient/android/app/build.gradle +++ b/ReactNativeClient/android/app/build.gradle @@ -129,6 +129,7 @@ dependencies { compile fileTree(dir: "libs", include: ["*.jar"]) compile "com.android.support:appcompat-v7:23.0.1" compile "com.facebook.react:react-native:+" // From node_modules + compile project(':react-native-sqlite-storage') } // Run this once to be able to run the application with BUCK diff --git a/ReactNativeClient/android/app/src/main/java/com/awesomeproject/MainApplication.java b/ReactNativeClient/android/app/src/main/java/com/awesomeproject/MainApplication.java index 1478db49f6..30d8948fb0 100644 --- a/ReactNativeClient/android/app/src/main/java/com/awesomeproject/MainApplication.java +++ b/ReactNativeClient/android/app/src/main/java/com/awesomeproject/MainApplication.java @@ -7,6 +7,7 @@ import com.facebook.react.ReactNativeHost; import com.facebook.react.ReactPackage; import com.facebook.react.shell.MainReactPackage; import com.facebook.soloader.SoLoader; +import org.pgsqlite.SQLitePluginPackage; import java.util.Arrays; import java.util.List; @@ -22,6 +23,7 @@ public class MainApplication extends Application implements ReactApplication { @Override protected List getPackages() { return Arrays.asList( + new SQLitePluginPackage(), new MainReactPackage() ); } diff --git a/ReactNativeClient/android/settings.gradle b/ReactNativeClient/android/settings.gradle index 5f5645eeb3..3058e08311 100644 --- a/ReactNativeClient/android/settings.gradle +++ b/ReactNativeClient/android/settings.gradle @@ -1,3 +1,6 @@ rootProject.name = 'AwesomeProject' include ':app' + +include ':react-native-sqlite-storage' +project(':react-native-sqlite-storage').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-sqlite-storage/src/android') \ No newline at end of file diff --git a/ReactNativeClient/index.android.js b/ReactNativeClient/index.android.js index 2e1bb7c9f0..7df3d50913 100644 --- a/ReactNativeClient/index.android.js +++ b/ReactNativeClient/index.android.js @@ -5,6 +5,27 @@ import { connect } from 'react-redux' import { createStore } from 'redux'; import { Provider } from 'react-redux' +import { WebApi } from 'src/web-api.js' +import { Database } from 'src/database.js' + +import { Log } from 'src/log.js' + +let debugMode = true; + +let db = new Database(); +db.setDebugEnabled(debugMode); +db.open(); + + +// let test = { +// 'abcd' : 123, +// 'efgh' : 456, +// } + +// for (let [key, value] of test) { +// console.info(key, value); +// } + let defaultState = { 'myButtonLabel': 'clicko123456', 'counter': 0, @@ -108,90 +129,18 @@ class App extends Component { } -const queryString = require('query-string'); - -class Api { - - constructor(baseUrl, clientId) { - this.baseUrl_ = baseUrl; - this.clientId_ = clientId; - } - - makeRequest(method, path, query, data) { - let url = this.baseUrl_; - if (path) url += '/' + path; - if (query) url += '?' + queryString(query); - let options = {}; - options.method = method.toUpperCase(); - if (data) { - var formData = new FormData(); - for (var key in data) { - if (!data.hasOwnProperty(key)) continue; - formData.append(key, data[key]); - } - options.body = formData; - } - - return { - url: url, - options: options - }; - } - - exec(method, path, query, data) { - let that = this; - return new Promise(function(resolve, reject) { - let r = that.makeRequest(method, path, query, data); - - fetch(r.url, r.options) - .then(function(response) { - let responseClone = response.clone(); - return response.json() - .then(function(data) { - resolve(data); - }) - .catch(function(error) { - responseClone.text() - .done(function(text) { - reject('Cannot parse JSON: ' + text); - }); - }); - }) - .then(function(data) { - resolve(data); - }) - .catch(function(error) { - reject(error); - }); - }); - } - - get(path, query) { - return this.exec('GET', path, query); - } - - post(path, query, data) { - return this.exec('POST', path, query, data); - } - - delete(path, query) { - return this.exec('DELETE', path, query); - } - -} - -let api = new Api('http://192.168.1.2', 'A7D301DA7D301DA7D301DA7D301DA7D3'); -api.exec('POST', 'sessions', null, { - 'email': 'laurent@cozic.net', - 'password': '12345678', -}) -.then(function(data) { - console.info('GOT DATA:'); - console.info(data); -}) -.catch(function(error) { - console.warn('GOT ERROR:'); - console.warn(error); -}) +// let api = new WebApi('http://192.168.1.2', 'A7D301DA7D301DA7D301DA7D301DA7D3'); +// api.exec('POST', 'sessions', null, { +// 'email': 'laurent@cozic.net', +// 'password': '12345678', +// }) +// .then(function(data) { +// console.info('GOT DATA:'); +// console.info(data); +// }) +// .catch(function(error) { +// console.warn('GOT ERROR:'); +// console.warn(error); +// }) AppRegistry.registerComponent('AwesomeProject', () => App); \ No newline at end of file diff --git a/ReactNativeClient/package.json b/ReactNativeClient/package.json index 88e92c0a3f..1db83dc6a5 100644 --- a/ReactNativeClient/package.json +++ b/ReactNativeClient/package.json @@ -17,7 +17,8 @@ "react-test-renderer": "16.0.0-alpha.6", "redux": "3.6.0", "react-redux": "4.4.8", - "query-string": "4.3.4" + "query-string": "4.3.4", + "react-native-sqlite-storage": "3.3.*" }, "jest": { "preset": "react-native" diff --git a/ReactNativeClient/src/database.js b/ReactNativeClient/src/database.js new file mode 100644 index 0000000000..d7ff67861b --- /dev/null +++ b/ReactNativeClient/src/database.js @@ -0,0 +1,155 @@ +import SQLite from 'react-native-sqlite-storage'; +import { Log } from 'src/log.js'; + +const structureSql = ` +CREATE TABLE folders ( + id TEXT PRIMARY KEY, + parent_id TEXT NOT NULL DEFAULT "", + title TEXT NOT NULL DEFAULT "", + created_time INT NOT NULL DEFAULT 0, + updated_time INT NOT NULL DEFAULT 0 +); + +CREATE TABLE notes ( + id TEXT PRIMARY KEY, + parent_id TEXT NOT NULL DEFAULT "", + title TEXT NOT NULL DEFAULT "", + body TEXT NOT NULL DEFAULT "", + created_time INT NOT NULL DEFAULT 0, + updated_time INT NOT NULL DEFAULT 0, + latitude NUMERIC NOT NULL DEFAULT 0, + longitude NUMERIC NOT NULL DEFAULT 0, + altitude NUMERIC NOT NULL DEFAULT 0, + source TEXT NOT NULL DEFAULT "", + author TEXT NOT NULL DEFAULT "", + source_url TEXT NOT NULL DEFAULT "", + is_todo BOOLEAN NOT NULL DEFAULT 0, + todo_due INT NOT NULL DEFAULT "", + todo_completed INT NOT NULL DEFAULT "", + source_application TEXT NOT NULL DEFAULT "", + application_data TEXT NOT NULL DEFAULT "", + \`order\` INT NOT NULL DEFAULT 0 +); + +CREATE TABLE tags ( + id TEXT PRIMARY KEY, + title TEXT, + created_time INT, + updated_time INT +); + +CREATE TABLE note_tags ( + id INTEGER PRIMARY KEY, + note_id TEXT, + tag_id TEXT +); + +CREATE TABLE resources ( + id TEXT PRIMARY KEY, + title TEXT, + mime TEXT, + filename TEXT, + created_time INT, + updated_time INT +); + +CREATE TABLE note_resources ( + id INTEGER PRIMARY KEY, + note_id TEXT, + resource_id TEXT +); + +CREATE TABLE version ( + version INT +); + +CREATE TABLE changes ( + id INTEGER PRIMARY KEY, + \`type\` INT, + item_id TEXT, + item_type INT, + item_field TEXT +); + +CREATE TABLE settings ( + \`key\` TEXT PRIMARY KEY, + \`value\` TEXT, + \`type\` INT +); + +INSERT INTO version (version) VALUES (1); +`; + +class Database { + + constructor() {} + + setDebugEnabled(v) { + SQLite.DEBUG(v); + } + + open() { + this.db_ = SQLite.openDatabase({ name: 'joplin.sqlite', location: 'Documents' }, (db) => { + Log.info('Database was open successfully'); + }, (error) => { + Log.error('Cannot open database: ', error); + }); + + this.updateSchema(); + } + + sqlStringToLines(sql) { + let output = []; + let lines = sql.split("\n"); + let statement = ''; + for (var i = 0; i < lines.length; i++) { + var line = lines[i]; + if (line == '') continue; + if (line.substr(0, 2) == "--") continue; + statement += line; + if (line[line.length - 1] == ';') { + output.push(statement); + statement = ''; + } + } + return output; + } + + selectOne(sql, params = null) { + return new Promise((resolve, reject) => { + this.db_.executeSql(sql, params, (r) => { + resolve(r.rows.length ? r.rows.item(0) : null); + }, (error) => { + reject(error); + }); + }); + } + + updateSchema() { + Log.info('Checking for database schema update...'); + + this.selectOne('SELECT * FROM version LIMIT 1').then((row) => { + // TODO: version update logic + }).catch((error) => { + // Assume that error was: + // { message: 'no such table: version (code 1): , while compiling: SELECT * FROM version', code: 0 } + // which means the database is empty and the tables need to be created. + + Log.info('Database is new - creating the schema...'); + + let statements = this.sqlStringToLines(structureSql) + this.db_.transaction((tx) => { + for (let i = 0; i < statements.length; i++) { + tx.executeSql(statements[i]); + } + }, (error) => { + Log.error('Could not create database schema:', error); + }, () => { + Log.info('Database schema created successfully'); + }); + }); + } + +} + +export { Database }; \ No newline at end of file diff --git a/ReactNativeClient/src/log.js b/ReactNativeClient/src/log.js new file mode 100644 index 0000000000..61857081d9 --- /dev/null +++ b/ReactNativeClient/src/log.js @@ -0,0 +1,19 @@ +// Custom wrapper for `console` to allow for custom logging (to file, etc.) if needed. + +class Log { + + static info(...o) { + console.info(...o); + } + + static warn(...o) { + console.info(...o); + } + + static error(...o) { + console.info(...o); + } + +} + +export { Log }; \ No newline at end of file diff --git a/ReactNativeClient/src/package.json b/ReactNativeClient/src/package.json new file mode 100644 index 0000000000..573456ed06 --- /dev/null +++ b/ReactNativeClient/src/package.json @@ -0,0 +1 @@ +{ "name": "src" } \ No newline at end of file diff --git a/ReactNativeClient/src/web-api.js b/ReactNativeClient/src/web-api.js new file mode 100644 index 0000000000..1ddeca3ef6 --- /dev/null +++ b/ReactNativeClient/src/web-api.js @@ -0,0 +1,70 @@ +const queryString = require('query-string'); + +class WebApi { + + constructor(baseUrl, clientId) { + this.baseUrl_ = baseUrl; + this.clientId_ = clientId; + } + + makeRequest(method, path, query, data) { + let url = this.baseUrl_; + if (path) url += '/' + path; + if (query) url += '?' + queryString(query); + let options = {}; + options.method = method.toUpperCase(); + if (data) { + var formData = new FormData(); + for (var key in data) { + if (!data.hasOwnProperty(key)) continue; + formData.append(key, data[key]); + } + options.body = formData; + } + + return { + url: url, + options: options + }; + } + + exec(method, path, query, data) { + let that = this; + return new Promise(function(resolve, reject) { + let r = that.makeRequest(method, path, query, data); + + fetch(r.url, r.options).then(function(response) { + let responseClone = response.clone(); + return response.json().then(function(data) { + resolve(data); + }) + .catch(function(error) { + responseClone.text().then(function(text) { + reject('Cannot parse JSON: ' + text); + }); + }); + }) + .then(function(data) { + resolve(data); + }) + .catch(function(error) { + reject(error); + }); + }); + } + + get(path, query) { + return this.exec('GET', path, query); + } + + post(path, query, data) { + return this.exec('POST', path, query, data); + } + + delete(path, query) { + return this.exec('DELETE', path, query); + } + +} + +export { WebApi }; \ No newline at end of file diff --git a/joplin.sublime-project b/joplin.sublime-project index 032496ef4b..7beb044436 100755 --- a/joplin.sublime-project +++ b/joplin.sublime-project @@ -5,33 +5,38 @@ "path": ".", "folder_exclude_patterns": [ "var", - "vendor", - "QtClient/build-JoplinQtClient-Visual_C_32_bits-Debug", - "QtClient/data/resources", - "app/data/uploads", - "ReactNativeClient/node_modules", - "ReactNativeClient/android", - "ReactNativeClient/ios", + "vendor", + "QtClient/build-JoplinQtClient-Visual_C_32_bits-Debug", + "QtClient/data/resources", + "app/data/uploads", + "ReactNativeClient/node_modules", + "ReactNativeClient/android/app/build", + "ReactNativeClient/android/build", + "ReactNativeClient/android/.idea", + "ReactNativeClient/android/.gradle", + "ReactNativeClient/android/local.properties", + "ReactNativeClient/ios" ], - "file_exclude_patterns": [ - "*.pro.user", - "*.pro.user.*" - ] + "file_exclude_patterns": [ + "*.pro.user", + "*.pro.user.*", + "*.iml" + ] } ], - "build_systems": - [ - { - "name": "Build evernote-import", - "shell_cmd": "D:\\Programmes\\cygwin\\bin\\bash.exe --login D:\\Web\\www\\joplin\\QtClient\\evernote-import\\build.sh" - }, - { - "name": "Qt Creator - Build and run", - "shell_cmd": "D:\\NonPortableApps\\AutoHotkey\\AutoHotkey.exe D:\\Docs\\PROGS\\AutoHotKey\\QtRun\\QtRun.ahk" - }, - { - "name": "Build QtClient CLI - Linux", - "shell_cmd": "/home/laurent/src/notes/QtClient/JoplinQtClient/build.sh" - } - ] + "build_systems": + [ + { + "name": "Build evernote-import", + "shell_cmd": "D:\\Programmes\\cygwin\\bin\\bash.exe --login D:\\Web\\www\\joplin\\QtClient\\evernote-import\\build.sh" + }, + { + "name": "Qt Creator - Build and run", + "shell_cmd": "D:\\NonPortableApps\\AutoHotkey\\AutoHotkey.exe D:\\Docs\\PROGS\\AutoHotKey\\QtRun\\QtRun.ahk" + }, + { + "name": "Build QtClient CLI - Linux", + "shell_cmd": "/home/laurent/src/notes/QtClient/JoplinQtClient/build.sh" + } + ] } diff --git a/src/AppBundle/Controller/ApiController.php b/src/AppBundle/Controller/ApiController.php index 597797a457..15f99ea7b4 100755 --- a/src/AppBundle/Controller/ApiController.php +++ b/src/AppBundle/Controller/ApiController.php @@ -32,12 +32,24 @@ abstract class ApiController extends Controller { $r->send(); echo "\n"; } else { - $msg = array(); - $msg[] = 'Exception: ' . $e->getMessage() . ' at ' . $e->getFile() . ':' . $e->getLine(); - $msg[] = ''; - $msg[] = $e->getTraceAsString(); - echo implode("\n", $msg); + $r = array( + 'error' => $e->getMessage(), + 'code' => 0, + 'type' => 'Exception', + //'trace' => $e->getTraceAsString(), + ); + $response = new JsonResponse($r); + $response->setStatusCode(500); + $response->send(); echo "\n"; + + + // $msg = array(); + // $msg[] = 'Exception: ' . $e->getMessage() . ' at ' . $e->getFile() . ':' . $e->getLine(); + // $msg[] = ''; + // $msg[] = $e->getTraceAsString(); + // echo implode("\n", $msg); + // echo "\n"; } }); diff --git a/src/AppBundle/Model/Session.php b/src/AppBundle/Model/Session.php index d3702799a1..0511dc389d 100755 --- a/src/AppBundle/Model/Session.php +++ b/src/AppBundle/Model/Session.php @@ -29,6 +29,8 @@ class Session extends BaseModel { $ok = self::verifyPassword($password, $user->password); if (!$ok) throw new AuthException(); + if (!$clientId) throw new \Exception('clientId is required'); + $session = new Session(); $session->owner_id = $user->id; $session->client_id = $clientId; diff --git a/web/app.php b/web/app.php index aa11a6fe3f..e70cb69ec0 100755 --- a/web/app.php +++ b/web/app.php @@ -21,7 +21,21 @@ try { $response->send(); $kernel->terminate($request, $response); } catch(\Exception $e) { - header('HTTP/1.1 500 Internal Server Error'); - echo $e->getMessage() . "\n"; - echo $e->getTraceAsString(); + // Separate exception handling for anything that could not be caught in ApiController, for + // example if the route doesn't exist. + $class = get_class($e); + $errorType = explode("\\", $class); + $errorType = $errorType[count($errorType) - 1]; + $response = array( + 'error' => $e->getMessage(), + 'code' => $e->getCode(), + 'type' => $errorType, + ); + if ($errorType == 'NotFoundHttpException') { + header('HTTP/1.1 404 Not found'); + } else { + header('HTTP/1.1 500 Internal Server Error'); + } + + die(json_encode($response) . "\n"); } \ No newline at end of file