1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-04-20 11:28:40 +02:00

686 lines
21 KiB
JavaScript
Raw Normal View History

2018-03-09 17:49:35 +00:00
const BaseModel = require("lib/BaseModel.js");
const { Database } = require("lib/database.js");
const Setting = require("lib/models/Setting.js");
const JoplinError = require("lib/JoplinError.js");
const { time } = require("lib/time-utils.js");
const { sprintf } = require("sprintf-js");
const { _ } = require("lib/locale.js");
const moment = require("moment");
2017-06-15 19:18:48 +01:00
class BaseItem extends BaseModel {
static useUuid() {
return true;
}
static encryptionSupported() {
return true;
}
2017-07-06 19:48:17 +00:00
static loadClass(className, classRef) {
for (let i = 0; i < BaseItem.syncItemDefinitions_.length; i++) {
if (BaseItem.syncItemDefinitions_[i].className == className) {
BaseItem.syncItemDefinitions_[i].classRef = classRef;
return;
}
}
2018-03-09 17:49:35 +00:00
throw new Error("Invalid class name: " + className);
2017-07-06 19:48:17 +00:00
}
2017-07-02 13:02:07 +01:00
// Need to dynamically load the classes like this to avoid circular dependencies
static getClass(name) {
2017-07-06 19:48:17 +00:00
for (let i = 0; i < BaseItem.syncItemDefinitions_.length; i++) {
if (BaseItem.syncItemDefinitions_[i].className == name) {
const classRef = BaseItem.syncItemDefinitions_[i].classRef;
2018-03-09 17:49:35 +00:00
if (!classRef) throw new Error("Class has not been loaded: " + name);
2017-07-06 19:48:17 +00:00
return BaseItem.syncItemDefinitions_[i].classRef;
}
}
2018-03-09 17:49:35 +00:00
throw new Error("Invalid class name: " + name);
2017-07-10 18:09:58 +00:00
}
2017-08-20 16:29:18 +02:00
static getClassByItemType(itemType) {
for (let i = 0; i < BaseItem.syncItemDefinitions_.length; i++) {
if (BaseItem.syncItemDefinitions_[i].type == itemType) {
return BaseItem.syncItemDefinitions_[i].classRef;
}
}
2018-03-09 17:49:35 +00:00
throw new Error("Invalid item type: " + itemType);
2017-08-20 16:29:18 +02:00
}
2017-07-16 17:06:05 +01:00
static async syncedCount(syncTarget) {
const ItemClass = this.itemClass(this.modelType());
const itemType = ItemClass.modelType();
// The fact that we don't check if the item_id still exist in the corresponding item table, means
// that the returned number might be innaccurate (for example if a sync operation was cancelled)
2018-03-09 17:49:35 +00:00
const sql = "SELECT count(*) as total FROM sync_items WHERE sync_target = ? AND item_type = ?";
const r = await this.db().selectOne(sql, [syncTarget, itemType]);
2017-07-16 17:06:05 +01:00
return r.total;
2017-07-02 13:02:07 +01:00
}
2017-06-20 19:18:19 +00:00
static systemPath(itemOrId) {
2018-03-09 17:49:35 +00:00
if (typeof itemOrId === "string") return itemOrId + ".md";
return itemOrId.id + ".md";
2017-06-15 19:18:48 +01:00
}
2017-07-18 21:03:07 +01:00
static isSystemPath(path) {
// 1b175bb38bba47baac22b0b47f778113.md
if (!path || !path.length) return false;
2018-03-09 17:49:35 +00:00
let p = path.split("/");
2017-07-18 21:03:07 +01:00
p = p[p.length - 1];
2018-03-09 17:49:35 +00:00
p = p.split(".");
2017-07-18 21:03:07 +01:00
if (p.length != 2) return false;
2018-03-09 17:49:35 +00:00
return p[0].length == 32 && p[1] == "md";
2017-07-18 21:03:07 +01:00
}
2017-06-17 19:40:08 +01:00
static itemClass(item) {
2018-03-09 17:49:35 +00:00
if (!item) throw new Error("Item cannot be null");
2017-06-18 23:06:10 +01:00
2018-03-09 17:49:35 +00:00
if (typeof item === "object") {
if (!("type_" in item)) throw new Error("Item does not have a type_ property");
2017-07-02 13:02:07 +01:00
return this.itemClass(item.type_);
2017-06-18 23:06:10 +01:00
} else {
2017-07-03 23:08:14 +01:00
for (let i = 0; i < BaseItem.syncItemDefinitions_.length; i++) {
let d = BaseItem.syncItemDefinitions_[i];
if (Number(item) == d.type) return this.getClass(d.className);
}
2018-03-09 17:49:35 +00:00
throw new Error("Unknown type: " + item);
2017-06-18 23:06:10 +01:00
}
2017-06-17 19:40:08 +01:00
}
2017-07-01 11:30:50 +01:00
// Returns the IDs of the items that have been synced at least once
2017-07-23 15:11:44 +01:00
static async syncedItemIds(syncTarget) {
2018-03-09 17:49:35 +00:00
if (!syncTarget) throw new Error("No syncTarget specified");
let temp = await this.db().selectAll("SELECT item_id FROM sync_items WHERE sync_time > 0 AND sync_target = ?", [syncTarget]);
2017-07-23 15:11:44 +01:00
let output = [];
for (let i = 0; i < temp.length; i++) {
output.push(temp[i].item_id);
}
return output;
2017-07-01 11:30:50 +01:00
}
2017-06-15 19:18:48 +01:00
static pathToId(path) {
2018-03-09 17:49:35 +00:00
let p = path.split("/");
let s = p[p.length - 1].split(".");
2017-06-15 19:18:48 +01:00
return s[0];
}
static loadItemByPath(path) {
2017-07-02 11:34:07 +01:00
return this.loadItemById(this.pathToId(path));
}
2017-07-02 19:38:34 +01:00
static async loadItemById(id) {
2017-07-03 23:08:14 +01:00
let classes = this.syncItemClassNames();
2017-07-02 19:38:34 +01:00
for (let i = 0; i < classes.length; i++) {
let item = await this.getClass(classes[i]).load(id);
2017-06-15 19:18:48 +01:00
if (item) return item;
2017-07-02 19:38:34 +01:00
}
return null;
2017-06-15 19:18:48 +01:00
}
2017-06-25 08:52:25 +01:00
static loadItemByField(itemType, field, value) {
let ItemClass = this.itemClass(itemType);
return ItemClass.loadByField(field, value);
}
static loadItem(itemType, id) {
let ItemClass = this.itemClass(itemType);
return ItemClass.load(id);
}
static deleteItem(itemType, id) {
let ItemClass = this.itemClass(itemType);
return ItemClass.delete(id);
}
2017-07-03 20:50:45 +01:00
static async delete(id, options = null) {
2017-07-11 18:17:23 +00:00
return this.batchDelete([id], options);
}
static async batchDelete(ids, options = null) {
2017-07-03 23:08:14 +01:00
let trackDeleted = true;
2017-07-03 20:50:45 +01:00
if (options && options.trackDeleted !== null && options.trackDeleted !== undefined) trackDeleted = options.trackDeleted;
2017-07-18 19:27:10 +00:00
// Don't create a deleted_items entry when conflicted notes are deleted
// since no other client have (or should have) them.
let conflictNoteIds = [];
if (this.modelType() == BaseModel.TYPE_NOTE) {
const conflictNotes = await this.db().selectAll('SELECT id FROM notes WHERE id IN ("' + ids.join('","') + '") AND is_conflict = 1');
2018-03-09 17:49:35 +00:00
conflictNoteIds = conflictNotes.map(n => {
return n.id;
});
2017-07-18 19:27:10 +00:00
}
2017-07-11 18:17:23 +00:00
await super.batchDelete(ids, options);
2017-07-03 20:50:45 +01:00
if (trackDeleted) {
2018-03-09 17:49:35 +00:00
const syncTargetIds = Setting.enumOptionValues("sync.target");
2017-07-11 18:17:23 +00:00
let queries = [];
let now = time.unixMs();
for (let i = 0; i < ids.length; i++) {
2017-07-18 19:27:10 +00:00
if (conflictNoteIds.indexOf(ids[i]) >= 0) continue;
2017-07-19 20:15:55 +01:00
// For each deleted item, for each sync target, we need to add an entry in deleted_items.
// That way, each target can later delete the remote item.
for (let j = 0; j < syncTargetIds.length; j++) {
queries.push({
2018-03-09 17:49:35 +00:00
sql: "INSERT INTO deleted_items (item_type, item_id, deleted_time, sync_target) VALUES (?, ?, ?, ?)",
2017-07-19 20:15:55 +01:00
params: [this.modelType(), ids[i], now, syncTargetIds[j]],
});
}
2017-07-11 18:17:23 +00:00
}
await this.db().transactionExecBatch(queries);
2017-07-03 20:50:45 +01:00
}
}
2018-01-15 18:35:39 +00:00
// Note: Currently, once a deleted_items entry has been processed, it is removed from the database. In practice it means that
// the following case will not work as expected:
// - Client 1 creates a note and sync with target 1 and 2
// - Client 2 sync with target 1
// - Client 2 deletes note and sync with target 1
// - Client 1 syncs with target 1 only (note is deleted from local machine, as expected)
// - Client 1 syncs with target 2 only => the note is *not* deleted from target 2 because no information
// that it was previously deleted exist (deleted_items entry has been deleted).
// The solution would be to permanently store the list of deleted items on each client.
2017-07-19 20:15:55 +01:00
static deletedItems(syncTarget) {
2018-03-09 17:49:35 +00:00
return this.db().selectAll("SELECT * FROM deleted_items WHERE sync_target = ?", [syncTarget]);
2017-07-03 20:50:45 +01:00
}
2017-07-19 20:15:55 +01:00
static async deletedItemCount(syncTarget) {
2018-03-09 17:49:35 +00:00
let r = await this.db().selectOne("SELECT count(*) as total FROM deleted_items WHERE sync_target = ?", [syncTarget]);
return r["total"];
2017-07-10 20:16:59 +01:00
}
2017-07-19 20:15:55 +01:00
static remoteDeletedItem(syncTarget, itemId) {
2018-03-09 17:49:35 +00:00
return this.db().exec("DELETE FROM deleted_items WHERE item_id = ? AND sync_target = ?", [itemId, syncTarget]);
2017-07-03 20:50:45 +01:00
}
2017-06-18 23:06:10 +01:00
static serialize_format(propName, propValue) {
2018-03-09 17:49:35 +00:00
if (["created_time", "updated_time", "sync_time", "user_updated_time", "user_created_time"].indexOf(propName) >= 0) {
if (!propValue) return "";
propValue =
moment
.unix(propValue / 1000)
.utc()
.format("YYYY-MM-DDTHH:mm:ss.SSS") + "Z";
2017-06-15 19:18:48 +01:00
} else if (propValue === null || propValue === undefined) {
2018-03-09 17:49:35 +00:00
propValue = "";
2017-06-15 19:18:48 +01:00
}
return propValue;
}
2017-06-18 23:06:10 +01:00
static unserialize_format(type, propName, propValue) {
2018-03-09 17:49:35 +00:00
if (propName[propName.length - 1] == "_") return propValue; // Private property
2017-06-15 19:18:48 +01:00
2017-06-18 23:06:10 +01:00
let ItemClass = this.itemClass(type);
2018-03-09 17:49:35 +00:00
if (["created_time", "updated_time", "user_created_time", "user_updated_time"].indexOf(propName) >= 0) {
2017-06-15 19:18:48 +01:00
if (!propValue) return 0;
2018-03-09 17:49:35 +00:00
propValue = moment(propValue, "YYYY-MM-DDTHH:mm:ss.SSSZ").format("x");
2017-06-15 19:18:48 +01:00
} else {
2017-06-18 23:06:10 +01:00
propValue = Database.formatValue(ItemClass.fieldType(propName), propValue);
2017-06-15 19:18:48 +01:00
}
return propValue;
}
2017-07-02 16:46:03 +01:00
static async serialize(item, type = null, shownKeys = null) {
2017-06-29 21:52:52 +01:00
item = this.filter(item);
2017-07-13 22:26:45 +01:00
let output = {};
2017-06-15 19:18:48 +01:00
2018-03-09 17:49:35 +00:00
if ("title" in item && shownKeys.indexOf("title") >= 0) {
2017-07-13 22:26:45 +01:00
output.title = item.title;
2017-07-03 21:38:26 +01:00
}
2018-03-09 17:49:35 +00:00
if ("body" in item && shownKeys.indexOf("body") >= 0) {
2017-07-13 22:26:45 +01:00
output.body = item.body;
2017-07-03 21:38:26 +01:00
}
2017-07-13 22:26:45 +01:00
output.props = [];
2017-06-15 19:18:48 +01:00
for (let i = 0; i < shownKeys.length; i++) {
2017-07-02 19:38:34 +01:00
let key = shownKeys[i];
2018-03-09 17:49:35 +00:00
if (key == "title" || key == "body") continue;
2017-07-02 19:38:34 +01:00
let value = null;
2018-03-09 17:49:35 +00:00
if (typeof key === "function") {
2017-07-02 19:38:34 +01:00
let r = await key();
key = r.key;
value = r.value;
} else {
value = this.serialize_format(key, item[key]);
}
2018-03-09 17:49:35 +00:00
output.props.push(key + ": " + value);
2017-06-15 19:18:48 +01:00
}
2017-07-13 22:26:45 +01:00
let temp = [];
if (output.title) temp.push(output.title);
if (output.body) temp.push(output.body);
if (output.props.length) temp.push(output.props.join("\n"));
return temp.join("\n\n");
2017-06-15 19:18:48 +01:00
}
static encryptionService() {
2018-03-09 17:49:35 +00:00
if (!this.encryptionService_) throw new Error("BaseItem.encryptionService_ is not set!!");
return this.encryptionService_;
}
static async serializeForSync(item) {
const ItemClass = this.itemClass(item);
let serialized = await ItemClass.serialize(item);
2018-03-09 17:49:35 +00:00
if (!Setting.value("encryption.enabled") || !ItemClass.encryptionSupported()) {
// Normally not possible since itemsThatNeedSync should only return decrypted items
2018-03-09 17:49:35 +00:00
if (!!item.encryption_applied) throw new JoplinError("Item is encrypted but encryption is currently disabled", "cannotSyncEncrypted");
return serialized;
}
2018-03-09 17:49:35 +00:00
if (!!item.encryption_applied) {
const e = new Error("Trying to encrypt item that is already encrypted");
e.code = "cannotEncryptEncrypted";
throw e;
}
const cipherText = await this.encryptionService().encryptString(serialized);
// List of keys that won't be encrypted - mostly foreign keys required to link items
// with each others and timestamp required for synchronisation.
2018-03-09 17:49:35 +00:00
const keepKeys = ["id", "note_id", "tag_id", "parent_id", "updated_time", "type_"];
const reducedItem = {};
for (let i = 0; i < keepKeys.length; i++) {
const n = keepKeys[i];
if (!item.hasOwnProperty(n)) continue;
reducedItem[n] = item[n];
}
reducedItem.encryption_applied = 1;
reducedItem.encryption_cipher_text = cipherText;
2018-03-09 17:49:35 +00:00
return ItemClass.serialize(reducedItem);
}
static async decrypt(item) {
2018-03-09 17:49:35 +00:00
if (!item.encryption_cipher_text) throw new Error("Item is not encrypted: " + item.id);
const ItemClass = this.itemClass(item);
const plainText = await this.encryptionService().decryptString(item.encryption_cipher_text);
// Note: decryption does not count has a change, so don't update any timestamp
const plainItem = await ItemClass.unserialize(plainText);
plainItem.updated_time = item.updated_time;
2018-03-09 17:49:35 +00:00
plainItem.encryption_cipher_text = "";
plainItem.encryption_applied = 0;
return ItemClass.save(plainItem, { autoTimestamp: false });
}
2017-07-02 16:46:03 +01:00
static async unserialize(content) {
2017-06-15 19:18:48 +01:00
let lines = content.split("\n");
let output = {};
2018-03-09 17:49:35 +00:00
let state = "readingProps";
2017-06-15 19:18:48 +01:00
let body = [];
2017-07-05 19:31:11 +01:00
2017-06-15 19:18:48 +01:00
for (let i = lines.length - 1; i >= 0; i--) {
let line = lines[i];
2018-03-09 17:49:35 +00:00
if (state == "readingProps") {
2017-06-15 19:18:48 +01:00
line = line.trim();
2018-03-09 17:49:35 +00:00
if (line == "") {
state = "readingBody";
2017-06-15 19:18:48 +01:00
continue;
}
2018-03-09 17:49:35 +00:00
let p = line.indexOf(":");
if (p < 0) throw new Error("Invalid property format: " + line + ": " + content);
2017-06-15 19:18:48 +01:00
let key = line.substr(0, p).trim();
let value = line.substr(p + 1).trim();
2017-06-18 23:06:10 +01:00
output[key] = value;
2018-03-09 17:49:35 +00:00
} else if (state == "readingBody") {
2017-06-15 19:18:48 +01:00
body.splice(0, 0, line);
}
}
2018-03-09 17:49:35 +00:00
if (!output.type_) throw new Error("Missing required property: type_: " + content);
2017-06-18 23:06:10 +01:00
output.type_ = Number(output.type_);
2017-07-03 21:38:26 +01:00
if (body.length) {
let title = body.splice(0, 2);
output.title = title[0];
}
if (output.type_ === BaseModel.TYPE_NOTE) output.body = body.join("\n");
2017-06-15 19:18:48 +01:00
2017-06-18 23:06:10 +01:00
for (let n in output) {
if (!output.hasOwnProperty(n)) continue;
2017-07-02 16:46:03 +01:00
output[n] = await this.unserialize_format(output.type_, n, output[n]);
2017-06-18 23:06:10 +01:00
}
2017-06-15 19:18:48 +01:00
return output;
}
2017-12-24 09:36:31 +01:00
static async encryptedItemsStats() {
const classNames = this.encryptableItemClassNames();
let encryptedCount = 0;
let totalCount = 0;
for (let i = 0; i < classNames.length; i++) {
const ItemClass = this.getClass(classNames[i]);
2018-03-09 17:49:35 +00:00
encryptedCount += await ItemClass.count({ where: "encryption_applied = 1" });
2017-12-24 09:36:31 +01:00
totalCount += await ItemClass.count();
}
return {
encrypted: encryptedCount,
total: totalCount,
};
}
static async encryptedItemsCount() {
const classNames = this.encryptableItemClassNames();
let output = 0;
for (let i = 0; i < classNames.length; i++) {
const className = classNames[i];
const ItemClass = this.getClass(className);
2018-03-09 17:49:35 +00:00
const count = await ItemClass.count({ where: "encryption_applied = 1" });
2017-12-24 09:36:31 +01:00
output += count;
}
return output;
}
static async hasEncryptedItems() {
const classNames = this.encryptableItemClassNames();
for (let i = 0; i < classNames.length; i++) {
const className = classNames[i];
const ItemClass = this.getClass(className);
2018-03-09 17:49:35 +00:00
const count = await ItemClass.count({ where: "encryption_applied = 1" });
if (count) return true;
}
return false;
}
static async itemsThatNeedDecryption(exclusions = [], limit = 100) {
const classNames = this.encryptableItemClassNames();
for (let i = 0; i < classNames.length; i++) {
const className = classNames[i];
const ItemClass = this.getClass(className);
2018-03-09 17:49:35 +00:00
const whereSql = className === "Resource" ? ["(encryption_blob_encrypted = 1 OR encryption_applied = 1)"] : ["encryption_applied = 1"];
if (exclusions.length) whereSql.push('id NOT IN ("' + exclusions.join('","') + '")');
2018-03-09 17:49:35 +00:00
const sql = sprintf(
`
SELECT *
FROM %s
WHERE %s
LIMIT %d
`,
this.db().escapeField(ItemClass.tableName()),
2018-03-09 17:49:35 +00:00
whereSql.join(" AND "),
limit
);
const items = await ItemClass.modelSelectAll(sql);
if (i >= classNames.length - 1) {
return { hasMore: items.length >= limit, items: items };
} else {
if (items.length) return { hasMore: true, items: items };
}
}
2018-03-09 17:49:35 +00:00
throw new Error("Unreachable");
}
static async itemsThatNeedSync(syncTarget, limit = 100) {
const classNames = this.syncItemClassNames();
for (let i = 0; i < classNames.length; i++) {
const className = classNames[i];
const ItemClass = this.getClass(className);
2018-03-09 17:49:35 +00:00
let fieldNames = ItemClass.fieldNames("items");
2017-07-19 20:15:55 +01:00
// // NEVER SYNCED:
// 'SELECT * FROM [ITEMS] WHERE id NOT INT (SELECT item_id FROM sync_items WHERE sync_target = ?)'
// // CHANGED:
// 'SELECT * FROM [ITEMS] items JOIN sync_items s ON s.item_id = items.id WHERE sync_target = ? AND'
let extraWhere = [];
2018-03-09 17:49:35 +00:00
if (className == "Note") extraWhere.push("is_conflict = 0");
if (className == "Resource") extraWhere.push("encryption_blob_encrypted = 0");
if (ItemClass.encryptionSupported()) extraWhere.push("encryption_applied = 0");
2018-03-09 17:49:35 +00:00
extraWhere = extraWhere.length ? "AND " + extraWhere.join(" AND ") : "";
2017-07-17 23:22:22 +01:00
2017-07-19 20:15:55 +01:00
// First get all the items that have never been synced under this sync target
2018-03-09 17:49:35 +00:00
let sql = sprintf(
`
2017-07-19 20:15:55 +01:00
SELECT %s
FROM %s items
WHERE id NOT IN (
SELECT item_id FROM sync_items WHERE sync_target = %d
)
%s
LIMIT %d
`,
2018-03-09 17:49:35 +00:00
this.db().escapeFields(fieldNames),
this.db().escapeField(ItemClass.tableName()),
Number(syncTarget),
extraWhere,
limit
);
2017-07-19 20:15:55 +01:00
let neverSyncedItem = await ItemClass.modelSelectAll(sql);
// Secondly get the items that have been synced under this sync target but that have been changed since then
const newLimit = limit - neverSyncedItem.length;
let changedItems = [];
if (newLimit > 0) {
2018-03-09 17:49:35 +00:00
fieldNames.push("sync_time");
2017-07-19 20:15:55 +01:00
2018-03-09 17:49:35 +00:00
let sql = sprintf(
`
2017-07-19 20:15:55 +01:00
SELECT %s FROM %s items
JOIN sync_items s ON s.item_id = items.id
WHERE sync_target = %d
AND (s.sync_time < items.updated_time OR force_sync = 1)
AND s.sync_disabled = 0
2017-07-19 20:15:55 +01:00
%s
LIMIT %d
`,
2018-03-09 17:49:35 +00:00
this.db().escapeFields(fieldNames),
this.db().escapeField(ItemClass.tableName()),
Number(syncTarget),
extraWhere,
newLimit
);
2017-07-19 20:15:55 +01:00
changedItems = await ItemClass.modelSelectAll(sql);
}
const items = neverSyncedItem.concat(changedItems);
if (i >= classNames.length - 1) {
return { hasMore: items.length >= limit, items: items };
} else {
if (items.length) return { hasMore: true, items: items };
}
}
2018-03-09 17:49:35 +00:00
throw new Error("Unreachable");
2017-06-18 23:06:10 +01:00
}
2017-07-03 23:08:14 +01:00
static syncItemClassNames() {
2018-03-09 17:49:35 +00:00
return BaseItem.syncItemDefinitions_.map(def => {
2017-07-03 23:08:14 +01:00
return def.className;
});
}
static encryptableItemClassNames() {
const temp = this.syncItemClassNames();
let output = [];
for (let i = 0; i < temp.length; i++) {
2018-03-09 17:49:35 +00:00
if (temp[i] === "MasterKey") continue;
output.push(temp[i]);
}
return output;
}
2017-08-20 16:29:18 +02:00
static syncItemTypes() {
2018-03-09 17:49:35 +00:00
return BaseItem.syncItemDefinitions_.map(def => {
2017-08-20 16:29:18 +02:00
return def.type;
});
}
2017-07-14 18:02:45 +00:00
static modelTypeToClassName(type) {
for (let i = 0; i < BaseItem.syncItemDefinitions_.length; i++) {
if (BaseItem.syncItemDefinitions_[i].type == type) return BaseItem.syncItemDefinitions_[i].className;
}
2018-03-09 17:49:35 +00:00
throw new Error("Invalid type: " + type);
2017-07-14 18:02:45 +00:00
}
static async syncDisabledItems(syncTargetId) {
2018-03-09 17:49:35 +00:00
const rows = await this.db().selectAll("SELECT * FROM sync_items WHERE sync_disabled = 1 AND sync_target = ?", [syncTargetId]);
let output = [];
for (let i = 0; i < rows.length; i++) {
2017-12-05 18:56:39 +00:00
const item = await this.loadItem(rows[i].item_type, rows[i].item_id);
if (!item) continue; // The referenced item no longer exist
output.push({
syncInfo: rows[i],
item: item,
});
}
return output;
}
2018-03-09 17:49:35 +00:00
static updateSyncTimeQueries(syncTarget, item, syncTime, syncDisabled = false, syncDisabledReason = "") {
const itemType = item.type_;
const itemId = item.id;
2018-03-09 17:49:35 +00:00
if (!itemType || !itemId || syncTime === undefined) throw new Error("Invalid parameters in updateSyncTimeQueries()");
return [
{
2018-03-09 17:49:35 +00:00
sql: "DELETE FROM sync_items WHERE sync_target = ? AND item_type = ? AND item_id = ?",
params: [syncTarget, itemType, itemId],
},
{
2018-03-09 17:49:35 +00:00
sql: "INSERT INTO sync_items (sync_target, item_type, item_id, sync_time, sync_disabled, sync_disabled_reason) VALUES (?, ?, ?, ?, ?, ?)",
params: [syncTarget, itemType, itemId, syncTime, syncDisabled ? 1 : 0, syncDisabledReason + ""],
},
];
}
static async saveSyncTime(syncTarget, item, syncTime) {
const queries = this.updateSyncTimeQueries(syncTarget, item, syncTime);
return this.db().transactionExecBatch(queries);
}
static async saveSyncDisabled(syncTargetId, item, syncDisabledReason) {
2018-03-09 17:49:35 +00:00
const syncTime = "sync_time" in item ? item.sync_time : 0;
const queries = this.updateSyncTimeQueries(syncTargetId, item, syncTime, true, syncDisabledReason);
return this.db().transactionExecBatch(queries);
}
// When an item is deleted, its associated sync_items data is not immediately deleted for
// performance reason. So this function is used to look for these remaining sync_items and
// delete them.
static async deleteOrphanSyncItems() {
const classNames = this.syncItemClassNames();
let queries = [];
for (let i = 0; i < classNames.length; i++) {
const className = classNames[i];
const ItemClass = this.getClass(className);
2018-03-09 17:49:35 +00:00
let selectSql = "SELECT id FROM " + ItemClass.tableName();
if (ItemClass.modelType() == this.TYPE_NOTE) selectSql += " WHERE is_conflict = 0";
2017-07-16 17:06:05 +01:00
2018-03-09 17:49:35 +00:00
queries.push("DELETE FROM sync_items WHERE item_type = " + ItemClass.modelType() + " AND item_id NOT IN (" + selectSql + ")");
}
await this.db().transactionExecBatch(queries);
}
static displayTitle(item) {
2018-03-09 17:49:35 +00:00
if (!item) return "";
return !!item.encryption_applied ? "🔑 " + _("Encrypted") : item.title + "";
}
static async markAllNonEncryptedForSync() {
const classNames = this.encryptableItemClassNames();
for (let i = 0; i < classNames.length; i++) {
const className = classNames[i];
const ItemClass = this.getClass(className);
2018-03-09 17:49:35 +00:00
const sql = sprintf(
`
SELECT id
FROM %s
WHERE encryption_applied = 0`,
this.db().escapeField(ItemClass.tableName())
);
const items = await ItemClass.modelSelectAll(sql);
2018-03-09 17:49:35 +00:00
const ids = items.map(item => {
return item.id;
});
if (!ids.length) continue;
await this.db().exec('UPDATE sync_items SET force_sync = 1 WHERE item_id IN ("' + ids.join('","') + '")');
}
}
static async forceSync(itemId) {
2018-03-09 17:49:35 +00:00
await this.db().exec("UPDATE sync_items SET force_sync = 1 WHERE item_id = ?", [itemId]);
}
static async forceSyncAll() {
2018-03-09 17:49:35 +00:00
await this.db().exec("UPDATE sync_items SET force_sync = 1");
}
static async save(o, options = null) {
if (!options) options = {};
if (options.userSideValidation === true) {
2018-03-09 17:49:35 +00:00
if (!!o.encryption_applied) throw new Error(_("Encrypted items cannot be modified"));
}
return super.save(o, options);
}
2017-06-15 19:18:48 +01:00
}
BaseItem.encryptionService_ = null;
2017-07-03 23:08:14 +01:00
// Also update:
// - itemsThatNeedSync()
// - syncedItems()
BaseItem.syncItemDefinitions_ = [
2018-03-09 17:49:35 +00:00
{ type: BaseModel.TYPE_NOTE, className: "Note" },
{ type: BaseModel.TYPE_FOLDER, className: "Folder" },
{ type: BaseModel.TYPE_RESOURCE, className: "Resource" },
{ type: BaseModel.TYPE_TAG, className: "Tag" },
{ type: BaseModel.TYPE_NOTE_TAG, className: "NoteTag" },
{ type: BaseModel.TYPE_MASTER_KEY, className: "MasterKey" },
2017-07-03 23:08:14 +01:00
];
2018-03-09 17:49:35 +00:00
module.exports = BaseItem;