From a409f994cd81a2038a030504ca98327f93c5e5cf Mon Sep 17 00:00:00 2001
From: Laurent Cozic <laurent@cozic.net>
Date: Tue, 27 Dec 2016 21:25:07 +0100
Subject: [PATCH] api sync

---
 .gitignore                                    |   3 +-
 QtClient/JoplinQtClient/JoplinQtClient.pro    |  12 +-
 QtClient/JoplinQtClient/application.cpp       |  49 +++++-
 QtClient/JoplinQtClient/application.h         |   7 +
 QtClient/JoplinQtClient/database.cpp          |   2 +-
 QtClient/JoplinQtClient/database.h            |   1 -
 QtClient/JoplinQtClient/models/folder.h       |   2 -
 QtClient/JoplinQtClient/models/item.cpp       |  29 ++--
 QtClient/JoplinQtClient/models/item.h         |   9 +-
 QtClient/JoplinQtClient/models/note.cpp       |  12 ++
 QtClient/JoplinQtClient/models/note.h         |   2 +
 QtClient/JoplinQtClient/schema.sql            |  18 +-
 QtClient/JoplinQtClient/settings.cpp          |   3 +
 QtClient/JoplinQtClient/settings.h            |  19 +++
 QtClient/JoplinQtClient/stable.h              |   9 +-
 QtClient/JoplinQtClient/synchronizer.cpp      |  83 ++++++++++
 QtClient/JoplinQtClient/synchronizer.h        |  32 ++++
 QtClient/JoplinQtClient/webapi.cpp            | 110 +++++++++++++
 QtClient/JoplinQtClient/webapi.h              |  55 +++++++
 app/data/uploads/.gitkeep                     |   0
 src/AppBundle/Controller/ApiController.php    |  73 +++++----
 .../Controller/FoldersController.php          |  14 +-
 src/AppBundle/Controller/NotesController.php  |  10 +-
 .../Controller/SessionsController.php         |   2 +-
 src/AppBundle/Controller/UsersController.php  | 155 ------------------
 src/AppBundle/Eloquent.php                    |   2 +-
 src/AppBundle/Model/BaseItem.php              |   2 +-
 src/AppBundle/Model/BaseModel.php             |   8 +-
 src/AppBundle/Model/Change.php                |  12 +-
 src/AppBundle/Model/FolderItem.php            |   2 +-
 src/AppBundle/Model/User.php                  |   2 +-
 .../Controller/DefaultControllerTest.php      |  18 --
 tests/Model/ChangeTest.php                    |  21 +++
 tests/setup.php                               |  20 ++-
 34 files changed, 528 insertions(+), 270 deletions(-)
 create mode 100755 QtClient/JoplinQtClient/settings.cpp
 create mode 100755 QtClient/JoplinQtClient/settings.h
 create mode 100755 QtClient/JoplinQtClient/synchronizer.cpp
 create mode 100755 QtClient/JoplinQtClient/synchronizer.h
 create mode 100755 QtClient/JoplinQtClient/webapi.cpp
 create mode 100755 QtClient/JoplinQtClient/webapi.h
 mode change 100644 => 100755 app/data/uploads/.gitkeep
 delete mode 100755 tests/AppBundle/Controller/DefaultControllerTest.php

diff --git a/.gitignore b/.gitignore
index f850c8dce7..4a5f419fcf 100755
--- a/.gitignore
+++ b/.gitignore
@@ -24,4 +24,5 @@ TODO.md
 QtClient/data/
 app/data/uploads/
 !app/data/uploads/.gitkeep
-sparse_test.php
\ No newline at end of file
+sparse_test.php
+INFO.md
\ No newline at end of file
diff --git a/QtClient/JoplinQtClient/JoplinQtClient.pro b/QtClient/JoplinQtClient/JoplinQtClient.pro
index c3832d6866..69ee231199 100755
--- a/QtClient/JoplinQtClient/JoplinQtClient.pro
+++ b/QtClient/JoplinQtClient/JoplinQtClient.pro
@@ -1,4 +1,4 @@
-QT += qml quick sql quickcontrols2
+QT += qml quick sql quickcontrols2 network
 
 CONFIG += c++11
 
@@ -16,7 +16,10 @@ SOURCES += \
     application.cpp \
     models/notecollection.cpp \
     services/notecache.cpp \
-    models/qmlnote.cpp
+    models/qmlnote.cpp \
+    webapi.cpp \
+    synchronizer.cpp \
+    settings.cpp
 
 RESOURCES += qml.qrc \
     database.qrc
@@ -44,7 +47,10 @@ HEADERS += \
     models/notecollection.h \
     services/notecache.h \
     sparsevector.hpp \
-    models/qmlnote.h
+    models/qmlnote.h \
+    webapi.h \
+    synchronizer.h \
+    settings.h
 
 DISTFILES +=
 
diff --git a/QtClient/JoplinQtClient/application.cpp b/QtClient/JoplinQtClient/application.cpp
index 4de7c75dc0..c95166519c 100755
--- a/QtClient/JoplinQtClient/application.cpp
+++ b/QtClient/JoplinQtClient/application.cpp
@@ -4,11 +4,20 @@
 #include "database.h"
 #include "models/foldermodel.h"
 #include "services/folderservice.h"
+#include "settings.h"
 
 using namespace jop;
 
