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> ¶meters); 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' => '',