You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	api sync
This commit is contained in:
		
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -24,4 +24,5 @@ TODO.md | ||||
| QtClient/data/ | ||||
| app/data/uploads/ | ||||
| !app/data/uploads/.gitkeep | ||||
| sparse_test.php | ||||
| sparse_test.php | ||||
| INFO.md | ||||
| @@ -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 += | ||||
|  | ||||
|   | ||||
| @@ -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"); | ||||
|   | ||||
| @@ -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); | ||||
|  | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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> ¶meters); | ||||
|  | ||||
| private: | ||||
|  | ||||
|   | ||||
| @@ -14,8 +14,6 @@ public: | ||||
|  | ||||
| private: | ||||
|  | ||||
|  | ||||
|  | ||||
| }; | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -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(); | ||||
| } | ||||
|   | ||||
| @@ -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_;	 | ||||
|  | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -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(); | ||||
| } | ||||
|   | ||||
| @@ -13,6 +13,8 @@ public: | ||||
| 	Note(); | ||||
| 	QString body() const; | ||||
| 	void setBody(const QString& v); | ||||
| 	static QStringList dbFields(); | ||||
| 	void fromSqlQuery(const QSqlQuery &q); | ||||
|  | ||||
| private: | ||||
|  | ||||
|   | ||||
| @@ -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 ( | ||||
|   | ||||
							
								
								
									
										3
									
								
								QtClient/JoplinQtClient/settings.cpp
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										3
									
								
								QtClient/JoplinQtClient/settings.cpp
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| #include "settings.h" | ||||
|  | ||||
| using namespace jop; | ||||
							
								
								
									
										19
									
								
								QtClient/JoplinQtClient/settings.h
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										19
									
								
								QtClient/JoplinQtClient/settings.h
									
									
									
									
									
										Executable file
									
								
							| @@ -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 | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
							
								
								
									
										83
									
								
								QtClient/JoplinQtClient/synchronizer.cpp
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										83
									
								
								QtClient/JoplinQtClient/synchronizer.cpp
									
									
									
									
									
										Executable file
									
								
							| @@ -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(); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										32
									
								
								QtClient/JoplinQtClient/synchronizer.h
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										32
									
								
								QtClient/JoplinQtClient/synchronizer.h
									
									
									
									
									
										Executable file
									
								
							| @@ -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 | ||||
							
								
								
									
										110
									
								
								QtClient/JoplinQtClient/webapi.cpp
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										110
									
								
								QtClient/JoplinQtClient/webapi.cpp
									
									
									
									
									
										Executable file
									
								
							| @@ -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; | ||||
| } | ||||
							
								
								
									
										55
									
								
								QtClient/JoplinQtClient/webapi.h
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										55
									
								
								QtClient/JoplinQtClient/webapi.h
									
									
									
									
									
										Executable file
									
								
							| @@ -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 | ||||
							
								
								
									
										0
									
								
								app/data/uploads/.gitkeep
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								app/data/uploads/.gitkeep
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -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]; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
|   | ||||
| @@ -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); | ||||
| 		} | ||||
|   | ||||
| @@ -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); | ||||
| 		} | ||||
|   | ||||
| @@ -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'])); | ||||
|   | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -14,7 +14,7 @@ class Eloquent { | ||||
| 			'host'      => 'localhost', | ||||
| 			'database'  => 'notes', | ||||
| 			'username'  => 'root', | ||||
| 			'password'  => 'pass', | ||||
| 			'password'  => '', | ||||
| 			'charset'   => 'utf8', | ||||
| 			'collation' => 'utf8_unicode_ci', | ||||
| 			'prefix'    => '', | ||||
|   | ||||
| @@ -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() { | ||||
|   | ||||
| @@ -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()) { | ||||
|   | ||||
| @@ -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; | ||||
| 		} | ||||
|   | ||||
| @@ -8,7 +8,7 @@ class FolderItem extends BaseModel { | ||||
| 	public $incrementing = false; | ||||
|  | ||||
| 	static protected $enums = array( | ||||
| 		'type' => array('folder', 'note', 'todo'), | ||||
| 		'type' => array('folder', 'note'), | ||||
| 	); | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -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(); | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -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()); | ||||
|     } | ||||
| } | ||||
| @@ -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. | ||||
| 		// | ||||
|   | ||||
| @@ -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'    => '', | ||||
|   | ||||
		Reference in New Issue
	
	Block a user