-Application::Application(int &argc, char **argv) : QGuiApplication(argc, argv) {
-	db_ = Database("D:/Web/www/joplin/QtClient/data/notes.sqlite");
+Application::Application(int &argc, char **argv) : QGuiApplication(argc, argv), db_("D:/Web/www/joplin/QtClient/data/notes.sqlite"), api_("http://joplin.local"), synchronizer_(api_, db_) {
+	// This is linked to where the QSettings will be saved. In other words,
+	// if these values are changed, the settings will be reset and saved
+	// somewhere else.
+	QCoreApplication::setOrganizationName("Cozic");
+	QCoreApplication::setOrganizationDomain("cozic.net");
+	QCoreApplication::setApplicationName("Joplin");
+
+	Settings settings;
+
 	folderService_ = FolderService(db_);
 	folderModel_.setService(folderService_);
 
@@ -29,6 +38,31 @@ Application::Application(int &argc, char **argv) : QGuiApplication(argc, argv) {
 	connect(rootObject, SIGNAL(currentNoteChanged()), this, SLOT(view_currentNoteChanged()));
 
 	view_.show();
+
+	connect(&api_, SIGNAL(requestDone(const QJsonObject&, const QString&)), this, SLOT(api_requestDone(const QJsonObject&, const QString&)));
+
+	QString sessionId = settings.value("sessionId").toString();
+	if (sessionId == "") {
+		QUrlQuery postData;
+		postData.addQueryItem("email", "laurent@cozic.net");
+		postData.addQueryItem("password", "12345678");
+		postData.addQueryItem("client_id", "B6E12222B6E12222");
+		api_.post("sessions", QUrlQuery(), postData, "getSession");
+	} else {
+		afterSessionInitialization();
+	}
+}
+
+void Application::api_requestDone(const QJsonObject& response, const QString& tag) {
+	// TODO: handle errors
+
+	if (tag == "getSession") {
+		QString sessionId = response.value("id").toString();
+		Settings settings;
+		settings.setValue("sessionId", sessionId);
+		afterSessionInitialization();
+		return;
+	}
 }
 
 QString Application::selectedFolderId() const {
@@ -47,6 +81,17 @@ QString Application::selectedNoteId() const {
 	return noteModel_.data(modelIndex, NoteModel::IdRole).toString();
 }
 
+void Application::afterSessionInitialization() {
+	// TODO: rather than saving the session id, save the username/password and
+	// request a new session everytime on startup.
+
+	Settings settings;
+	QString sessionId = settings.value("sessionId").toString();
+	qDebug() << "Session:" << sessionId;
+	api_.setSessionId(sessionId);
+	synchronizer_.start();
+}
+
 void Application::view_currentFolderChanged() {
 	QString folderId = selectedFolderId();
 	noteCollection_ = NoteCollection(db_, folderId, "title ASC");
diff --git a/QtClient/JoplinQtClient/application.h b/QtClient/JoplinQtClient/application.h
index dade48ebc9..95555f96b5 100755
--- a/QtClient/JoplinQtClient/application.h
+++ b/QtClient/JoplinQtClient/application.h
@@ -11,6 +11,8 @@
 #include "services/notecache.h"
 #include "models/notemodel.h"
 #include "models/qmlnote.h"
+#include "webapi.h"
+#include "synchronizer.h"
 
 namespace jop {
 
@@ -35,11 +37,16 @@ private:
 	QString selectedNoteId() const;
 	NoteCache noteCache_;
 	QmlNote selectedQmlNote_;
+	WebApi api_;
+	Synchronizer synchronizer_;
+
+	void afterSessionInitialization();
 
 public slots:
 
 	void view_currentFolderChanged();
 	void view_currentNoteChanged();
+	void api_requestDone(const QJsonObject& response, const QString& tag);
 
 };
 
diff --git a/QtClient/JoplinQtClient/database.cpp b/QtClient/JoplinQtClient/database.cpp
index 2bce00ed2f..35ecfb68ae 100755
--- a/QtClient/JoplinQtClient/database.cpp
+++ b/QtClient/JoplinQtClient/database.cpp
@@ -5,7 +5,7 @@ using namespace jop;
 Database::Database(const QString &path) {
 	version_ = -1;
 
-	// QFile::remove(path);
+	//QFile::remove(path);
 
 	db_ = QSqlDatabase::addDatabase("QSQLITE");
 	db_.setDatabaseName(path);
diff --git a/QtClient/JoplinQtClient/database.h b/QtClient/JoplinQtClient/database.h
index 1461ef11ae..8ed8d54420 100755
--- a/QtClient/JoplinQtClient/database.h
+++ b/QtClient/JoplinQtClient/database.h
@@ -12,7 +12,6 @@ public:
 	Database(const QString& path);
 	Database();
 	QSqlQuery query(const QString& sql) const;
-	//QSqlQuery exec(const QString& sql, const QMap<QString, QVariant> &parameters);
 
 private:
 
diff --git a/QtClient/JoplinQtClient/models/folder.h b/QtClient/JoplinQtClient/models/folder.h
index 97e907555d..b478818eec 100755
--- a/QtClient/JoplinQtClient/models/folder.h
+++ b/QtClient/JoplinQtClient/models/folder.h
@@ -14,8 +14,6 @@ public:
 
 private:
 
-
-
 };
 
 }
diff --git a/QtClient/JoplinQtClient/models/item.cpp b/QtClient/JoplinQtClient/models/item.cpp
index 346391beb5..a611d2e7e1 100755
--- a/QtClient/JoplinQtClient/models/item.cpp
+++ b/QtClient/JoplinQtClient/models/item.cpp
@@ -5,16 +5,7 @@ using namespace jop;
 
 Item::Item() {
 	isPartial_ = true;
-}
-
-void Item::fromSqlQuery(const QSqlQuery &q) {
-	int i_id = q.record().indexOf("id");
-	int i_title = q.record().indexOf("title");
-	int i_created_time = q.record().indexOf("created_time");
-
-	id_ = q.value(i_id).toString();
-	title_ = q.value(i_title).toString();
-	createdTime_ = q.value(i_created_time).toInt();
+	synced_ = false;
 }
 
 QString Item::id() const {
@@ -29,6 +20,10 @@ int Item::createdTime() const {
 	return createdTime_;
 }
 
+int Item::updatedTime() const {
+	return updatedTime_;
+}
+
 void Item::setId(const QString& v) {
 	id_ = v;
 }
@@ -48,3 +43,17 @@ void Item::setIsPartial(bool v) {
 bool Item::isPartial() const {
 	return isPartial_;
 }
+
+QStringList Item::dbFields() {
+	QStringList output;
+	output << "id" << "title" << "created_time" << "updated_time" << "synced";
+	return output;
+}
+
+void Item::fromSqlQuery(const QSqlQuery &q) {
+	id_ = q.value(0).toString();
+	title_ = q.value(1).toString();
+	createdTime_ = q.value(2).toInt();
+	updatedTime_ = q.value(3).toInt();
+	synced_ = q.value(4).toBool();
+}
diff --git a/QtClient/JoplinQtClient/models/item.h b/QtClient/JoplinQtClient/models/item.h
index 4b23dc4bb7..33b515836e 100755
--- a/QtClient/JoplinQtClient/models/item.h
+++ b/QtClient/JoplinQtClient/models/item.h
@@ -14,7 +14,9 @@ public:
 	QString id() const;
 	QString title() const;
 	int createdTime() const;
+	int updatedTime() const;
 	bool isPartial() const;
+	static QStringList dbFields();
 
 	void setId(const QString &v);
 	void setTitle(const QString& v);
@@ -27,8 +29,11 @@ private:
 
 	QString id_;
 	QString title_;
-	int createdTime_;
-	bool isPartial_;
+	time_t createdTime_;
+	time_t updatedTime_;
+	bool synced_;
+
+	bool isPartial_;	
 
 };
 
diff --git a/QtClient/JoplinQtClient/models/note.cpp b/QtClient/JoplinQtClient/models/note.cpp
index 447e8e16eb..d94d91c21a 100755
--- a/QtClient/JoplinQtClient/models/note.cpp
+++ b/QtClient/JoplinQtClient/models/note.cpp
@@ -14,3 +14,15 @@ QString Note::body() const {
 void Note::setBody(const QString &v) {
 	body_ = v;
 }
+
+QStringList Note::dbFields() {
+	QStringList output = Item::dbFields();
+	output << "body";
+	return output;
+}
+
+void Note::fromSqlQuery(const QSqlQuery &q) {
+	Item::fromSqlQuery(q);
+	int idx = Item::dbFields().size();
+	body_ = q.value(idx).toString();
+}
diff --git a/QtClient/JoplinQtClient/models/note.h b/QtClient/JoplinQtClient/models/note.h
index 967257218f..9e692900da 100755
--- a/QtClient/JoplinQtClient/models/note.h
+++ b/QtClient/JoplinQtClient/models/note.h
@@ -13,6 +13,8 @@ public:
 	Note();
 	QString body() const;
 	void setBody(const QString& v);
+	static QStringList dbFields();
+	void fromSqlQuery(const QSqlQuery &q);
 
 private:
 
diff --git a/QtClient/JoplinQtClient/schema.sql b/QtClient/JoplinQtClient/schema.sql
index 035d354bfd..d88c659e61 100755
--- a/QtClient/JoplinQtClient/schema.sql
+++ b/QtClient/JoplinQtClient/schema.sql
@@ -2,7 +2,8 @@ CREATE TABLE folders (
 	id TEXT PRIMARY KEY,
 	title TEXT,
 	created_time INT,
-	updated_time INT
+	updated_time INT,
+	synced BOOLEAN DEFAULT 0
 );
 
 CREATE TABLE notes (
@@ -23,20 +24,23 @@ CREATE TABLE notes (
 	todo_completed INT,
 	source_application TEXT,
 	application_data TEXT,
-	`order` INT
+	`order` INT,
+	synced BOOLEAN DEFAULT 0
 );
 
 CREATE TABLE tags (
 	id TEXT PRIMARY KEY,
 	title TEXT,
 	created_time INT,
-	updated_time INT
+	updated_time INT,
+	synced BOOLEAN DEFAULT 0
 );
 
 CREATE TABLE note_tags (
 	id INTEGER PRIMARY KEY,
 	note_id TEXT,
-	tag_id TEXT
+	tag_id TEXT,
+	synced BOOLEAN DEFAULT 0
 );
 
 CREATE TABLE resources (
@@ -45,13 +49,15 @@ CREATE TABLE resources (
 	mime TEXT,
 	filename TEXT,
 	created_time INT,
-	updated_time INT
+	updated_time INT,
+	synced BOOLEAN DEFAULT 0
 );
 
 CREATE TABLE note_resources (
 	id INTEGER PRIMARY KEY,
 	note_id TEXT,
-	resource_id TEXT
+	resource_id TEXT,
+	synced BOOLEAN DEFAULT 0
 );
 
 CREATE TABLE version (
diff --git a/QtClient/JoplinQtClient/settings.cpp b/QtClient/JoplinQtClient/settings.cpp
new file mode 100755
index 0000000000..b6eb0013a0
--- /dev/null
+++ b/QtClient/JoplinQtClient/settings.cpp
@@ -0,0 +1,3 @@
+#include "settings.h"
+
+using namespace jop;
diff --git a/QtClient/JoplinQtClient/settings.h b/QtClient/JoplinQtClient/settings.h
new file mode 100755
index 0000000000..7e9e0ba702
--- /dev/null
+++ b/QtClient/JoplinQtClient/settings.h
@@ -0,0 +1,19 @@
+#ifndef SETTINGS_H
+#define SETTINGS_H
+
+#include <stable.h>
+
+namespace jop {
+
+class Settings : public QSettings {
+
+	Q_OBJECT
+
+//public:
+//	Settings();
+
+};
+
+}
+
+#endif // SETTINGS_H
diff --git a/QtClient/JoplinQtClient/stable.h b/QtClient/JoplinQtClient/stable.h
index afd68b3dec..16bdb1779f 100755
--- a/QtClient/JoplinQtClient/stable.h
+++ b/QtClient/JoplinQtClient/stable.h
@@ -10,8 +10,6 @@
 #include <QSqlDatabase>
 #include <QSqlQuery>
 #include <QSqlRecord>
-//#include <QUuid>
-//#include <vector>
 #include <QList>
 #include <QGuiApplication>
 #include <QQmlApplicationEngine>
@@ -20,6 +18,13 @@
 #include <QQmlContext>
 #include <QQmlProperty>
 #include <QSqlError>
+#include <QNetworkAccessManager>
+#include <QUrlQuery>
+#include <QNetworkRequest>
+#include <QNetworkReply>
+#include <QSettings>
+#include <QJsonObject>
+#include <QJsonParseError>
 
 #endif // __cplusplus
 
diff --git a/QtClient/JoplinQtClient/synchronizer.cpp b/QtClient/JoplinQtClient/synchronizer.cpp
new file mode 100755
index 0000000000..50bf66b5c5
--- /dev/null
+++ b/QtClient/JoplinQtClient/synchronizer.cpp
@@ -0,0 +1,83 @@
+#include "synchronizer.h"
+#include "models/folder.h"
+#include "models/note.h"
+
+using namespace jop;
+
+Synchronizer::Synchronizer(WebApi& api, Database &database) : api_(api), db_(database) {
+	qDebug() << api_.baseUrl();
+	connect(&api_, SIGNAL(requestDone(QJsonObject,QString)), this, SLOT(api_requestDone(QJsonObject,QString)));
+}
+
+void Synchronizer::start() {
+	qDebug() << "Starting synchronizer...";
+
+	QSqlQuery query;
+
+	std::vector<Folder> folders;
+	query = db_.query("SELECT " + Folder::dbFields().join(',') + " FROM folders WHERE synced = 0");
+	query.exec();
+
+	while (query.next()) {
+		Folder folder;
+		folder.fromSqlQuery(query);
+		folders.push_back(folder);
+	}
+
+	QList<Note> notes;
+	query = db_.query("SELECT " + Note::dbFields().join(',') + " FROM notes WHERE synced = 0");
+	query.exec();
+
+	while (query.next()) {
+		Note note;
+		note.fromSqlQuery(query);
+		notes << note;
+	}
+
+	for (size_t i = 0; i < folders.size(); i++) {
+		Folder folder = folders[i];
+		QUrlQuery data;
+		data.addQueryItem("id", folder.id());
+		data.addQueryItem("title", folder.title());
+		data.addQueryItem("created_time", QString::number(folder.createdTime()));
+		data.addQueryItem("updated_time", QString::number(folder.updatedTime()));
+		api_.put("folders/" + folder.id(), QUrlQuery(), data, "putFolder:" + folder.id());
+	}
+
+	for (size_t i = 0; i < notes.size(); i++) {
+		Note note = notes[i];
+		QUrlQuery data;
+		data.addQueryItem("id", note.id());
+		data.addQueryItem("title", note.title());
+		data.addQueryItem("body", note.body());
+		data.addQueryItem("created_time", QString::number(note.createdTime()));
+		data.addQueryItem("updated_time", QString::number(note.updatedTime()));
+		api_.put("notes/" + note.id(), QUrlQuery(), data, "putNote:" + note.id());
+	}
+}
+
+void Synchronizer::api_requestDone(const QJsonObject& response, const QString& tag) {
+	QSqlQuery query;
+	QStringList parts = tag.split(':');
+	QString action = tag;
+	QString id = "";
+
+	if (parts.size() == 2) {
+		action = parts[0];
+		id = parts[1];
+	}
+
+	if (action == "putFolder") {
+		// qDebug() << "Done folder" << id;
+//		query = db_.query("UPDATE folders SET synced = 1 WHERE id = ?");
+//		query.addBindValue(id);
+//		query.exec();
+	}
+
+	if (action == "putNote") {
+		// qDebug() << "Done note" << id;
+//		query = db_.query("UPDATE notes SET synced = 1 WHERE id = ?");
+//		query.addBindValue(id);
+//		query.exec();
+	}
+}
diff --git a/QtClient/JoplinQtClient/synchronizer.h b/QtClient/JoplinQtClient/synchronizer.h
new file mode 100755
index 0000000000..105843b5a0
--- /dev/null
+++ b/QtClient/JoplinQtClient/synchronizer.h
@@ -0,0 +1,32 @@
+#ifndef SYNCHRONIZER_H
+#define SYNCHRONIZER_H
+
+#include <stable.h>
+#include "webapi.h"
+#include "database.h"
+
+namespace jop {
+
+class Synchronizer : public QObject {
+
+	Q_OBJECT
+
+public:
+
+	Synchronizer(WebApi &api, Database& database);
+	void start();
+
+private:
+
+	WebApi& api_;
+	Database& db_;
+
+public slots:
+
+	void api_requestDone(const QJsonObject& response, const QString& tag);
+
+};
+
+}
+
+#endif // SYNCHRONIZER_H
diff --git a/QtClient/JoplinQtClient/webapi.cpp b/QtClient/JoplinQtClient/webapi.cpp
new file mode 100755
index 0000000000..73f680f483
--- /dev/null
+++ b/QtClient/JoplinQtClient/webapi.cpp
@@ -0,0 +1,110 @@
+#include <stable.h>
+
+#include "webapi.h"
+
+using namespace jop;
+
+WebApi::WebApi(const QString &baseUrl) {
+	baseUrl_ = baseUrl;
+	sessionId_ = "";
+	connect(&manager_, SIGNAL(finished(QNetworkReply*)), this, SLOT(request_finished(QNetworkReply*)));
+}
+
+QString WebApi::baseUrl() const {
+	return baseUrl_;
+}
+
+void WebApi::execRequest(QNetworkAccessManager::Operation method, const QString &path, const QUrlQuery &query, const QUrlQuery &data, const QString& tag) {
+	QueuedRequest r;
+	r.method = method;
+	r.path = path;
+	r.query = query;
+	r.data = data;
+	r.tag = tag;
+	queuedRequests_ << r;
+
+	processQueue();
+}
+
+void WebApi::post(const QString& path,const QUrlQuery& query, const QUrlQuery& data, const QString& tag) { execRequest(QNetworkAccessManager::PostOperation, path, query, data, tag); }
+void WebApi::get(const QString& path,const QUrlQuery& query, const QUrlQuery& data, const QString& tag) { execRequest(QNetworkAccessManager::GetOperation, path, query, data, tag); }
+void WebApi::put(const QString& path,const QUrlQuery& query, const QUrlQuery& data, const QString& tag) { execRequest(QNetworkAccessManager::PutOperation, path, query, data, tag); }
+//void patch(const QString& path,const QUrlQuery& query = QUrlQuery(), const QUrlQuery& data = QUrlQuery(), const QString& tag = "") { execRequest(QNetworkAccessManager::PatchOperation, query, data, tag); }
+
+void WebApi::setSessionId(const QString &v) {
+	sessionId_ = v;
+}
+
+void WebApi::processQueue() {
+	if (!queuedRequests_.size() || inProgressRequests_.size() >= 50) return;
+	QueuedRequest& r = queuedRequests_.takeFirst();
+
+	QString url = baseUrl_ + "/" + r.path;
+
+	QNetworkRequest* request = new QNetworkRequest(url);
+	request->setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
+
+	QNetworkReply* reply = NULL;
+
+	if (r.method == QNetworkAccessManager::GetOperation) {
+	   // TODO
+	   //manager->get(QNetworkRequest(QUrl("http://qt-project.org")));
+	}
+
+	if (r.method == QNetworkAccessManager::PostOperation) {
+		reply = manager_.post(*request, r.data.toString(QUrl::FullyEncoded).toUtf8());
+	}
+
+	if (r.method == QNetworkAccessManager::PutOperation) {
+		reply = manager_.put(*request, r.data.toString(QUrl::FullyEncoded).toUtf8());
+	}
+
+	if (!reply) {
+		qWarning() << "WebApi::processQueue(): reply object was not created - invalid request method";
+		return;
+	}
+
+	r.reply = reply;
+	r.request = request;
+	connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(request_error(QNetworkReply::NetworkError)));
+
+	QStringList cmd;
+	cmd << "curl";
+	if (r.method == QNetworkAccessManager::PutOperation) {
+		cmd << "-X" << "PUT";
+		cmd << "--data" << "'" + r.data.toString(QUrl::FullyEncoded) + "'";
+		cmd << url;
+	}
+
+	//qDebug().noquote() << cmd.join(" ");
+
+	inProgressRequests_.push_back(r);
+}
+
+void WebApi::request_finished(QNetworkReply *reply) {
+	QByteArray responseBodyBA = reply->readAll();
+	QJsonObject response;
+	QJsonParseError err;
+	QJsonDocument doc = QJsonDocument::fromJson(responseBodyBA, &err);
+	if (err.error != QJsonParseError::NoError) {
+		qWarning() << "Could not parse JSON:" << err.errorString();
+		qWarning().noquote() << QString(responseBodyBA);
+	} else {
+		response = doc.object();
+	}
+
+	for (size_t i = 0; i < inProgressRequests_.size(); i++) {
+		QueuedRequest r = inProgressRequests_[i];
+		if (r.reply == reply) {
+			inProgressRequests_.erase(inProgressRequests_.begin() + i);
+			emit requestDone(response, r.tag);
+			break;
+		}
+	}
+
+	processQueue();
+}
+
+void WebApi::request_error(QNetworkReply::NetworkError e) {
+	qDebug() << "Network error" << e;
+}
diff --git a/QtClient/JoplinQtClient/webapi.h b/QtClient/JoplinQtClient/webapi.h
new file mode 100755
index 0000000000..9bdbc1edf6
--- /dev/null
+++ b/QtClient/JoplinQtClient/webapi.h
@@ -0,0 +1,55 @@
+#ifndef WEBAPI_H
+#define WEBAPI_H
+
+#include <stable.h>
+
+namespace jop {
+
+class WebApi : public QObject {
+
+	Q_OBJECT
+
+public:
+
+	struct QueuedRequest {
+		QNetworkAccessManager::Operation method;
+		QString path;
+		QUrlQuery query;
+		QUrlQuery data;
+		QNetworkReply* reply;
+		QNetworkRequest* request;
+		QString tag;
+	};
+
+	WebApi(const QString& baseUrl);
+	QString baseUrl() const;
+	void execRequest(QNetworkAccessManager::Operation method, const QString& path,const QUrlQuery& query = QUrlQuery(), const QUrlQuery& data = QUrlQuery(), const QString& tag = "");
+	void post(const QString& path,const QUrlQuery& query = QUrlQuery(), const QUrlQuery& data = QUrlQuery(), const QString& tag = "");
+	void get(const QString& path,const QUrlQuery& query = QUrlQuery(), const QUrlQuery& data = QUrlQuery(), const QString& tag = "");
+	void put(const QString& path,const QUrlQuery& query = QUrlQuery(), const QUrlQuery& data = QUrlQuery(), const QString& tag = "");
+	//void patch(const QString& path,const QUrlQuery& query = QUrlQuery(), const QUrlQuery& data = QUrlQuery(), const QString& tag = "");
+	void setSessionId(const QString& v);
+
+private:
+
+	QString baseUrl_;
+	QList<QueuedRequest> queuedRequests_;
+	QList<QueuedRequest> inProgressRequests_;
+	void processQueue();
+	QString sessionId_;
+	QNetworkAccessManager manager_;
+
+public slots:
+
+	void request_finished(QNetworkReply* reply);
+	void request_error(QNetworkReply::NetworkError e);
+
+signals:
+
+	void requestDone(const QJsonObject& response, const QString& tag);
+
+};
+
+}
+
+#endif // WEBAPI_H
diff --git a/app/data/uploads/.gitkeep b/app/data/uploads/.gitkeep
old mode 100644
new mode 100755
diff --git a/src/AppBundle/Controller/ApiController.php b/src/AppBundle/Controller/ApiController.php
index 236dcb687a..8ed2f13a1a 100755
--- a/src/AppBundle/Controller/ApiController.php
+++ b/src/AppBundle/Controller/ApiController.php
@@ -66,7 +66,11 @@ abstract class ApiController extends Controller {
 	protected function session() {
 		if ($this->useTestUserAndSession) {
 			$session = Session::find(Session::unhex('BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB'));
-			if ($session) $session->delete();
+			if ($session) return $session;
+			// if ($session) {
+			// 	$ok = $session->delete();
+			// 	if (!$ok) throw new \Exception("Cannot delete session");
+			// }
 			$session = new Session();
 			$session->id = Session::unhex('BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB');
 			$session->owner_id = Session::unhex('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA');
@@ -75,7 +79,6 @@ abstract class ApiController extends Controller {
 			return $session;
 		}
 
-
 		if ($this->session) return $this->session;
 		$request = $this->container->get('request_stack')->getCurrentRequest();
 		$this->session = Session::find(BaseModel::unhex($request->query->get('session')));
@@ -142,37 +145,49 @@ abstract class ApiController extends Controller {
 	protected function patchParameters() {
 		$output = array();
 		$input = file_get_contents('php://input');
-		preg_match('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'], $matches);
-		$boundary = $matches[1];
-		$blocks = preg_split("/-+$boundary/", $input);
-		array_pop($blocks);
-		foreach ($blocks as $id => $block) {
-			if (empty($block)) continue;
 
-			// you'll have to var_dump $block to understand this and maybe replace \n or \r with a visibile char
+		// Two content types are supported:
+		//
+		// multipart/form-data; boundary=------------------------68670b1a1565e787
+		// application/x-www-form-urlencoded
 
-			// parse uploaded files
-			if (strpos($block, 'application/octet-stream') !== FALSE) {
-				// match "name", then everything after "stream" (optional) except for prepending newlines 
-				preg_match("/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s", $block, $matches);
-			} else {
-				// match "name" and optional value in between newline sequences
-				preg_match('/name=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $block, $matches);
-			}
-			if (!isset($matches[2])) {
-				// Regex above will not find anything if the parameter has not value. For example
-				// "parent_id" below:
+		if (!isset($_SERVER['CONTENT_TYPE']) || $_SERVER['CONTENT_TYPE'] == 'application/x-www-form-urlencoded') {
+			parse_str($input, $output);
+		} else {
+			if (!isset($_SERVER['CONTENT_TYPE'])) throw new \Exception("Cannot decode input data");
+			preg_match('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'], $matches);
+			if (!isset($matches[1])) throw new \Exception("Cannot decode input data");
+			$boundary = $matches[1];
+			$blocks = preg_split("/-+$boundary/", $input);
+			array_pop($blocks);
+			foreach ($blocks as $id => $block) {
+				if (empty($block)) continue;
 
-				// Content-Disposition: form-data; name="parent_id"
-				//
-				//
-				// Content-Disposition: form-data; name="id"
-				//
-				// 54ad197be333c98778c7d6f49506efcb
+				// you'll have to var_dump $block to understand this and maybe replace \n or \r with a visibile char
 
-				$output[$matches[1]] = '';
-			} else {
-				$output[$matches[1]] = $matches[2];
+				// parse uploaded files
+				if (strpos($block, 'application/octet-stream') !== FALSE) {
+					// match "name", then everything after "stream" (optional) except for prepending newlines 
+					preg_match("/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s", $block, $matches);
+				} else {
+					// match "name" and optional value in between newline sequences
+					preg_match('/name=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $block, $matches);
+				}
+				if (!isset($matches[2])) {
+					// Regex above will not find anything if the parameter has no value. For example
+					// "parent_id" below:
+
+					// Content-Disposition: form-data; name="parent_id"
+					//
+					//
+					// Content-Disposition: form-data; name="id"
+					//
+					// 54ad197be333c98778c7d6f49506efcb
+
+					$output[$matches[1]] = '';
+				} else {
+					$output[$matches[1]] = $matches[2];
+				}
 			}
 		}
 
diff --git a/src/AppBundle/Controller/FoldersController.php b/src/AppBundle/Controller/FoldersController.php
index 3395ba4e5c..669baf1416 100755
--- a/src/AppBundle/Controller/FoldersController.php
+++ b/src/AppBundle/Controller/FoldersController.php
@@ -36,18 +36,10 @@ class FoldersController extends ApiController {
 		}
 
 		if ($request->isMethod('PUT')) {
-			// TODO: call fromPublicArray() - handles unhex conversion
-
-			$data = $this->putParameters();
-			$isNew = !$folder;
-			if ($isNew) $folder = new Folder();
-			foreach ($data as $n => $v) {
-				if ($n == 'parent_id') $v = Folder::unhex($v);
-				$folder->{$n} = $v;
-			}
-			$folder->owner_id = $this->user()->id;
+			if (!$folder) $folder = new Folder();
+			$folder->fromPublicArray($this->putParameters());
 			$folder->id = Folder::unhex($id);
-			$folder->setIsNew($isNew);
+			$folder->owner_id = $this->user()->id;
 			$folder->save();
 			return static::successResponse($folder);
 		}
diff --git a/src/AppBundle/Controller/NotesController.php b/src/AppBundle/Controller/NotesController.php
index faa17a5f7b..189867dc7e 100755
--- a/src/AppBundle/Controller/NotesController.php
+++ b/src/AppBundle/Controller/NotesController.php
@@ -37,16 +37,10 @@ class NotesController extends ApiController {
 		}
 
 		if ($request->isMethod('PUT')) {
-			$data = $this->putParameters();
-			$isNew = !$note;
-			if ($isNew) $note = new Note();
-			foreach ($data as $n => $v) {
-				if ($n == 'parent_id') $v = Note::unhex($v);
-				$note->{$n} = $v;
-			}
+			if (!$note) $note = new Note();
+			$note->fromPublicArray($this->putParameters());
 			$note->id = Note::unhex($id);
 			$note->owner_id = $this->user()->id;
-			$note->setIsNew($isNew);
 			$note->save();
 			return static::successResponse($note);
 		}
diff --git a/src/AppBundle/Controller/SessionsController.php b/src/AppBundle/Controller/SessionsController.php
index f05b1b31c0..3ca30459e0 100755
--- a/src/AppBundle/Controller/SessionsController.php
+++ b/src/AppBundle/Controller/SessionsController.php
@@ -20,7 +20,7 @@ class SessionsController extends ApiController {
 	 * @Route("/sessions")
 	 */
 	public function allAction(Request $request) {
-		if ($request->isMethod('POST')) {
+		if ($request->isMethod('POST')) {			
 			$data = $request->request->all();
 			// Note: the login method will throw an exception in case of failure
 			$session = Session::login($data['email'], $data['password'], Session::unhex($data['client_id']));
diff --git a/src/AppBundle/Controller/UsersController.php b/src/AppBundle/Controller/UsersController.php
index a8aa9aa1cb..084c7469a7 100755
--- a/src/AppBundle/Controller/UsersController.php
+++ b/src/AppBundle/Controller/UsersController.php
@@ -25,161 +25,6 @@ class UsersController extends ApiController {
 	 * @Route("/users")
 	 */
 	public function allAction(Request $request) {
-
-
-
-		$source = "This is the first line.\n\nThis is the second line.";
-		$target1 = "This is the first line XXX.\n\nThis is the second line.";
-		$target2 = "This is the first line.\n\nThis is the second line YYY.";
-
-		$r = Diff::merge3($source, $target1, $target2);
-		var_dump($r);die();
-
-
-		// $dmp = new DiffMatchPatch();
-		// $patches = $dmp->patch_make($source, $target1);
-		// // @@ -1,11 +1,12 @@
-		// //  Th
-		// // -e
-		// // +at
-		// //   quick b
-		// // @@ -22,18 +22,17 @@
-		// //  jump
-		// // -s
-		// // +ed
-		// //   over
-		// // -the
-		// // +a
-		// //   laz
-		// $result = $dmp->patch_apply($patches, $target2);
-		// var_dump($result);
-		// die();
-
-		// $dmp = new DiffMatchPatch();
-
-		// $source = "This is the first line.\n\nThis is the second line.";
-		// $target1 = "This is the first line XXX.\n\nThis is the second line.";
-		// $target2 = "edsùfrklq lkzerlmk zemlkrmzlkerm lze.";
-
-
-		// $diff1 = $dmp->patch_make($source, $target1);
-		// $diff2 = $dmp->patch_make($source, $target2);
-
-		// //var_dump($dmp->patch_toText($diff1));
-		// // //var_dump($diff1[0]->patch_toText());
-
-		// $r = $dmp->patch_apply($diff1, $source);
-		// $r = $dmp->patch_apply($diff1, $target2);
-		// var_dump($r);die();
-
-		// $r = $dmp->patch_apply($diff2, $r[0]);
-
-		// var_dump($r);
-
-
-		// $dmp = new DiffMatchPatch();
-		// $patches = $dmp->patch_make($source, $target1);
-		// // @@ -1,11 +1,12 @@
-		// //  Th
-		// // -e
-		// // +at
-		// //   quick b
-		// // @@ -22,18 +22,17 @@
-		// //  jump
-		// // -s
-		// // +ed
-		// //   over
-		// // -the
-		// // +a
-		// //   laz
-		// $result = $dmp->patch_apply($patches, $target2);
-		// var_dump($result);
-
-		// die();
-
-		// $r = Diff::merge($source, $target1, $target2);
-		// var_dump($r);die();
-
-		// $diff1 = xdiff_string_diff($source, $target1);
-		// $diff2 = xdiff_string_diff($source, $target2);
-
-		// $errors = array();
-		// $t = xdiff_string_merge3($source , $target1, $target2, $errors);
-		// var_dump($errors);
-		// var_dump($t);die();
-
-		// var_dump($diff1);
-		// var_dump($diff2);
-
-		// $errors = array();
-		// $t = xdiff_string_patch($source, $diff1, XDIFF_PATCH_NORMAL, $errors);
-		// var_dump($t);
-		// var_dump($errors);
-
-		// $errors = array();
-		// $t = xdiff_string_patch($t, $diff2, XDIFF_PATCH_NORMAL, $errors);
-		// var_dump($t);
-		// var_dump($errors);
-
-
-
-		// var_dump($diff1);
-		// var_dump($diff2);
-
-		// $change = new Change();
-		// $change->user_id = BaseItem::unhex('204705F2E2E698036034FDC709840B80');
-		// $change->client_id = BaseItem::unhex('11111111111111111111111111111111');
-		// $change->item_type = BaseItem::enumId('type', 'note');
-		// $change->item_field = BaseItem::enumId('field', 'title');
-		// $change->item_id = BaseItem::unhex('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD');
-		// $change->delta = 'salut ca va';
-		// $change->save();
-
-
-		// $change = new Change();
-		// $change->user_id = BaseItem::unhex('204705F2E2E698036034FDC709840B80');
-		// $change->client_id = BaseItem::unhex('11111111111111111111111111111111');
-		// $change->item_type = BaseItem::enumId('type', 'note');
-		// $change->item_field = BaseItem::enumId('field', 'title');
-		// $change->item_id = BaseItem::unhex('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD');
-		// $change->createDelta('salut, ça va ? oui très bien');
-		// $change->save();
-
-		// $change = new Change();
-		// $change->user_id = BaseItem::unhex('204705F2E2E698036034FDC709840B80');
-		// $change->client_id = BaseItem::unhex('11111111111111111111111111111111');
-		// $change->item_type = BaseItem::enumId('type', 'note');
-		// $change->item_field = BaseItem::enumId('field', 'title');
-		// $change->item_id = BaseItem::unhex('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD');
-		// $change->createDelta('salut - oui très bien');
-		// $change->save();
-
-		// $change = new Change();
-		// $change->user_id = BaseItem::unhex('204705F2E2E698036034FDC709840B80');
-		// $change->client_id = BaseItem::unhex('11111111111111111111111111111111');
-		// $change->item_type = BaseItem::enumId('type', 'note');
-		// $change->item_field = BaseItem::enumId('field', 'title');
-		// $change->item_id = BaseItem::unhex('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD');
-		// $change->createDelta('salut, ça va ? oui bien');
-		// $change->save();
-
-
-
-		$d = Change::fullFieldText(BaseItem::unhex('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD'), BaseItem::enumId('field', 'title'));
-		var_dump($d);die();
-
-
-
-		die();
-
-
-		// $fineDiff = $this->get('app.fine_diff');
-		// $opcodes = $fineDiff->getDiffOpcodes('salut ca va', 'salut va?');
-		// var_dump($opcodes);
-		// $merged = $fineDiff->renderToTextFromOpcodes('salut ca va', $opcodes);
-		// var_dump($merged);
-		// die();
-
 		if ($request->isMethod('POST')) {
 			$user = new User();
 			$data = $request->request->all();
diff --git a/src/AppBundle/Eloquent.php b/src/AppBundle/Eloquent.php
index 130985ad0c..34d070bb05 100755
--- a/src/AppBundle/Eloquent.php
+++ b/src/AppBundle/Eloquent.php
@@ -14,7 +14,7 @@ class Eloquent {
 			'host'      => 'localhost',
 			'database'  => 'notes',
 			'username'  => 'root',
-			'password'  => 'pass',
+			'password'  => '',
 			'charset'   => 'utf8',
 			'collation' => 'utf8_unicode_ci',
 			'prefix'    => '',
diff --git a/src/AppBundle/Model/BaseItem.php b/src/AppBundle/Model/BaseItem.php
index 8177a07f70..a88d399fc7 100755
--- a/src/AppBundle/Model/BaseItem.php
+++ b/src/AppBundle/Model/BaseItem.php
@@ -8,7 +8,7 @@ class BaseItem extends BaseModel {
 	public $incrementing = false;
 
 	static protected $enums = array(
-		'type' => array('folder', 'note', 'todo', 'tag'),
+		'type' => array('folder', 'note', 'tag'),
 	);
 
 	public function itemTypeId() {
diff --git a/src/AppBundle/Model/BaseModel.php b/src/AppBundle/Model/BaseModel.php
index 10a901a28e..682eca421d 100755
--- a/src/AppBundle/Model/BaseModel.php
+++ b/src/AppBundle/Model/BaseModel.php
@@ -322,7 +322,7 @@ class BaseModel extends \Illuminate\Database\Eloquent\Model {
 		$this->updated_time = time(); // TODO: maybe only update if one of the fields, or if some of versioned data has changed
 		if ($isNew) $this->created_time = time();
 
-		parent::save($options);
+		$output = parent::save($options);
 
 		$this->isNew = null;
 
@@ -330,14 +330,18 @@ class BaseModel extends \Illuminate\Database\Eloquent\Model {
 			$this->recordChanges($isNew ? 'create' : 'update', $this->changedVersionedFieldValues);
 		}
 		$this->changedVersionedFieldValues = array();
+
+		return $output;
 	}
 
 	public function delete() {
-		parent::delete();
+		$output = parent::delete();
 
 		if (count($this->versionedFields)) {
 			$this->recordChanges('delete');
 		}
+
+		return $output;
 	}
 
 	protected function recordChanges($type, $versionedData = array()) {
diff --git a/src/AppBundle/Model/Change.php b/src/AppBundle/Model/Change.php
index a3c9e0cd68..a926674c5f 100755
--- a/src/AppBundle/Model/Change.php
+++ b/src/AppBundle/Model/Change.php
@@ -80,12 +80,14 @@ class Change extends BaseModel {
 		$revId = 0;
 		for ($i = 0; $i < count($changes); $i++) {
 			$change = $changes[$i];
-			$result = Diff::patch($output, $change->delta);
-			if (!count($result[1])) throw new \Exception('Unexpected result format for patch operation: ' . json_encode($result));
-			if (!$result[1][0]) {
-				// Could not patch the string. TODO: handle conflict
+			if (!empty($change->delta)) {
+				$result = Diff::patch($output, $change->delta);
+				if (!count($result[1])) throw new \Exception('Unexpected result format for patch operation: ' . json_encode($result));
+				if (!$result[1][0]) {
+					// Could not patch the string. TODO: handle conflict
+				}
+				$output = $result[0];
 			}
-			$output = $result[0];
 
 			$revId = $change->id;
 		}
diff --git a/src/AppBundle/Model/FolderItem.php b/src/AppBundle/Model/FolderItem.php
index 48267b6925..f743795802 100755
--- a/src/AppBundle/Model/FolderItem.php
+++ b/src/AppBundle/Model/FolderItem.php
@@ -8,7 +8,7 @@ class FolderItem extends BaseModel {
 	public $incrementing = false;
 
 	static protected $enums = array(
-		'type' => array('folder', 'note', 'todo'),
+		'type' => array('folder', 'note'),
 	);
 
 }
diff --git a/src/AppBundle/Model/User.php b/src/AppBundle/Model/User.php
index f2f83d02a9..f76524cbbe 100755
--- a/src/AppBundle/Model/User.php
+++ b/src/AppBundle/Model/User.php
@@ -29,7 +29,7 @@ class User extends BaseModel {
 		return $output;
 	}
 
-	public function byEmail($email) {
+	static public function byEmail($email) {
 		return self::where('email', '=', $email)->first();
 	}
 
diff --git a/tests/AppBundle/Controller/DefaultControllerTest.php b/tests/AppBundle/Controller/DefaultControllerTest.php
deleted file mode 100755
index 594803cce9..0000000000
--- a/tests/AppBundle/Controller/DefaultControllerTest.php
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php
-
-namespace Tests\AppBundle\Controller;
-
-use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
-
-class DefaultControllerTest extends WebTestCase
-{
-    public function testIndex()
-    {
-        $client = static::createClient();
-
-        $crawler = $client->request('GET', '/');
-
-        $this->assertEquals(200, $client->getResponse()->getStatusCode());
-        $this->assertContains('Welcome to Symfony', $crawler->filter('#container h1')->text());
-    }
-}
diff --git a/tests/Model/ChangeTest.php b/tests/Model/ChangeTest.php
index 58ab67ed9e..a5976169e0 100755
--- a/tests/Model/ChangeTest.php
+++ b/tests/Model/ChangeTest.php
@@ -48,6 +48,27 @@ class ChangeTest extends BaseTestCase {
 		$this->assertEquals($r, $text2);
 	}
 
+	public function testSame() {
+		$note = new Note();
+		$note->fromPublicArray(array('body' => 'test'));
+		$note->owner_id = $this->userId();
+		$note->save();
+
+		$noteId = $note->id;
+
+		$note = Note::find($noteId);
+
+		$this->assertEquals('test', $note->versionedFieldValue('body'));
+
+		$note->fromPublicArray(array('body' => 'test'));
+		$note->owner_id = $this->userId();
+		$note->save();
+
+		$note = Note::find($noteId);
+
+		$this->assertEquals('test', $note->versionedFieldValue('body'));
+	}
+
 	public function testDiff3Ways() {
 		// Scenario where two different clients change the same note at the same time.
 		//
diff --git a/tests/setup.php b/tests/setup.php
index 2adcca94c7..b236a81356 100755
--- a/tests/setup.php
+++ b/tests/setup.php
@@ -2,23 +2,29 @@
 
 require_once dirname(__FILE__) . '/BaseTestCase.php';
 
-$dbName = 'notes_test';
+$dbConfig = array(
+	'dbName' => 'notes_test',
+	'user' => 'root',
+	'password' => '',
+	'host' => '127.0.0.1',
+);
+
 $structureFile = dirname(dirname(__FILE__)) . '/structure.sql';
 
-$cmd = sprintf("mysql -u root -ppass -e 'DROP DATABASE IF EXISTS %s; CREATE DATABASE %s;'", $dbName, $dbName);
+$cmd = sprintf("mysql -u %s %s -h %s -e 'DROP DATABASE IF EXISTS %s; CREATE DATABASE %s;'", $dbConfig['user'], empty($dbConfig['password']) ? '' : '-p' . $dbConfig['password'], $dbConfig['host'], $dbConfig['dbName'], $dbConfig['dbName']);
 exec($cmd);
 
-$cmd = sprintf("mysql -u root -ppass %s < '%s'", $dbName, $structureFile);
+$cmd = sprintf("mysql -u %s %s -h %s %s < '%s'", $dbConfig['user'], empty($dbConfig['password']) ? '' : '-p' . $dbConfig['password'], $dbConfig['host'], $dbConfig['dbName'], $structureFile);
 exec($cmd);
 
 $capsule = new \Illuminate\Database\Capsule\Manager();
 
 $capsule->addConnection([
 	'driver'    => 'mysql',
-	'host'      => 'localhost',
-	'database'  => $dbName,
-	'username'  => 'root',
-	'password'  => 'pass',
+	'host'      => $dbConfig['host'],
+	'database'  => $dbConfig['dbName'],
+	'username'  => $dbConfig['user'],
+	'password'  => $dbConfig['password'],
 	'charset'   => 'utf8',
 	'collation' => 'utf8_unicode_ci',
 	'prefix'    => '',